http_router

The http_router library provides the first declarative routing layer on top of the http_core library. It is implemented as a category so that router objects can implement the http_handler_protocol protocol directly while reusing common method dispatch and path-template matching logic.

Layering

http_router sits between the normalized http_core message layer and higher-level API authoring helpers:

  • Use http directly when you only need normalized messages, parsers, generators, or low-level handlers.

  • Use http_router when you want the handler object to keep exposing handle/2 while route matching, metadata annotation, middleware, and content negotiation are derived from route/4 declarations.

  • Add rest when route/4 is still too low-level and you prefer endpoint descriptors plus small normalized result terms.

  • Pair router objects with open_api when you want the route declarations and metadata to derive OpenAPI operations automatically.

  • Add companion libraries such as http_parameters, http_cors, http_htmx, http_session, http_static_files, and http_directory_listing when you need parameter extraction, middleware helpers, sessions, or static-file routing on the same dispatch layer.

API documentation

Open the ../../apis/library_index.html#http_router link in a web browser.

Loading

To load the library, load the loader.lgt file:

| ?- logtalk_load(http_router(loader)).

Testing

To test this library, load the tester.lgt file:

| ?- logtalk_load(http_router(tester)).

Current scope

The initial routing solution provides one public predicate through the imported category:

  • handle/2 routes a normalized request using route/4 clauses defined by the importing object.

The remaining router behavior is organized in the sections below.

Route declarations

Importing router objects must define route descriptors using:

  • route(Id, Method, PathTemplate, Handler)

The Handler argument is the name of a declared local predicate with arity 2, typically a protected predicate.

Path templates

Path-template atoms support literal segments, anonymous * wildcard segments, plain {name} placeholders, and typed placeholders such as {id:integer} and {score:number}. Example:

route(show_user, get, '/users/{id}', show_user).

Request annotations

The router category calls the matched handler after annotating the request with route(Id) and path_params(Pairs). On the normal routing path it also scrubs stale internal synthetic properties such as open_api_probe/1, automatic_options/1, effective_methods/1, and response_media_type/1 before handler execution.

Route metadata

Importing router objects can optionally declare additional route-specific metadata using:

  • route_metadata(Id, Metadata)

When defined, Metadata must be a list of compound terms. The router removes any existing request properties with the same functors, prepends the metadata to the matched request, and then adds the standard route/1 and path_params/1 annotations. This keeps route metadata available to both route handlers and response middleware and allows metadata descriptors such as summary/1, description/1, tags/1, or other application-specific terms.

Route authorization

Importing router objects can optionally authorize or decorate routed requests after route matching and metadata annotation but before route handler dispatch using:

  • authorize_routed_request(Request, Action)

The hook must return either continue(Request) or respond(Response). Short-circuited responses still flow through response middleware and keep the routed request annotations such as route/1, path_params/1, and any route metadata properties.

Content negotiation

Importing router objects can optionally declare route response media types using:

  • route_produces(Id, MediaTypes)

When defined, MediaTypes must be a non-empty list of media type atoms. The router negotiates the request Accept header against that list, annotates the matched request with response_media_type(MediaType), and returns a generic 406 Not Acceptable response when no produced media type matches.

On the normal routing path, any stale incoming response_media_type/1 annotation is scrubbed before route dispatch and replaced only when the matched route successfully negotiates one of its declared route_produces/2 media types.

Middleware chaining

Importing router objects can also optionally define ordered middleware descriptors using:

  • middleware(Id, Handler)

The Handler argument is the name of a declared local predicate with arity 2 that receives the current request and returns either continue(Request) or respond(Response). Middleware runs before route matching, so it can rewrite requests before dispatch or short-circuit processing with an immediate response.

Response middleware

Importing router objects can also optionally define ordered response middleware descriptors using:

  • response_middleware(Id, Handler)

The Handler argument is the name of a declared local predicate with arity 3 that receives the current request, the current response, and returns the transformed response. Response middleware runs after route dispatch or short-circuit processing, so it can decorate or rewrite any generated response.

HEAD fallback

HEAD requests match exact head routes first and otherwise fall back to a matching get route.

OPTIONS handling

OPTIONS requests match explicit options routes first. When no explicit options route exists for a matched path, the router returns an automatic 200 OK response with the derived Allow header and an empty body. The synthetic request used for this path is annotated with automatic_options(true) and effective_methods(Methods). When the router can identify exactly one matching non-options route template, it also annotates that synthetic request with route(Id), path_params(Pairs), and that route route_metadata/2 properties before response middleware runs. When multiple non-options routes match the same path, the synthetic request omits route/1, keeps path_params/1 only when all matches produce the same value, and preserves only metadata properties that are identical across all matched routes. Automatic OPTIONS can be customized using route_automatic_options_response/3 and still flows through response middleware.

Custom bad-request responses

Importing router objects can optionally define:

  • route_bad_request_response(Request, Errors, Response)

Dedicated route-handler exceptions matching error(http_parameter_validation(Errors), Context) with a non-empty Errors list are translated into 400 Bad Request responses before response middleware runs. The routed request annotations remain available to the optional route_bad_request_response/3 hook and to any later response middleware.

Custom error responses

Importing router objects can optionally customize other routing errors by defining:

  • route_not_found_response(Request, Response)

  • route_method_not_allowed_response(Request, AllowedMethods, Response)

When a path matches but the request method does not, the router returns a 405 Method Not Allowed response with an Allow header derived from the matching route descriptors. A get route implicitly contributes both GET and HEAD to that header. Automatic router support for OPTIONS also contributes OPTIONS to the header for matched paths.

The AllowedMethods argument passed to the 405 hook is the effective method list as lowercase atoms and already includes implicit head support for get routes and automatic options support. The Request passed to the 405 hook and to response middleware is annotated with matched_path(true) and effective_methods(AllowedMethods).

Custom negotiation failures

Importing router objects can optionally define:

  • route_not_acceptable_response(Request, ProducedMediaTypes, Response)

The ProducedMediaTypes argument passed to the 406 hook is the normalized list declared by route_produces/2 for the matched route.

OpenAPI derivation

When a router object also implements the open_api_provider_protocol protocol, the imported category can automatically derive operations/1 from route/4, route_metadata/2, and route_produces/2. Recognized route-metadata terms for OpenAPI derivation are:

  • summary(Summary)

  • description(Description)

  • tags(Tags)

  • deprecated(Boolean)

  • security(Requirements)

  • parameters(Parameters)

  • request_body(RequestBody)

  • responses(Responses)

Path-template placeholders automatically derive default OpenAPI path parameter descriptors using a string schema. Metadata parameters/1 can override those defaults or add extra query, header, or cookie parameters. When responses/1 is not provided, the router first attempts to infer a successful response by probing the route handler with a synthetic annotated request and then falls back to a default successful response. When available, route_produces/2 guides both response probing and the derived OpenAPI media types.

When request_body/1 metadata is not provided, the router also attempts to infer a request body descriptor by probing the route handler with a synthetic annotated request whose body is left open for the handler to constrain. The inferred request and response schemas are emitted inline and are derived from the observed payload terms:

  • json/1 payloads infer JSON Schema types recursively from the observed JSON values.

  • text/1 and binary/1 payloads infer a string schema.

  • form/1 payloads infer an object schema from the observed key-value pairs.

Explicit request_body/1 or responses/1 metadata always takes precedence over inferred descriptors.

The imported category also exposes top-level OpenAPI provider predicates. The default api_info/1 descriptor is derived from the importing object identifier, servers/1 defaults to [], and importing objects can override or extend the provider surface with these hooks:

  • open_api_info(Info)

  • open_api_servers(Servers)

  • open_api_security(Security)

  • open_api_schema(Name, Schema)

  • open_api_security_scheme(Name, SecurityScheme)

Current limitations

  • OpenAPI inference is best-effort. Even with the optional route_open_api_request_body_example/2 and route_open_api_response_example/2 hooks, explicit metadata remains the right choice for reusable component schemas and for routes whose contracts should be stated directly instead of inferred.