Skip to content

Programmable HTTP Access Phase and Request Body Reading#1044

Draft
xeioex wants to merge 2 commits intonginx:masterfrom
xeioex:nginx_body_routing_done
Draft

Programmable HTTP Access Phase and Request Body Reading#1044
xeioex wants to merge 2 commits intonginx:masterfrom
xeioex:nginx_body_routing_done

Conversation

@xeioex
Copy link
Copy Markdown
Contributor

@xeioex xeioex commented Apr 3, 2026

Programmable HTTP Access Phase and Request Body Reading

Overview

Two new capabilities let JavaScript handlers participate in request
processing before the content phase begins:

  • js_access -- registers a handler in the HTTP access phase for
    authorization, routing, and request preprocessing.
  • r.readRequestText(), r.readRequestArrayBuffer(),
    r.readRequestJSON()
    -- async methods that read and cache the
    request body, available in any HTTP handler.

Together they enable decisions based on headers, arguments, variables,
and the request body -- all resolved before content generation or
proxying starts.

js_access Directive

js_access <module.function>;

Context: http, server, location

The handler runs in NGX_HTTP_ACCESS_PHASE, before content handlers
(js_content, proxy_pass, fastcgi_pass, etc.) and after built-in
access checkers (allow/deny, auth_basic, auth_request).
Configuration inherits from outer to inner blocks.

  • A synchronous handler can set variables, call r.return(status)
    to reject the request, or simply return to continue to the next phase.
  • An async handler (returning a Promise) suspends the request until
    the Promise settles, enabling ngx.fetch(), r.subrequest(),
    setTimeout(), and body reading without blocking the event loop.
  • An unhandled exception produces 500 Internal Server Error.
  • If the handler does not call r.return(), processing continues
    normally to the content phase.

Request Body Reading

Three async methods read and cache the request body:

Method Returns
r.readRequestText() Promise<string>
r.readRequestArrayBuffer() Promise<ArrayBuffer>
r.readRequestJSON() Promise<object>
  • Concurrent reads from different methods are rejected with an error.
  • The body remains available for downstream phases (proxy_pass
    forwards it unchanged).
  • Works with chunked transfer encoding, large bodies (respects
    client_body_buffer_size), client_body_in_file_only, and
    client_max_body_size enforcement.
  • Available in both js_access and js_content handlers.

Examples

Authentication with an External Service

async function auth(r) {
    let resp = await ngx.fetch(`http://auth-service/check?token=${r.args.token}`);

    if (resp.status !== 200) {
        r.return(resp.status);
        return;
    }

    r.variables.user = await resp.text();
}
location /api/ {
    js_access auth.auth;
    proxy_pass http://backend;
}

The access handler calls an auth service via ngx.fetch(). On failure
the request is rejected immediately; on success a variable is set for
downstream use. The content handler (proxy_pass) only runs after
authentication succeeds.

Authentication with a Subrequest

async function auth(r) {
    let reply = await r.subrequest('/auth_check?token=' + r.args.token);

    if (reply.status !== 200) {
        r.return(reply.status);
        return;
    }

    r.variables.user = reply.responseText;
}

Same pattern using an internal subrequest instead of an outbound fetch.

Dynamic Upstream Routing

function route(r) {
    r.variables.upstream = (r.args.dest === 'one')
        ? '127.0.0.1:8081' : '127.0.0.1:8082';
}
js_var $upstream;

location /route {
    js_access test.route;
    proxy_pass http://$upstream;
}

The access handler computes a routing variable synchronously; proxy_pass
evaluates it after the access phase completes.

Body-Based Access Control

async function body_gate(r) {
    let body = await r.readRequestJSON();

    if (body.role === 'admin') {
        r.return(403);
        return;
    }

    r.variables.foo = body.method + ':' + body.name;
}
js_var $foo;

location /api {
    js_access policy.body_gate;
    proxy_pass http://backend;
}

The request body is parsed as JSON in the access phase. The policy
decision is made before the request reaches the backend. The body is
preserved and forwarded to proxy_pass unchanged.

Body-Driven Routing

const backends = {
    us: '127.0.0.1:8081',
    eu: '127.0.0.1:8082',
};

async function route_by_body(r) {
    let body = await r.readRequestJSON();

    r.variables.upstream = backends[body.region]
                           || '127.0.0.1:8083';
}
js_var $upstream;

location /route {
    js_access routing.route_by_body;
    proxy_pass http://$upstream;
}

Combines body reading with dynamic routing: the request is parsed once
and the upstream is selected based on a field in the payload.

Questions

This is a draft. We are collecting feedback before finalizing the API.
Please share your thoughts on any of the following:

  1. Naming -- are readRequestText(), readRequestArrayBuffer(), and
    readRequestJSON() clear and consistent? This follow elements of Fetch API.

  2. Use cases -- which of the examples above match problems you are
    solving today? Are there scenarios you would need that are not
    covered?

  3. Error handling -- the handler can call r.return(status) to
    reject a request or let an unhandled exception produce 500. Is this
    sufficient, or do you need more control over error responses from
    access handlers?

  4. Missing features -- is there anything you would expect from an
    access-phase handler that is not present here?

The js_access directive registers a JavaScript handler in the
HTTP ACCESS phase.
@xeioex xeioex changed the title Nginx body routing done Programmable HTTP Access Phase and Request Body Reading Apr 3, 2026
Added async methods
    - r.readRequestText() as string
    - r.readRequestBuffer() as ArrayBuffer
    - r.readRequestJSON() as object.

that return Promises resolving with the request body wrapped
as a corresponding type.
@xeioex xeioex force-pushed the nginx_body_routing_done branch from 930f611 to 3145d7c Compare April 3, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant