http_cors

The http_cors library provides a transport-neutral CORS layer on top of the normalized http_core library. It classifies normalized requests as CORS or preflight requests, builds direct preflight responses from CORS policies, and decorates normalized responses with the relevant Access-Control-* headers.

This library reuses the http_core, http_server, and http_router. It uses normalized request and response terms and can be called directly from plain handlers or from router hooks and response middleware.

API documentation

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

Loading

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

| ?- logtalk_load(http_cors(loader)).

Testing

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

| ?- logtalk_load(http_cors(tester)).

Current scope

The current implementation provides one transport-neutral public object with five public predicates:

  • request_origin/2

  • is_cors_request/1

  • is_preflight_request/1

  • preflight_response/3

  • add_response_headers/4

The option DSL follows the options library conventions and supports:

  • allowed_origins(any)

  • allowed_origins(Origins)

  • allowed_methods(any)

  • allowed_methods(Methods)

  • allowed_headers(any)

  • allowed_headers(requested)

  • allowed_headers(Headers)

  • expose_headers(any)

  • expose_headers(Headers)

  • allow_credentials(Boolean)

  • max_age(none)

  • max_age(Seconds)

The default policy is:

  • allowed_origins([])

  • allowed_methods([get])

  • allowed_headers(requested)

  • expose_headers([])

  • allow_credentials(false)

  • max_age(none)

This library consumes the normalized HTTP terms provided by the http_core library:

request(Method, Target, Version, Headers, Body, Properties)
response(Version, Status, Headers, Body, Properties)

It reads normalized request headers such as origin, access_control_request_method, and access_control_request_headers and emits normalized response headers such as access_control_allow_origin, access_control_allow_methods, access_control_allow_headers, access_control_expose_headers, and vary.

When policy depends on a specific requesting origin, generated responses add or preserve the appropriate Vary metadata. Existing Vary: * headers are treated as absorbing and remain * when CORS headers are merged into a response.

Wildcard header policies can be expressed explicitly using allowed_headers(any) and expose_headers(any). For compatibility, exact ['*'] option values are normalized to the same wildcard behavior. On non-credentialed requests these policies emit * when that has the intended CORS wildcard meaning. On credentialed requests, and for Authorization preflight requests, the library materializes explicit header-name lists instead because browsers otherwise treat * as a literal header name.

Similarly, allowed_origins(any) emits * for non-credentialed requests but reflects the concrete request origin for credentialed requests and adds Vary: Origin so caches keep those responses partitioned correctly.

Wildcard origin policies can also be expressed using pattern atoms inside allowed_origins(Origins), for example allowed_origins(['https://*.example.com']). These patterns match exactly one leftmost hostname label, require exact scheme matching, and require exact port matching when the pattern includes an explicit port. Matching pattern policies always emit the concrete request origin and add or preserve Vary: Origin because the response depends on the requesting origin.

Wildcard method policies can be expressed using allowed_methods(any). For compatibility, exact ['*'] values are normalized to the same wildcard behavior. Method wildcards expand from the request effective_methods(Methods) property after removing options, so they are intended for routed or otherwise annotated requests that provide that metadata. The generated Access-Control-Allow-Methods header still contains an explicit method list.

Route-level overrides

When present, the request property cors(Options) overrides the caller-supplied default options. This makes it possible to define router-wide defaults and then refine them per route using route_metadata/2.

Example route metadata:

route_metadata(show_page, [
    cors([
        allowed_origins(['https://app.example.com']),
        expose_headers([x_trace_id]),
        allow_credentials(true)
    ])
]).

Router integration

For actual routed responses, use router response middleware:

response_middleware(cors, add_cors_headers).

add_cors_headers(Request, Response0, Response) :-
    Defaults = [
        allowed_origins([]),
        allowed_methods([get]),
        allowed_headers(requested),
        expose_headers([]),
        allow_credentials(false),
        max_age(none)
    ],
    http_cors::add_response_headers(Request, Response0, Response, Defaults).

For automatic router OPTIONS responses, use route_automatic_options_response/3 when the router object wants to build a direct preflight response:

route_automatic_options_response(Request, _EffectiveMethods, Response) :-
    Defaults = [
        allowed_origins([]),
        allowed_methods([get, post]),
        allowed_headers(requested),
        expose_headers([]),
        allow_credentials(false),
        max_age(none)
    ],
    http_cors::preflight_response(Request, Response, Defaults).

The current http_router implementation annotates synthetic automatic OPTIONS requests with automatic_options(true), effective_methods(Methods), and route metadata when the match is unique or the metadata is shared across same-path matches. This allows http_cors integrations to reuse those annotations directly.

Plain handler integration

For plain handlers using http_server, the recommended pattern is:

  1. detect preflight requests with is_preflight_request/1

  2. build the direct preflight response with preflight_response/3

  3. otherwise build the application response and pass it through add_response_headers/4

When add_response_headers/4 denies a request, it does not add permission headers but it may still preserve or add cache-relevant Vary metadata. Use preflight_response/3 when an explicit 403 Forbidden preflight response is required.

Current limitations

  • Origin matching is exact and string-based

  • Dynamic callback policies are not supported