Documentation
¶
Overview ¶
Package router provides an HTTP router with error-returning handlers, middleware, and centrally rendered Problem Details errors.
New creates a Router. Handler errors are rendered in one place, and standard http.Handler values can be adapted with HTTP. Middleware chains are shared across requests: registered routes capture one chain per registration, and unmatched requests share a separate chain built on first use. HTTPMiddleware constructs native middleware at chain creation, not once per request; its handlers must support concurrent requests. Forwarded request contexts must retain the original context's values, which carry lifecycle hooks and middleware state; derive them from request.Context.
Router.Mount serves a standard http.Handler under a prefix, which is stripped, inside the router's middleware. Router.MCPTools returns a live route listing for explicit MCP registration; callers own access policy.
Router.Routes lists the registered routes with their handler functions, for route listings and inspection tools. It returns an eager collection snapshot in registration order, not a live query. Registration must not run concurrently with inspection; the returned slice is detached from the tree. Optional trailing OperationBuilderFunc callbacks describe operations for Router.OpenAPI without changing request execution. Export emits OpenAPI 3.2.1 JSON from annotated, nonhidden registrations only; schemas are rendered at declaration time, never evaluated or remotely fetched. Opaque catch-all routes and mounts must be omitted or hidden. Router.OpenAPIHandler caches export on its first request; applications own route setup and access policy.
Example:
routes := router.New()
routes.Get("/health", func(writer http.ResponseWriter, request *http.Request) error {
writer.WriteHeader(http.StatusNoContent)
return nil
}, func(operation *router.OperationBuilder) {
operation.ID("health").Response(http.StatusNoContent, "Healthy")
})
routes.Get("/openapi.json", routes.OpenAPIHandler("Example", "1"))
With RecoverBuilder.Events, Recover publishes PanicRecovered ("http.panic.recovered") with the converted error and the stack for every recovered panic. These are sensitive server-side diagnostics, not client-safe payloads: restrict listener output and sanitize it before forwarding. The event stack and each Stack method result are detached from the error's stack.
Index ¶
- Constants
- Variables
- type HTTPHandlerError
- type HTTPStatusError
- type Handler
- type Middleware
- type OpenAPIBuilder
- type OpenAPIBuilderFunc
- type OperationBuilder
- func (operation *OperationBuilder) Body(media string, schema Schema) *OperationBuilder
- func (operation *OperationBuilder) BodyRequired(required bool) *OperationBuilder
- func (operation *OperationBuilder) Deprecated(deprecated bool) *OperationBuilder
- func (operation *OperationBuilder) Description(text string) *OperationBuilder
- func (operation *OperationBuilder) Example(media, name string, value any) *OperationBuilder
- func (operation *OperationBuilder) Hidden(hidden bool) *OperationBuilder
- func (operation *OperationBuilder) ID(id string) *OperationBuilder
- func (operation *OperationBuilder) JSONBody(schema Schema) *OperationBuilder
- func (operation *OperationBuilder) JSONResponse(status int, schema Schema) *OperationBuilder
- func (operation *OperationBuilder) MediaResponse(status int, media string, schema Schema) *OperationBuilder
- func (operation *OperationBuilder) Parameter(name, location string, schema Schema) *OperationBuilder
- func (operation *OperationBuilder) Problem(details problem.Details) *OperationBuilder
- func (operation *OperationBuilder) Response(status int, description string) *OperationBuilder
- func (operation *OperationBuilder) ResponseHeader(status int, name string, schema Schema) *OperationBuilder
- func (operation *OperationBuilder) Security(name string, scopes ...string) *OperationBuilder
- func (operation *OperationBuilder) Summary(text string) *OperationBuilder
- func (operation *OperationBuilder) Tags(tags ...string) *OperationBuilder
- type OperationBuilderFunc
- type PanicRecoveredEvent
- type ProvideFunc
- type RawSchema
- type RecoverBuilder
- type RecoverBuilderFunc
- type Route
- type Router
- func (router *Router) Any(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Clone() *Router
- func (router *Router) Connect(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Delete(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Get(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Group(pattern string, subrouter func(*Router))
- func (router *Router) Grouped(subrouter func(*Router))
- func (router *Router) Handler(method, pattern string) (handler Handler, ok bool)
- func (router *Router) HandlerMatch(request *http.Request) (handler Handler, ok bool)
- func (router *Router) Has(method, pattern string) bool
- func (router *Router) Head(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) MCPTools(prefix string) []mcp.Tool
- func (router *Router) Matches(request *http.Request) bool
- func (router *Router) Method(method string, pattern string, handler Handler, ...)
- func (router *Router) Methods(methods []string, pattern string, handler Handler, ...)
- func (router *Router) Mount(prefix string, handler http.Handler, configure ...OperationBuilderFunc)
- func (router *Router) OpenAPI(title, version string, configure ...OpenAPIBuilderFunc) ([]byte, error)
- func (router *Router) OpenAPIHandler(title, version string, configure ...OpenAPIBuilderFunc) Handler
- func (router *Router) Options(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Patch(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Post(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Put(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Query(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Record(request *http.Request) *http.Response
- func (router *Router) Routes() collection.Slice[Route]
- func (router *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request)
- func (router *Router) Trace(pattern string, handler Handler, configure ...OperationBuilderFunc)
- func (router *Router) Use(middlewares ...Middleware)
- func (router *Router) With(middlewares ...Middleware) *Router
- type Schema
Examples ¶
Constants ¶
const StatusClientClosedRequest = 499
StatusClientClosedRequest is the non-standard HTTP status code used when the request context ends before an error response is rendered. It is useful for server-side classification; a disconnected client may receive no response at all.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Status(router.StatusClientClosedRequest)
})
fmt.Println(failure.HTTPStatus()) // 499
Variables ¶
var ErrFailedRecovering = errors.New(
"failed recovering from unexpected error",
)
ErrFailedRecovering identifies a failure reading or marshaling a panic value during default conversion. The conversion failure remains in the error chain for server-side diagnosis; custom handlers define their own error identities.
Example:
err := errors.Join(router.ErrFailedRecovering, io.ErrUnexpectedEOF) fmt.Println(errors.Is(err, router.ErrFailedRecovering)) // true
var ErrRecoverUnexpected = errors.New("an unexpected error occurred")
ErrRecoverUnexpected identifies a panic converted by the default Recover handler, or a nil custom conversion result. The default conversion preserves the original cause separately; match this sentinel with errors.Is rather than comparing diagnostic text. The HTTP renderer does not expose plain error text.
Example:
err := fmt.Errorf("request failed: %w", router.ErrRecoverUnexpected)
fmt.Println(errors.Is(err, router.ErrRecoverUnexpected)) // true
var PanicRecovered = event.NewTopic[PanicRecoveredEvent]("http.panic.recovered")
PanicRecovered is published when Recover converts a panic into an error. net/http.ErrAbortHandler is re-raised and publishes nothing. It requires an explicitly supplied event bus; listener failures do not change the recovered error. Treat Err and Stack as sensitive server-side diagnostics.
Example:
bus := event.New()
_, err := bus.Listen(router.PanicRecovered, func(ctx context.Context, notice router.PanicRecoveredEvent) error {
slog.ErrorContext(ctx, "request panicked", "method", notice.Method, "pattern", notice.Pattern)
return nil
})
if err != nil {
return err
}
routes := router.New()
routes.Use(router.Recover(func(recovery *router.RecoverBuilder) {
recovery.Events(bus)
}))
Functions ¶
This section is empty.
Types ¶
type HTTPHandlerError ¶
HTTPHandlerError is an error that renders its own response. A handler that returns one has it served directly instead of a generated problem. Discovery follows the error chain; rendering runs only before the response is committed. The implementation owns content negotiation and client-safe output.
Example:
var failure router.HTTPHandlerError = problem.NotFound() response := httptest.NewRecorder() failure.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/users/7", nil)) fmt.Println(response.Code) // 404
Example ¶
ExampleHTTPHandlerError checks that a problem can own rendering through the public error interface, without requiring a router implementation detail.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"arkoren.dev/core/http/router"
"arkoren.dev/foundation/problem"
)
func main() {
var failure router.HTTPHandlerError = problem.NotFound()
response := httptest.NewRecorder()
failure.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/users/7", nil))
fmt.Println(response.Code)
}
Output: 404
type HTTPStatusError ¶
type HTTPStatusError interface {
error
// HTTPStatus returns the status code used for the error response. Return a
// valid HTTP status; the router passes it to the problem renderer.
//
// Example:
//
// var failure router.HTTPStatusError = problem.Forbidden()
// fmt.Println(failure.HTTPStatus()) // 403
HTTPStatus() int
}
HTTPStatusError supplies the HTTP status for centralized error rendering. The router discovers it through the error chain; its error text remains a server-side diagnostic unless the error also implements HTTPHandlerError.
Example:
var failure router.HTTPStatusError = problem.NotFound() fmt.Println(failure.HTTPStatus()) // 404
type Handler ¶
type Handler func(writer http.ResponseWriter, request *http.Request) error
Handler handles an HTTP request and may return an error for centralized rendering by ServeHTTP. Errors cannot replace a response already started. Returning nil without writing produces 204; use HTTP for net/http's default 200 behavior. Captures must support concurrent requests when shared by a server.
Example:
handler := router.Handler(func(http.ResponseWriter, *http.Request) error {
return problem.NotFound()
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/users/7", nil))
fmt.Println(response.Code) // 404
Example ¶
ExampleHandler checks the error-returning handler example through ServeHTTP, including centralized rendering rather than invoking a private renderer.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"arkoren.dev/core/http/router"
"arkoren.dev/foundation/problem"
)
func main() {
handler := router.Handler(func(http.ResponseWriter, *http.Request) error {
return problem.NotFound()
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/users/7", nil))
fmt.Println(response.Code)
}
Output: 404
func HTTP ¶
HTTP adapts a standard http.Handler to a Handler that always returns nil. As in net/http, a handler that writes nothing, or only headers, responds 200. Router.Routes reports the adapted handler, not the adapter.
Example:
handler := router.HTTP(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("X-Service", "example")
}))
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", nil))
fmt.Println(response.Code) // 200
func (Handler) Record ¶
Record runs the full handler lifecycle against an in-memory recorder and returns its HTTP response. No server or network is used. The caller owns the returned response body; request handling and panic behavior match ServeHTTP.
Example:
handler := router.HTTP(http.NotFoundHandler()) response := handler.Record(httptest.NewRequest(http.MethodGet, "/missing", nil)) defer response.Body.Close() fmt.Println(response.StatusCode) // 404
func (Handler) ServeHTTP ¶
func (handler Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request)
ServeHTTP implements http.Handler, running lifecycle hooks and rendering errors. After-response hooks run even when the handler panics. A recovered panic after the response started aborts the connection, so the client sees a failed response instead of a truncated one that looks complete. A recovery response rendered by HTTPMiddleware before any prior output is complete error handling, not evidence that output preceded the panic.
Example:
handler := router.Handler(func(writer http.ResponseWriter, request *http.Request) error {
writer.WriteHeader(http.StatusAccepted)
return nil
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/jobs", nil))
fmt.Println(response.Code) // 202
type Middleware ¶
Middleware constructs a handler around next. Registered routes capture the chain once and share it across requests and explicit methods; unmatched responses use their own lazily constructed chain. Request-specific state belongs inside the returned handler, not in a shared mutable capture. Forwarded requests must preserve context values used by hooks and adapters.
Example:
var header router.Middleware = func(next router.Handler) router.Handler {
return func(writer http.ResponseWriter, request *http.Request) error {
writer.Header().Set("X-Service", "example")
return next(writer, request)
}
}
routes := router.New()
routes.Use(header)
func HTTPMiddleware ¶
func HTTPMiddleware(middleware func(http.Handler) http.Handler) Middleware
HTTPMiddleware adapts standard HTTP middleware to Middleware. The native constructor runs once when the adapter wraps next, normally during route registration. Its handler and captured state are shared across requests, so they must support concurrent use, just as with net/http.
Errors from next are rendered inside the wrapped middleware, so it observes the real status and body, and are still returned for outer middleware and hooks. Recovered errors rendered before any prior response writes are wrapped to distinguish them from panics after commitment; errors.Is and errors.As preserve their identities and diagnostics. Native middleware must preserve the request context when calling next: it carries lifecycle hooks and the request-local error result. Rendering is skipped when next wrote a final response, or an enclosing native boundary observed commitment, including through opaque wrappers. Output buffered privately by native middleware before calling next is not observable until that middleware forwards it through a tracked boundary.
Example:
routes := router.New()
routes.Use(router.HTTPMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("X-Service", "example")
next.ServeHTTP(writer, request)
})
}))
func Provide ¶
func Provide(f ProvideFunc) Middleware
Provide resolves a context for each request and passes it to next. The resolver must derive it from the supplied request context so lifecycle hooks and upstream middleware state remain available. Resolver errors are returned without calling next; functions and their captured values remain shared.
Example:
type valueKey struct{}
routes := router.New()
routes.Use(router.Provide(func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
return context.WithValue(r.Context(), valueKey{}, "value"), nil
}))
func ProvideStatic ¶
func ProvideStatic(key any, val any) Middleware
ProvideStatic returns middleware that adds a static value to the request context while preserving upstream values and cancellation. The key and value are retained, not copied; mutable values must be safe for concurrent requests.
Example:
type serviceKey struct{}
routes := router.New()
routes.Use(router.ProvideStatic(serviceKey{}, "billing"))
func Recover ¶
func Recover(configure ...RecoverBuilderFunc) Middleware
Recover returns middleware that converts panics into ErrRecoverUnexpected, or into the error from RecoverBuilder.Handler. A nil conversion result falls back to ErrRecoverUnexpected, not success. http.ErrAbortHandler is re-raised so net/http can abort the connection. With RecoverBuilder.Events every converted panic publishes PanicRecovered.
Example:
routes := router.New() routes.Use(router.Recover()) // before handlers that can panic
type OpenAPIBuilder ¶
type OpenAPIBuilder struct {
// contains filtered or unexported fields
}
OpenAPIBuilder configures document-level metadata, not route behavior. Owned settings detach after callbacks; schemes are explicit and never inferred from authentication middleware.
Example:
data, err := routes.OpenAPI("Users", "1", func(document *router.OpenAPIBuilder) {
document.SecurityScheme("bearer", router.RawSchema(`{"type":"http","scheme":"bearer"}`))
})
if err != nil {
return err
}
_ = data
func (*OpenAPIBuilder) Description ¶
func (document *OpenAPIBuilder) Description(text string) *OpenAPIBuilder
Description replaces the document's Info description. It accepts CommonMark without interpreting it, and does not alter any operation description.
Example:
document.Description("API for the public user directory.")
func (*OpenAPIBuilder) SecurityScheme ¶
func (document *OpenAPIBuilder) SecurityScheme(name string, definition RawSchema) *OpenAPIBuilder
SecurityScheme declares a named HTTP, API-key, or mutual-TLS scheme as a JSON object. This bounded API rejects other scheme kinds rather than partially validating OAuth flows. No middleware is installed or inferred.
Example:
document.SecurityScheme("bearer", router.RawSchema(`{"type":"http","scheme":"bearer"}`))
type OpenAPIBuilderFunc ¶
type OpenAPIBuilderFunc = func(*OpenAPIBuilder)
OpenAPIBuilderFunc configures one document synchronously in order. Nil entries panic; an expanded nil slice leaves the defaults unchanged.
Example:
var configure router.OpenAPIBuilderFunc = func(document *router.OpenAPIBuilder) {
document.Description("Public API")
}
type OperationBuilder ¶
type OperationBuilder struct {
// contains filtered or unexported fields
}
OperationBuilder describes one registration, shared by its explicit methods. Errors are sticky and reported by OpenAPI, not by request execution. Schemas and examples are captured when supplied, and containers detach at publication.
Example:
routes.Get("/users", list, func(operation *router.OperationBuilder) {
operation.ID("listUsers").JSONResponse(200, router.RawSchema(`{"type":"array"}`))
})
func (*OperationBuilder) Body ¶
func (operation *OperationBuilder) Body(media string, schema Schema) *OperationBuilder
Body adds or replaces one request representation. Schema bytes are rendered immediately; errors remain sticky until export. No body parsing is added.
Example:
operation.Body("application/xml", router.RawSchema(`{"type":"string"}`))
func (*OperationBuilder) BodyRequired ¶
func (operation *OperationBuilder) BodyRequired(required bool) *OperationBuilder
BodyRequired describes whether the request body is required. This setting does not reject empty requests; handlers remain responsible for validation.
Example:
operation.BodyRequired(true)
func (*OperationBuilder) Deprecated ¶
func (operation *OperationBuilder) Deprecated(deprecated bool) *OperationBuilder
Deprecated marks the operation as deprecated in the document only. The route remains callable and no warning header is added.
Example:
operation.Deprecated(true)
func (*OperationBuilder) Description ¶
func (operation *OperationBuilder) Description(text string) *OperationBuilder
Description replaces the longer operation documentation. CommonMark is passed through without interpreting or sanitizing it.
Example:
operation.Description("Returns the current user's public profile.")
func (*OperationBuilder) Example ¶
func (operation *OperationBuilder) Example(media, name string, value any) *OperationBuilder
Example adds a named request-body example for an existing representation. The JSON encoding is captured immediately; encoding failures are sticky. Examples are descriptive and are not validated against schemas.
Example:
operation.Example("application/json", "newUser", map[string]string{"name": "Ada"})
func (*OperationBuilder) Hidden ¶
func (operation *OperationBuilder) Hidden(hidden bool) *OperationBuilder
Hidden excludes this registration from export, including its metadata errors. It does not hide the route from Routes or prevent requests.
Example:
operation.Hidden(true)
func (*OperationBuilder) ID ¶
func (operation *OperationBuilder) ID(id string) *OperationBuilder
ID sets the optional globally unique operation identifier. A registration expanded to multiple methods must omit ID or export reports duplicates.
Example:
operation.ID("getUser")
func (*OperationBuilder) JSONBody ¶
func (operation *OperationBuilder) JSONBody(schema Schema) *OperationBuilder
JSONBody describes an application/json request body without requiring it. Use BodyRequired to mark presence as required in the document.
Example:
operation.JSONBody(router.RawSchema(`{"type":"object"}`)).BodyRequired(true)
func (*OperationBuilder) JSONResponse ¶
func (operation *OperationBuilder) JSONResponse(status int, schema Schema) *OperationBuilder
JSONResponse describes application/json output at status. It never marshals handler values, validates output, or adds automatic error responses.
Example:
operation.JSONResponse(200, router.RawSchema(`{"type":"object"}`))
func (*OperationBuilder) MediaResponse ¶
func (operation *OperationBuilder) MediaResponse(status int, media string, schema Schema) *OperationBuilder
MediaResponse adds a response representation without choosing an encoder. Repeated calls with different media types describe content negotiation.
Example:
operation.MediaResponse(200, "text/plain", router.RawSchema(`{"type":"string"}`))
func (*OperationBuilder) Parameter ¶
func (operation *OperationBuilder) Parameter(name, location string, schema Schema) *OperationBuilder
Parameter appends a named path, query, header, or cookie parameter. Path parameters are always required; others are optional. The final group path is checked during export. Duplicate names in one location are errors.
Example:
operation.Parameter("id", "path", router.RawSchema(`{"type":"string"}`))
func (*OperationBuilder) Problem ¶
func (operation *OperationBuilder) Problem(details problem.Details) *OperationBuilder
Problem explicitly describes the router's negotiated RFC 9457 wire model at the supplied status. It advertises both JSON media types and plain text, not Details' private Go fields or its error cause. It performs no automatic mapping and does not imply that every route error has this shape.
Example:
operation.Problem(problem.NotFound())
func (*OperationBuilder) Response ¶
func (operation *OperationBuilder) Response(status int, description string) *OperationBuilder
Response declares a response status and description without adding a body. Existing representations and headers for that status are preserved. Status must be 100–599; an empty description uses the standard HTTP status text.
Example:
operation.Response(204, "Deleted")
func (*OperationBuilder) ResponseHeader ¶
func (operation *OperationBuilder) ResponseHeader(status int, name string, schema Schema) *OperationBuilder
ResponseHeader describes a response header's value without adding the header at runtime. Names match case-insensitively, as HTTP headers do. Repeated declarations replace both the schema and spelling of the earlier name for this response status. Invalid names or schemas remain export errors.
Example:
operation.ResponseHeader(200, "X-Request-ID", router.RawSchema(`{"type":"string"}`))
func (*OperationBuilder) Security ¶
func (operation *OperationBuilder) Security(name string, scopes ...string) *OperationBuilder
Security adds an alternative security requirement. Each call is OR with previous calls. Every named scheme must be explicitly declared on OpenAPIBuilder. The supported non-OAuth schemes require empty scopes; nonempty scopes are reported as an export error.
Example:
operation.Security("bearer")
func (*OperationBuilder) Summary ¶
func (operation *OperationBuilder) Summary(text string) *OperationBuilder
Summary sets a short human-facing label; it never changes handler behavior. Later calls replace the previous text.
Example:
operation.Summary("Get a user")
func (*OperationBuilder) Tags ¶
func (operation *OperationBuilder) Tags(tags ...string) *OperationBuilder
Tags replaces the classification labels, copying the caller's slice. Empty input clears the labels and does not affect registration.
Example:
operation.Tags("users", "public")
type OperationBuilderFunc ¶
type OperationBuilderFunc = func(*OperationBuilder)
OperationBuilderFunc configures descriptive metadata once per public route registration. Callbacks run synchronously in order; nil entries panic. An expanded nil slice omits metadata. Neither metadata nor its errors affect route execution. Retained drafts cannot change the published description.
Example:
var describe router.OperationBuilderFunc = func(operation *router.OperationBuilder) {
operation.ID("listUsers").Summary("List users")
}
type PanicRecoveredEvent ¶
type PanicRecoveredEvent struct {
// Method is the request method, not an observed response status or a
// guarantee that the client received a replacement problem response.
//
// Example:
//
// notice := router.PanicRecoveredEvent{Method: http.MethodPost}
// fmt.Println(notice.Method) // POST
Method string
// Pattern is the matched route pattern, such as "GET /users/{id}";
// empty when no route matched. It describes registration, not the actual
// request path, query, or path parameter values.
//
// Example:
//
// notice := router.PanicRecoveredEvent{Pattern: "GET /users/{id}"}
// fmt.Println(notice.Pattern) // GET /users/{id}
Pattern string
// Err is the error the panic was converted into, as [Recover] returns it.
// Its cause and text can contain sensitive panic values. Use errors.Is for
// classification and restrict any diagnostic output to approved sinks.
//
// Example:
//
// notice := router.PanicRecoveredEvent{Err: router.ErrRecoverUnexpected}
// fmt.Println(errors.Is(notice.Err, router.ErrRecoverUnexpected)) // true
Err error
// Stack is the goroutine stack captured when the panic was recovered. It
// does not alias the returned error's stack; listeners sharing this event
// must copy it before mutation to avoid changing what later listeners see.
// It is a sensitive diagnostic, not a client-safe notification body.
//
// Example:
//
// notice := router.PanicRecoveredEvent{Stack: []byte("redacted diagnostic")}
// retained := bytes.Clone(notice.Stack)
// _ = retained
Stack []byte
}
PanicRecoveredEvent is the diagnostic payload of PanicRecovered. It is emitted synchronously only when Recover has an event bus. Err and Stack can contain sensitive panic diagnostics; applications must restrict and sanitize their listener output rather than forward the entire payload to clients. The event's stack bytes are detached from the error's diagnostic stack.
Example:
bus := event.New()
_, err := bus.Listen(router.PanicRecovered, func(ctx context.Context, notice router.PanicRecoveredEvent) error {
slog.ErrorContext(ctx, "request panicked", "method", notice.Method, "pattern", notice.Pattern)
return nil
})
if err != nil {
return err
}
type ProvideFunc ¶
ProvideFunc resolves a context for the current request. Successful contexts must derive from r.Context() to retain cancellation, lifecycle hooks, and upstream middleware state. An error stops the chain without calling next.
Example:
type valueKey struct{}
var resolve router.ProvideFunc = func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
return context.WithValue(r.Context(), valueKey{}, "value"), nil
}
routes := router.New()
routes.Use(router.Provide(resolve))
type RawSchema ¶
type RawSchema []byte
RawSchema is an unevaluated JSON Schema document. Arbitrary keywords and references are preserved; references are never fetched or rewritten.
Example:
schema := router.RawSchema(`{"type":"integer","minimum":1}`)
data, err := schema.JSONSchema()
if err != nil {
return err
}
_ = data
func (RawSchema) JSONSchema ¶
JSONSchema returns a detached copy after checking JSON syntax and the root kind. It accepts objects and booleans, not arrays, strings or null.
Example:
data, err := router.RawSchema(`false`).JSONSchema()
if err != nil {
return err
}
_ = data
type RecoverBuilder ¶
type RecoverBuilder struct {
// contains filtered or unexported fields
}
RecoverBuilder configures Recover before publication. Callbacks run directly once, synchronously in order on one fresh draft; nil entries panic. Retaining the draft cannot change published settings. Methods return the same draft.
Example:
r := router.New()
r.Use(router.Recover(func(recovery *router.RecoverBuilder) {
recovery.Handler(nil)
}))
func (*RecoverBuilder) Events ¶
func (recovery *RecoverBuilder) Events(bus *event.Bus) *RecoverBuilder
Events receives PanicRecovered. Nil, the default, publishes nothing.
The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.
Example:
r := router.New()
r.Use(router.Recover(func(recovery *router.RecoverBuilder) {
recovery.Events(event.New())
}))
func (*RecoverBuilder) Handler ¶
func (recovery *RecoverBuilder) Handler(handler func(value any) error) *RecoverBuilder
Handler converts a recovered panic value into an error. A nil function uses the default conversion to ErrRecoverUnexpected. If a supplied function returns nil, Recover uses ErrRecoverUnexpected without the raw panic value: recovery always reports a failure and retains its diagnostic stack.
The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.
Example:
r := router.New()
r.Use(router.Recover(func(recovery *router.RecoverBuilder) {
recovery.Handler(nil)
}))
type RecoverBuilderFunc ¶
type RecoverBuilderFunc = func(*RecoverBuilder)
RecoverBuilderFunc configures Recover synchronously during construction. Callbacks share one draft and run once in order; nil entries panic. Retaining the draft cannot change published settings, but function captures stay shared.
Example:
var configure router.RecoverBuilderFunc = func(recovery *router.RecoverBuilder) {
recovery.Handler(nil) // select the default conversion
}
routes := router.New()
routes.Use(router.Recover(configure))
type Route ¶
type Route struct {
// Method is the HTTP method, such as "GET".
// Any registrations yield one Route per explicit method, rather than an
// empty method standing for all methods.
//
// Example:
//
// route := router.Route{Method: http.MethodGet}
// fmt.Println(route.Method) // GET
Method string
// Pattern is the route pattern with every group prefix applied, such as
// "/users/{id}". It is the pattern the application registered: the
// trailing-slash and parent-path variants the router adds internally are
// not reported.
//
// Example:
//
// route := router.Route{Pattern: "/users/{id}"}
// fmt.Println(route.Pattern) // /users/{id}
Pattern string
// Handler is the fully qualified name of the application's handler
// function, such as "shop/users.Show", taken before any middleware wraps
// it. Closures carry the compiler's name (for example "shop/users.New.func1")
// and method values lose their "-fm" suffix. For a handler adapted with
// [HTTP], it names the adapted [http.Handler]: the function for an
// [http.HandlerFunc], otherwise the dynamic type, such as "*pkg.Files".
//
// Example:
//
// route := router.Route{Handler: "shop/users.Show"}
// fmt.Println(route.Handler) // shop/users.Show
Handler string
// Source is the "dir/file.go:line" where the handler function is
// declared: the last directory and the file name only, because the module
// root is not knowable at run time and full build paths leak the build
// machine's layout. It is empty when the location is unknown.
//
// Example:
//
// route := router.Route{Source: "users/handlers.go:42"}
// fmt.Println(route.Source) // users/handlers.go:42
Source string
}
Route describes one route registered on a Router tree, as the application wrote it. Inspection tools such as route listings and debugging endpoints read it through Router.Routes. It carries no request data or executable handler state, but names and source locations can disclose application layout.
Example:
routes := router.New()
routes.Get("/users/{id}", router.HTTP(http.NotFoundHandler()))
for _, route := range routes.Routes() {
fmt.Println(route.Method, route.Pattern)
}
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router routes Arkoren Handler values, wrapping http.ServeMux with middleware, route groups, and automatic trailing-slash handling.
A Router is not safe for concurrent use during route registration. All routes and middleware must be registered before calling ServeHTTP or passing the router to an HTTP server. Do not copy a Router after first use; use Router.Clone for an independent middleware scope sharing the same multiplexer.
Example:
routes := router.New()
routes.Get("/health", func(writer http.ResponseWriter, request *http.Request) error {
writer.WriteHeader(http.StatusNoContent)
return nil
})
response := routes.Record(httptest.NewRequest(http.MethodGet, "/health", nil))
fmt.Println(response.StatusCode) // 204
func New ¶
func New() *Router
New creates a new Router with an empty http.ServeMux. Unmatched routes are rendered as RFC 9457 Problem Details responses. It installs no application middleware or routes; complete registration before serving.
Example:
routes := router.New()
routes.Get("/health", func(http.ResponseWriter, *http.Request) error {
return nil
})
fmt.Println(routes.Has(http.MethodGet, "/health")) // true
func (*Router) Any ¶
func (router *Router) Any(pattern string, handler Handler, configure ...OperationBuilderFunc)
Any registers a handler for all standard HTTP methods except TRACE and CONNECT. Metadata callbacks run once, not once per method; see Router.Methods.
Example:
routes.Any("/health", health, func(operation *router.OperationBuilder) {
operation.Response(200, "Healthy")
})
func (*Router) Clone ¶
Clone creates a sub-router that shares the same http.ServeMux but has an independent copy of the current middleware stack and the same path prefix. Handler functions and their captures remain shared. Registering through the clone also seals ancestor middleware configuration.
Example:
routes := router.New()
public := routes.Clone()
public.Use(router.Recover())
public.Get("/health", router.HTTP(http.NotFoundHandler()))
func (*Router) Connect ¶
func (router *Router) Connect(pattern string, handler Handler, configure ...OperationBuilderFunc)
Connect registers a handler for http.MethodConnect using Router.Method.
Example:
routes.Connect("/tunnel", connect)
func (*Router) Delete ¶
func (router *Router) Delete(pattern string, handler Handler, configure ...OperationBuilderFunc)
Delete registers a handler for http.MethodDelete using Router.Method.
Example:
routes.Delete("/users/{id}", remove)
func (*Router) Get ¶
func (router *Router) Get(pattern string, handler Handler, configure ...OperationBuilderFunc)
Get registers a handler for http.MethodGet using Router.Method.
Example:
routes.Get("/users", list, func(operation *router.OperationBuilder) {
operation.Summary("List users")
})
func (*Router) Group ¶
Group uses the given pattern to automatically mount a sub-router that has that pattern as a prefix. See Router.Method for path normalization rules. It inherits the current middleware stack and shares the root route registry; the callback runs synchronously during registration.
Example:
routes := router.New()
routes.Group("/users", func(users *router.Router) {
users.Get("/{id}", router.HTTP(http.NotFoundHandler()))
})
fmt.Println(routes.Has(http.MethodGet, "/users/7")) // true
func (*Router) Grouped ¶
Grouped creates a cloned sub-router for scoping middleware without a path prefix. The callback runs synchronously; its routes join the shared registry but its middleware edits do not change the parent stack.
Example:
routes := router.New()
routes.Grouped(func(public *router.Router) {
public.Use(router.Recover())
public.Get("/health", router.HTTP(http.NotFoundHandler()))
})
func (*Router) Handler ¶
Handler returns the wrapped handler matching method and target, or false for invalid targets, native responses, and missing routes. It performs lookup only; call ServeHTTP on the result to install hooks and render returned errors. Lookup does not populate request path values as multiplexer dispatch does.
Example:
routes := router.New()
routes.Get("/health", router.HTTP(http.NotFoundHandler()))
if handler, ok := routes.Handler(http.MethodGet, "/health"); ok {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/health", nil))
fmt.Println(recorder.Code) // 404
}
func (*Router) HandlerMatch ¶
HandlerMatch returns the wrapped Arkoren handler for request, or false for a native response or missing route. It neither executes the handler nor sets request path values; use Router.ServeHTTP when dispatch needs those values.
Example:
routes := router.New()
routes.Get("/health", router.HTTP(http.NotFoundHandler()))
r := httptest.NewRequest(http.MethodGet, "/health", nil)
if handler, ok := routes.HandlerMatch(r); ok {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, r)
fmt.Println(recorder.Code) // 404
}
func (*Router) Has ¶
Has reports whether method and target match a registered route, including trailing-slash variants and net/http method matching. Invalid request targets return false; it does not call the matched handler or its middleware.
Example:
routes := router.New()
routes.Get("/users/{id}", router.HTTP(http.NotFoundHandler()))
fmt.Println(routes.Has(http.MethodGet, "/users/7")) // true
func (*Router) Head ¶
func (router *Router) Head(pattern string, handler Handler, configure ...OperationBuilderFunc)
Head registers a handler for http.MethodHead using Router.Method.
Example:
routes.Head("/users", headers)
func (*Router) MCPTools ¶
MCPTools returns a live, read-only route listing for explicit registration. A nonempty prefix produces prefix.routes and uses prefix as its CLI group; an empty prefix produces routes in the router CLI group. Invalid names are reported by each tool's Err method. Calls list the entire router tree with optional case-insensitive method and case-sensitive path-prefix filters.
Handler identities and source paths can disclose application structure. The caller owns authentication and capability policy; nothing is registered implicitly. Routes added after construction appear on subsequent calls.
Example:
tools := router.New().MCPTools("http")
if err := tools[0].Err(); err != nil {
return err
}
result, err := tools[0].Call(context.Background(), []byte(`{"path":"/api"}`))
if err != nil {
return err
}
fmt.Println(string(result))
Example ¶
ExampleRouter_MCPTools checks the documented native tool call without exposing source locations or requiring a network endpoint.
package main
import (
"context"
"fmt"
"arkoren.dev/core/http/router"
)
func main() {
tools := router.New().MCPTools("http")
if err := tools[0].Err(); err != nil {
fmt.Println(err)
return
}
result, err := tools[0].Call(context.Background(), []byte(`{"path":"/api"}`))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(result))
}
Output: {"items":[],"truncated":false}
func (*Router) Matches ¶
Matches reports whether request resolves to an Arkoren handler in the shared multiplexer. It does not execute handlers; native redirects or generated 404/405 responses are not registered-handler matches.
Example:
routes := router.New()
routes.Get("/users/{id}", router.HTTP(http.NotFoundHandler()))
r := httptest.NewRequest(http.MethodGet, "/users/7", nil)
fmt.Println(routes.Matches(r)) // true
func (*Router) Method ¶
func (router *Router) Method(method string, pattern string, handler Handler, configure ...OperationBuilderFunc)
Method registers a new handler to the router with the given method and pattern. Non-root routes match both trailing-slash forms. Metadata callbacks follow Router.Methods; they never affect dispatch.
Example:
routes.Method("COPY", "/users/{id}", copyUser, func(operation *router.OperationBuilder) {
operation.Summary("Copy a user")
})
func (*Router) Methods ¶
func (router *Router) Methods(methods []string, pattern string, handler Handler, configure ...OperationBuilderFunc)
Methods registers a handler for each method in the given slice. The middleware chain is built once and shared by every registration. Optional descriptive callbacks run directly once, synchronously in order on one fresh draft, even when metadata is invalid. Nil entries panic; expanded nil or empty slices omit metadata. Publication detaches owned containers and rendered schema bytes, not handler functions or captures. Errors in metadata are reported only by Router.OpenAPI. Each explicit method receives the completed metadata; a shared ID will therefore fail export's uniqueness check. No implicit trailing-slash or parent-path routes are described. Routes with omitted callbacks are excluded from export.
Example:
routes.Methods([]string{"GET", "HEAD"}, "/users", list, func(operation *router.OperationBuilder) {
operation.Summary("List users")
})
func (*Router) Mount ¶
func (router *Router) Mount(prefix string, handler http.Handler, configure ...OperationBuilderFunc)
Mount serves handler for every standard method under prefix: the exact path and everything below it. The prefix is stripped from the request before handler sees it, so a handler written to serve "/" and "/{x}/{y}" can be mounted anywhere, as chi's Mount does. A request for exactly prefix, with or without a trailing slash, reaches the handler with the path "/".
prefix is joined to the router's own group prefix, so a Mount inside Router.Group strips the whole pattern. prefix must contain literal segments: a wildcard such as "{id}" panics. A parent group may contain named wildcards; their values remain available through request.PathValue after stripping. Like Router.Any it covers every method except TRACE and CONNECT; a request with one of those gets the router's 405. Literal segments follow http.ServeMux pattern escaping. Stripping preserves escaped segment boundaries in URL.RawPath, including encoded slashes, and does not modify the original request's URL or query.
Mounted handlers are ordinary routes: they run inside the router's middleware, in registration order, exactly as a handler registered with Router.Get would, so authentication, recovery, and telemetry middleware apply. The handler renders its own errors, because an http.Handler has no error to return; the wrapper only answers 200 when it wrote nothing, as HTTP does. Routes are registered through Router.Methods, so Router.Use panics afterwards and a route that overlaps one already registered panics as it does for any pattern.
Router.Routes lists the mount once per method with the pattern prefix+"/{path...}", the pattern that was registered, and names the handler's own function or type (for an http.HandlerFunc its function, otherwise the dynamic type such as "*http.ServeMux"), not the wrapper that strips the prefix.
Metadata callbacks follow Router.Methods and run once for the public mount, not once per method. Mounts are opaque catch-alls, so omit metadata or set OperationBuilder.Hidden to exclude them; annotated visible mounts return a contextual Router.OpenAPI error rather than a fabricated single path.
Example:
admin := router.New()
admin.Use(auth.Middleware(authenticator), auth.Require[User]())
admin.Mount("/tools", tools) // tools sees paths relative to /tools
func (*Router) OpenAPI ¶
func (router *Router) OpenAPI(title, version string, configure ...OpenAPIBuilderFunc) ([]byte, error)
OpenAPI exports only annotated, nonhidden routes as deterministic OpenAPI 3.2.1 JSON. It reads the entire router tree, using final group paths and synthesizing required string parameters for otherwise undescribed wildcards. Catch-alls and mounts are opaque and must be omitted or hidden. Declaration errors, duplicate IDs and equivalent path templates return contextual errors. Schemas remain inline and references are neither rewritten nor fetched. Registration must finish before exporting concurrently.
Example:
data, err := routes.OpenAPI("Users", "1.0")
if err != nil {
return err
}
_ = data
func (*Router) OpenAPIHandler ¶
func (router *Router) OpenAPIHandler(title, version string, configure ...OpenAPIBuilderFunc) Handler
OpenAPIHandler returns an ordinary error-returning handler. Configuration callbacks run once now; document JSON or its error is cached once on the first request with sync.OnceValues. Routes added before that request are included. Later changes do not invalidate the cache. No route is registered, compiled, or frozen by construction; finish registration before serving.
Example:
routes.Get("/openapi.json", routes.OpenAPIHandler("Users", "1.0"))
func (*Router) Options ¶
func (router *Router) Options(pattern string, handler Handler, configure ...OperationBuilderFunc)
Options registers a handler for http.MethodOptions using Router.Method.
Example:
routes.Options("/users", options)
func (*Router) Patch ¶
func (router *Router) Patch(pattern string, handler Handler, configure ...OperationBuilderFunc)
Patch registers a handler for http.MethodPatch using Router.Method.
Example:
routes.Patch("/users/{id}", update)
func (*Router) Post ¶
func (router *Router) Post(pattern string, handler Handler, configure ...OperationBuilderFunc)
Post registers a handler for http.MethodPost using Router.Method.
Example:
routes.Post("/users", create)
func (*Router) Put ¶
func (router *Router) Put(pattern string, handler Handler, configure ...OperationBuilderFunc)
Put registers a handler for http.MethodPut using Router.Method.
Example:
routes.Put("/users/{id}", replace)
func (*Router) Query ¶
func (router *Router) Query(pattern string, handler Handler, configure ...OperationBuilderFunc)
Query registers a handler for QUERY using Router.Method.
Example:
routes.Query("/users", search, func(operation *router.OperationBuilder) {
operation.Summary("Search users")
})
func (*Router) Record ¶
Record returns an http.Response produced by dispatching the given HTTP request through the router's full middleware and handler pipeline. It uses an in-memory recorder, not a network server; the caller owns the response body. Panics retain the same behavior as ServeHTTP.
Example:
routes := router.New() response := routes.Record(httptest.NewRequest(http.MethodGet, "/missing", nil)) defer response.Body.Close() fmt.Println(response.StatusCode) // 404
func (*Router) Routes ¶
func (router *Router) Routes() collection.Slice[Route]
Routes returns every route registered through this router or any router in its tree (Router.Group, Router.Grouped, Router.Clone, Router.With), in registration order. It answers for the whole tree whichever router it is called on, and returns a copy the caller may modify. Router.Any yields one entry per method. The eager collection is a detached snapshot, not a live query. Its wrapper adds operations, not ownership or additional copying.
Example:
r := router.New()
r.Group("/users", func(users *router.Router) {
users.Get("/{id}", show)
})
gets := r.Routes().Filter(func(_ int, route router.Route) bool {
return route.Method == http.MethodGet
})
for _, route := range gets {
fmt.Println(route.Method, route.Pattern, route.Handler)
}
func (*Router) ServeHTTP ¶
func (router *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request)
ServeHTTP implements http.Handler. It delegates matched routes to the underlying http.ServeMux and renders unmatched 404 and 405 responses as Problem Details through the router's middleware, so middleware such as CORS also answers requests that match no route. The fallback middleware chain is constructed on the first unmatched request and shared by subsequent unmatched requests. Configure middleware before serving requests, including on routers with no registered routes. Middleware forwarding a modified request must preserve its context values, which carry lifecycle hooks and the captured native fallback response.
Example:
routes := router.New() response := httptest.NewRecorder() routes.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/missing", nil)) fmt.Println(response.Code) // 404
func (*Router) Trace ¶
func (router *Router) Trace(pattern string, handler Handler, configure ...OperationBuilderFunc)
Trace registers a handler for http.MethodTrace using Router.Method.
Example:
routes.Trace("/diagnostics", trace)
func (*Router) Use ¶
func (router *Router) Use(middlewares ...Middleware)
Use appends to the current router the given middlewares. Subsequent route registrations are wrapped with both existing and newly registered middleware.
Use panics when a route was already registered through this router or any router derived from it (Router.Group, Router.Grouped, Router.Clone, Router.With), because those routes were wrapped with the middleware as it was and would silently skip the new one, for example an authentication check. Call Use before registering routes. A sub-router created before its parent registered routes may still call Use as long as it has registered nothing itself.
Example:
r.Use(auth.Middleware(authenticator), auth.Require[User]()) // before the routes it protects
r.Get("/me", me)
func (*Router) With ¶
func (router *Router) With(middlewares ...Middleware) *Router
With creates a new sub-router that applies the given middlewares in addition to any inherited ones. In contrast to Router.Use, it does not modify the current router. It retains middleware functions, not copies of their captures; route registration and the path prefix are shared as with Clone.
Example:
routes := router.New()
routes.With(router.Recover()).Get("/health", router.HTTP(http.NotFoundHandler()))
type Schema ¶
type Schema interface {
// JSONSchema returns one JSON object or boolean, or a construction error.
// Returned bytes are copied by the router, never retained directly.
//
// Example:
// data, err := router.RawSchema(`true`).JSONSchema()
// if err != nil {
// return err
// }
// _ = data
JSONSchema() ([]byte, error)
}
Schema supplies a JSON Schema object or boolean for descriptive metadata. Implementations are rendered at declaration time; no validation is executed.
Example:
var schema router.Schema = router.RawSchema(`{"type":"string"}`)
data, err := schema.JSONSchema()
if err != nil {
return err
}
_ = data
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
openapi
Package openapi exports descriptive route snapshots as OpenAPI documents.
|
Package openapi exports descriptive route snapshots as OpenAPI documents. |
|
snapshot
Package snapshot defines detached route metadata for documentation export.
|
Package snapshot defines detached route metadata for documentation export. |