Help us shape the future of Go! Take the 2026 Go Developer Survey

hook

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 8 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func NewContext(ctx context.Context, registry *Registry) context.Context

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

func FromContext(ctx context.Context) (*Registry, bool)

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

func (registry *Registry) AfterResponse(callbacks ...func(error))

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

func (registry *Registry) RunAfterResponse(err error)

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL