Documentation
¶
Overview ¶
Package hook implements the request lifecycle behind router.Handler and the public request.Hooks registration interface. Each request owns a Registry; callbacks run in reverse registration order from snapshots taken under a mutex, so callbacks can register further hooks without deadlocking.
ResponseWriter dispatches final-status and body callbacks while delegating native capabilities through Unwrap. Informational statuses other than 101 leave the final response pending; successful hijacking commits ownership to the caller. Its commitment state supports concurrent observation, not arbitrary concurrent writes to a native HTTP response.
Example:
registry := hook.New()
registry.BeforeWriteHeader(func(writer http.ResponseWriter, status int) {
writer.Header().Set("X-Service", "example")
})
writer := hook.NewResponseWriter(httptest.NewRecorder(), registry)
writer.WriteHeader(http.StatusNoContent)
Index ¶
- Variables
- func NewContext(ctx context.Context, registry *Registry) context.Context
- type Registry
- func (registry *Registry) AfterResponse(callbacks ...func(error))
- func (registry *Registry) BeforeWrite(callbacks ...func(http.ResponseWriter, []byte))
- func (registry *Registry) BeforeWriteHeader(callbacks ...func(http.ResponseWriter, int))
- func (registry *Registry) RunAfterResponse(err error)
- func (registry *Registry) RunBeforeWrite(writer http.ResponseWriter, content []byte)
- func (registry *Registry) RunBeforeWriteHeader(writer http.ResponseWriter, status int)
- type ResponseWriter
- func (writer *ResponseWriter) Flush()
- func (writer *ResponseWriter) FlushError() error
- func (writer *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
- func (writer *ResponseWriter) Unwrap() http.ResponseWriter
- func (writer *ResponseWriter) Write(content []byte) (int, error)
- func (writer *ResponseWriter) WriteHeader(status int)
- func (writer *ResponseWriter) WriteHeaderCalled() bool
Constants ¶
This section is empty.
Variables ¶
var ErrNoHooks = problem.New(func(details *problem.Builder) { details.Type("tag:arkoren.dev,2026-09:core:internal/hook:no-hooks") details.Title("No hooks context") details.Detail("Unable to resolve hooks as there's no context value") details.Status(http.StatusInternalServerError) })
ErrNoHooks is the problem raised when hooks are required on a request that was not served through a router.Handler, which installs them.
Example:
failure := hook.ErrNoHooks.Derive(func(details *problem.Builder) {
details.Detail("The request cannot be completed.")
})
_ = failure.Status()
Functions ¶
func NewContext ¶
NewContext derives a context carrying registry without copying it or losing parent cancellation and values. The caller supplies a non-nil parent and a live registry; the request owner remains responsible for running its phases.
Example:
registry := hook.New() ctx := hook.NewContext(context.Background(), registry) resolved, ok := hook.FromContext(ctx) fmt.Println(ok, resolved == registry) // true true
Types ¶
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry stores callbacks for one HTTP request lifecycle. Registration and snapshot creation are synchronized; callbacks run without holding its mutex and in reverse registration order. Function captures remain shared, and nil callbacks panic when invoked. Do not copy a Registry after first use.
Example:
registry := hook.New()
registry.AfterResponse(func(err error) {
fmt.Println(err == nil)
})
registry.RunAfterResponse(nil)
func FromContext ¶
FromContext returns the shared lifecycle registry attached by NewContext. A context without that value returns nil and false; it does not create a new registry. Callers requiring hooks report ErrNoHooks when no value exists.
Example:
ctx := hook.NewContext(context.Background(), hook.New())
if registry, ok := hook.FromContext(ctx); ok {
registry.AfterResponse(func(err error) {
fmt.Println(err == nil)
})
}
func New ¶
func New() *Registry
New creates an empty lifecycle registry for one request. It performs no I/O and retains no request until the caller registers callbacks or adds it to a context. The request owner controls when each phase runs.
Example:
registry := hook.New() writer := hook.NewResponseWriter(httptest.NewRecorder(), registry) writer.WriteHeader(http.StatusNoContent)
func (*Registry) AfterResponse ¶
AfterResponse appends callbacks for the response-finalization phase. They receive the handler error in reverse registration order, including when the router is unwinding a panic. Registration retains the functions and captures; it does not execute callbacks or transfer their resources to the registry.
Example:
registry := hook.New()
registry.AfterResponse(func(err error) {
fmt.Println(err == nil)
})
registry.RunAfterResponse(nil)
func (*Registry) BeforeWrite ¶
func (registry *Registry) BeforeWrite(callbacks ...func(http.ResponseWriter, []byte))
BeforeWrite appends callbacks for each body write. They receive the native writer and the borrowed bytes about to be sent, in reverse registration order. Copy bytes before retaining them beyond the callback; registration retains the callback functions and does not execute them.
Example:
registry := hook.New()
registry.BeforeWrite(func(writer http.ResponseWriter, body []byte) {
fmt.Println(len(body))
})
registry.RunBeforeWrite(httptest.NewRecorder(), []byte("hello"))
func (*Registry) BeforeWriteHeader ¶
func (registry *Registry) BeforeWriteHeader(callbacks ...func(http.ResponseWriter, int))
BeforeWriteHeader appends callbacks that may edit headers before the final response status is written. ResponseWriter skips this phase for informational statuses other than 101. Registration retains the functions; invocation uses reverse registration order and does not hold the registry mutex.
Example:
registry := hook.New()
registry.BeforeWriteHeader(func(writer http.ResponseWriter, status int) {
writer.Header().Set("X-Service", "example")
})
writer := hook.NewResponseWriter(httptest.NewRecorder(), registry)
writer.WriteHeader(http.StatusNoContent)
func (*Registry) RunAfterResponse ¶
RunAfterResponse invokes a snapshot in last-in, first-out order with err. It does not clear registrations or recover callback panics. The request owner calls it once during finalization; registrations made by callbacks wait for a subsequent invocation rather than changing the current snapshot.
Example:
registry := hook.New()
registry.AfterResponse(func(err error) {
fmt.Println(err == nil)
})
registry.RunAfterResponse(nil)
func (*Registry) RunBeforeWrite ¶
func (registry *Registry) RunBeforeWrite(writer http.ResponseWriter, content []byte)
RunBeforeWrite invokes a last-in, first-out callback snapshot for one body write. It borrows content and does not write it, clear registrations, or recover panics; ResponseWriter performs the actual write after this phase.
Example:
registry := hook.New()
registry.BeforeWrite(func(writer http.ResponseWriter, body []byte) {
fmt.Println(len(body))
})
registry.RunBeforeWrite(httptest.NewRecorder(), []byte("hello"))
func (*Registry) RunBeforeWriteHeader ¶
func (registry *Registry) RunBeforeWriteHeader(writer http.ResponseWriter, status int)
RunBeforeWriteHeader invokes a last-in, first-out callback snapshot with the native writer and final status. It neither validates nor writes the status; ResponseWriter owns those operations. Callback panics propagate to the caller.
Example:
registry := hook.New()
registry.BeforeWriteHeader(func(writer http.ResponseWriter, status int) {
writer.Header().Set("X-Service", "example")
})
registry.RunBeforeWriteHeader(httptest.NewRecorder(), http.StatusNoContent)
type ResponseWriter ¶
type ResponseWriter struct {
// ResponseWriter is the underlying writer that receives delegated writes.
// Accessing it directly bypasses this wrapper's callbacks and commitment
// tracking; use Unwrap-based controllers only for native operations.
//
// Example:
//
// native := httptest.NewRecorder()
// writer := hook.NewResponseWriter(native, hook.New())
// fmt.Println(writer.ResponseWriter == native) // true
http.ResponseWriter
// contains filtered or unexported fields
}
ResponseWriter dispatches lifecycle callbacks around writes to an underlying HTTP response. Before-header hooks run for the final status only, including 101; other informational statuses do not commit the final response. Successful hijacking also suppresses later HTTP fallback writes. Unwrap lets response controllers reach native capabilities without hiding their errors. Do not copy a ResponseWriter after first use.
Example:
registry := hook.New()
registry.BeforeWriteHeader(func(writer http.ResponseWriter, status int) {
writer.Header().Set("X-Service", "example")
})
writer := hook.NewResponseWriter(httptest.NewRecorder(), registry)
writer.WriteHeader(http.StatusNoContent)
func Lookup ¶
func Lookup(writer http.ResponseWriter) *ResponseWriter
Lookup returns the hook writer in writer's chain, looking through wrappers that expose Unwrap. It returns the nearest hook writer, or nil when none is reachable. Opaque wrappers stop the search; Unwrap chains must be acyclic.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New()) fmt.Println(hook.Lookup(writer) == writer) // true
func NewResponseWriter ¶
func NewResponseWriter(writer http.ResponseWriter, registry *Registry) *ResponseWriter
NewResponseWriter wraps a native writer and live registry without copying either. It performs no writes until called; neither the native writer nor the registry may be nil. The owner must serialize writes as net/http requires.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New()) writer.WriteHeader(http.StatusNoContent) fmt.Println(writer.WriteHeaderCalled()) // true
func (*ResponseWriter) Flush ¶
func (writer *ResponseWriter) Flush()
Flush implements http.Flusher by committing an implicit 200 and flushing the native writer through its controller. The interface cannot return failures; use FlushError when the caller needs to handle unsupported or failed flushes.
Example:
native := httptest.NewRecorder() writer := hook.NewResponseWriter(native, hook.New()) writer.Flush() fmt.Println(native.Flushed) // true
func (*ResponseWriter) FlushError ¶
func (writer *ResponseWriter) FlushError() error
FlushError commits an implicit 200 when needed and flushes the native writer through http.ResponseController. It returns native errors unchanged, including errors matching http.ErrNotSupported; the response may already be committed.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New())
if err := writer.FlushError(); err != nil {
return err
}
func (*ResponseWriter) Hijack ¶
func (writer *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
Hijack delegates connection hijacking through native Unwrap layers and marks the response as started only on success. Failures preserve the underlying error and leave an unwritten response available for ordinary error handling. The caller owns and must close a successfully hijacked connection.
Example:
writer := hook.NewResponseWriter(native, hook.New()) // native is the server writer
connection, _, err := writer.Hijack()
if err != nil {
return err
}
defer connection.Close()
func (*ResponseWriter) Unwrap ¶
func (writer *ResponseWriter) Unwrap() http.ResponseWriter
Unwrap returns the retained native writer so http.ResponseController can discover its capabilities. Direct writes through the result bypass hooks and commitment tracking, so normal HTTP output must use this wrapper instead.
Example:
native := httptest.NewRecorder() writer := hook.NewResponseWriter(native, hook.New()) fmt.Println(writer.Unwrap() == native) // true
func (*ResponseWriter) Write ¶
func (writer *ResponseWriter) Write(content []byte) (int, error)
Write commits an implicit 200 when needed, invokes body callbacks, and delegates content to the native writer. Native byte counts and errors are returned unchanged. Content is borrowed for this call, not retained here.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New())
if _, err := writer.Write([]byte("hello")); err != nil {
return err
}
func (*ResponseWriter) WriteHeader ¶
func (writer *ResponseWriter) WriteHeader(status int)
WriteHeader sends status. Before-header callbacks run once, for the final status only; informational 1xx statuses pass straight through. Status 101 is final, matching net/http's protocol-switch behavior. As with net/http, callers must serialize writes and supply a valid status code.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New()) writer.WriteHeader(http.StatusNoContent)
func (*ResponseWriter) WriteHeaderCalled ¶
func (writer *ResponseWriter) WriteHeaderCalled() bool
WriteHeaderCalled reports whether a final response has started or the connection was successfully hijacked. Informational statuses other than 101 leave it false. Reading this state is safe while another goroutine writes; this does not make arbitrary concurrent ResponseWriter writes safe.
Example:
writer := hook.NewResponseWriter(httptest.NewRecorder(), hook.New()) writer.WriteHeader(http.StatusNoContent) fmt.Println(writer.WriteHeaderCalled()) // true