-module(roadrunner_req).
-moduledoc """
Pure accessors over the `request()` map.
Decouples handler code from the underlying map shape — handlers should
prefer these functions over direct `maps:get/2` so the request
representation can evolve without breaking them.
The `request/0` type is canonical here. The shared HTTP primitives
re-exported alongside it (`headers/0`, `version/0`, `status/0`,
`redirect_status/0`) are aliases of the definitions in
`roadrunner_http`.
""".
-on_load(init_patterns/0).
-define(QMARK_CP_KEY, {?MODULE, qmark_cp}).
-define(SEMI_CP_KEY, {?MODULE, semi_cp}).
-define(COMMA_CP_KEY, {?MODULE, comma_cp}).
-define(EQ_CP_KEY, {?MODULE, eq_cp}).
-define(QUOTE_CP_KEY, {?MODULE, quote_cp}).
-export_type([request/0, body_reader/0, cached_decisions/0]).
-doc """
The parsed request map handlers receive.
Required fields are present on every request the framework
delivers; optional fields are populated by the framework when
applicable (e.g. `body` in `body_buffering => auto` mode,
`bindings` for routed dispatch, `request_id` for telemetry).
The same shape flows through both the h1 and h2 paths — h2's
`roadrunner_http2_request` synthesizes a request matching this
type from frames + pseudo-headers, so handlers don't have to
care which protocol delivered the bytes.
""".
-type request() :: #{
method := binary(),
target := binary(),
version := version(),
headers := headers(),
%% H1-parser internal optimization — handlers should ignore.
%% Hot-path conn helpers (`body_framing/1`,
%% `keep_alive_decision/2`, `has_continue_expectation/1`) read
%% this directly to skip per-request header re-lowercasing.
%% Absent on h2 requests and on manually-built request maps.
cached_decisions => cached_decisions(),
%% Body is set by `roadrunner_conn` before the handler is
%% invoked. Auto mode delivers the full body as `iodata()` (an
%% iolist of recv chunks for multi-chunk bodies, a single binary
%% otherwise) so the conn can skip a flatten that many handlers
%% do not need. Handlers that require a flat binary call
%% `iolist_to_binary/1` themselves. The parser never populates
%% this field; it leaves the buffered body in the `Rest` element
%% of `roadrunner_http1:parse_request/1` instead.
body => iodata(),
%% Bindings captured from `:param` segments by
%% `roadrunner_router`, set by `roadrunner_conn` before dispatch.
%% Empty map for single-handler dispatch or routes with no params.
bindings => roadrunner_router:bindings(),
%% Client TCP peer captured once per connection from
%% `inet:peername/1`. `undefined` when the OS call fails (rare;
%% usually socket teardown).
peer => {inet:ip_address(), inet:port_number()} | undefined,
%% Connection scheme — `http` for plain TCP, `https` for TLS.
%% Set once per connection by `roadrunner_conn` from the
%% transport tag.
scheme => http | https,
%% Per-route handler state attached at compile time via the
%% 3-tuple route shape `{Path, Handler, State}` or the map
%% shape's `state` key (and the listener's `{Module, State}`
%% single-handler form). `undefined` when no state was attached.
state => term(),
%% Body-read state attached by `roadrunner_conn` in
%% `body_buffering => manual` mode. Threaded through
%% `roadrunner_req:read_body/1,2`. Never present in `auto` mode
%% or in manually-constructed request maps.
body_reader => body_reader(),
%% Per-request correlation token attached by `roadrunner_conn`
%% once the headers parse. 16 lowercase hex chars (8 bytes of
%% CSPRNG output). Mirrored into `logger:set_process_metadata/1`
%% so any `?LOG_*` call from middleware or the handler picks it
%% up automatically.
request_id => binary(),
%% Registered name of the owning `roadrunner_listener`. Set
%% once per conn from `proto_opts.listener_name`. Surfaced in
%% `roadrunner_telemetry` event metadata so subscribers can
%% filter by listener in multi-listener deployments.
listener_name => atom()
}.
-type headers() :: roadrunner_http:headers().
-type version() :: roadrunner_http:version().
-doc """
Framework-internal h1-parser hot-path optimization carried on the
request map's `cached_decisions` field. Handlers should ignore both
the field and this type — they're absent on h2 requests and on
manually-built request maps. Populated by the h1 parser at
request-read time and consumed by framework code to skip
per-request header re-lowercasing on the dispatch hot path.
""".
-type cached_decisions() :: #{
is_chunked := boolean(),
has_transfer_encoding := boolean(),
expects_continue := boolean(),
connection_lower := binary(),
content_length := none | {ok, non_neg_integer()} | {error, bad_content_length},
has_host := boolean()
}.
-doc """
Body-read envelope attached to the request in `body_buffering =>
manual` mode. `read_body/1,2` consumes from it; the conn owns the
recv closure and tracks how much remains.
`pending` holds decoded body bytes that have been parsed off the
wire but not yet handed to the caller — used for chunked framing
to absorb a chunk's payload across multiple length-bounded calls.
`done` flips true once the size-0 last chunk is parsed.
""".
-type body_reader() :: #{
framing := none | chunked | {content_length, non_neg_integer()},
buffered := binary(),
bytes_read := non_neg_integer(),
pending := binary(),
done := boolean(),
recv := fun(() -> {ok, binary()} | {error, term()}),
max := non_neg_integer()
}.
-export([
method/1,
method_is/2,
path/1,
qs/1,
version/1,
headers/1,
header/2,
has_header/2,
parse_qs/1,
parse_cookies/1,
body/1,
has_body/1,
read_body/1,
read_body/2,
read_body_chunked/1,
read_form/1,
bindings/1,
peer/1,
forwarded_for/1,
scheme/1,
state/1,
request_id/1
]).
-doc "Return the request method (uppercase ASCII binary).".
-spec method(request()) -> binary().
method(#{method := M}) -> M.
-doc """
Return whether the request method matches the given binary.
Comparison is byte-exact and case-sensitive — the parser already
enforces uppercase methods on the wire, so callers should pass
uppercase too (`~"GET"`, `~"POST"`, etc.).
""".
-spec method_is(binary(), request()) -> boolean().
method_is(Method, #{method := M}) when is_binary(Method) ->
Method =:= M.
-doc """
Return the path component of the request-target.
If the target contains a `?` query separator, only the bytes before it
are returned. The path is **not** percent-decoded — that's the
router's job.
""".
-spec path(request()) -> binary().
path(#{target := T}) ->
case binary:split(T, persistent_term:get(?QMARK_CP_KEY)) of
[P, _Q] -> P;
[P] -> P
end.
-doc """
Return the raw query string portion of the request-target, without the
leading `?`. Empty binary when no `?` is present (or nothing follows it).
For decoded `{Key, Value}` pairs, pipe through `roadrunner_qs:parse/1`.
""".
-spec qs(request()) -> binary().
qs(#{target := T}) ->
case binary:split(T, persistent_term:get(?QMARK_CP_KEY)) of
[_P, Q] -> Q;
[_P] -> <<>>
end.
-doc "Return the HTTP version tuple ({1,0} or {1,1}).".
-spec version(request()) -> version().
version(#{version := V}) -> V.
-doc "Return the full ordered list of `{Name, Value}` header pairs.".
-spec headers(request()) -> headers().
headers(#{headers := H}) -> H.
-doc """
Look up a single header value by name. Returns `undefined` if absent.
The lookup is case-insensitive on `Name` — the parser already
lowercases header names on the wire, so any-case input is normalized
before searching.
""".
-spec header(binary(), request()) -> binary() | undefined.
header(Name, #{headers := H}) when is_binary(Name) ->
Lower = roadrunner_bin:ascii_lowercase(Name),
case lists:keyfind(Lower, 1, H) of
{_, Value} -> Value;
false -> undefined
end.
-doc """
Return whether `Name` is present in the request headers.
Lookup is case-insensitive on `Name` — same convention as `header/2`.
""".
-spec has_header(binary(), request()) -> boolean().
has_header(Name, #{headers := H}) when is_binary(Name) ->
lists:keymember(roadrunner_bin:ascii_lowercase(Name), 1, H).
-doc """
Parse the query string portion of the request target into a list of
`{Key, Value}` pairs (or `{Key, true}` for bare flags) via
`roadrunner_qs:parse/1`.
Returns `[]` when the target has no query component.
""".
-spec parse_qs(request()) -> [{binary(), binary() | true}].
parse_qs(Req) ->
roadrunner_qs:parse(qs(Req)).
-doc """
Parse the `Cookie` request header into a list of `{Name, Value}` pairs
via `roadrunner_cookie:parse/1`.
Returns `[]` when the request carries no `Cookie` header.
""".
-spec parse_cookies(request()) -> [{binary(), binary()}].
parse_cookies(Req) ->
case header(~"cookie", Req) of
undefined -> [];
Value -> roadrunner_cookie:parse(Value)
end.
-doc """
Return the buffered request body bytes as seen by the handler.
The connection process embeds whatever bytes followed the header block
under the `body` map key before invoking the handler. Auto-mode delivers
the full body as `iodata()` (an iolist of recv chunks for multi-chunk
bodies, a single binary otherwise) so handlers that only need
`iolist_size/1` or `gen_tcp:send/2` skip a flatten. Handlers requiring
a flat binary call `iolist_to_binary/1` themselves.
Returns `<<>>` when the request has no body field (e.g. when a request
map is constructed manually outside the connection pipeline).
""".
-spec body(request()) -> iodata().
body(#{body := B}) -> B;
body(_) -> <<>>.
-doc """
Return whether the request carries a non-empty body.
Returns `false` for both an absent `body` field and an empty body.
Handlers can use this as a short-circuit before doing any body-aware
work. Uses `iolist_size/1` so the check is O(length of iolist), not
O(total bytes).
""".
-spec has_body(request()) -> boolean().
has_body(#{body := B}) -> iolist_size(B) > 0;
has_body(_) -> false.
-doc """
Read the request body in one shot. Works in both `auto` and `manual`
body-buffering modes:
- **auto** (default): the conn already buffered the body before
invoking the handler — this returns the buffered bytes unchanged.
`Req` is returned as-is.
- **manual**: the conn parked the body on the socket. This drains it
and returns the bytes. The returned `Req2` carries the updated
body-read state — to enable keep-alive on the same connection in
manual mode, hand `Req2` back via the 4-tuple handler return shape
`{Status, Headers, Body, Req2}` so the conn can drain whatever the
handler skipped.
""".
-spec read_body(request()) ->
{ok, iodata(), request()} | {error, term()}.
read_body(Req) ->
read_body(Req, #{}).
-doc """
Read the request body, optionally bounded by `length`.
`Opts` may contain `length => non_neg_integer()`. If absent, behaves
like `read_body/1` (drain to end). When set, returns up to `Length`
bytes per call:
- `{ok, Bytes, Req2}` — body is fully drained (no more bytes left).
- `{more, Bytes, Req2}` — more bytes remain; call again with `Req2`.
Works for both content-length and chunked framing — chunked bodies
are streamed transparently across chunk boundaries up to `Length`
bytes per call.
In `auto` mode the body is already buffered, so `length` has no
effect — the buffered bytes are returned in one shot.
""".
-spec read_body(request(), #{length => non_neg_integer()}) ->
{ok, iodata(), request()}
| {more, iodata(), request()}
| {error, term()}.
read_body(#{body_reader := BS} = Req, Opts) ->
Mode =
case Opts of
#{length := L} -> {length, L};
_ -> all
end,
case roadrunner_conn:consume_body_reader(BS, Mode) of
{ok, Bytes, BS2} ->
{ok, Bytes, Req#{body_reader := BS2, body => Bytes}};
{more, Bytes, BS2} ->
{more, Bytes, Req#{body_reader := BS2}};
{error, _} = E ->
E
end;
read_body(Req, _Opts) ->
%% Auto mode (or a manually-constructed req): the body is already
%% sitting in the `body` field — return it.
{ok, body(Req), Req}.
-doc """
Read the next decoded HTTP chunk from a chunked-encoded request body.
Mirrors cowboy's chunk-at-a-time read pattern: each call returns one
chunk's payload. `{more, Bytes, Req2}` means more chunks remain;
`{ok, <<>>, Req2}` signals end-of-body (the size-0 last chunk has
been seen).
For non-chunked framing (auto mode, content-length, or no body),
`read_body_chunked/1` falls through to `read_body/1`'s behavior — the
buffered body comes back in one shot. The "chunk boundary" concept
only applies to wire-level chunked transfer encoding.
Threading is the same as `read_body/1,2`: hand `Req2` back to the
conn via the `{Response, Req2}` handler return shape so any unread
chunks get drained for keep-alive.
""".
-spec read_body_chunked(request()) ->
{ok, iodata(), request()}
| {more, iodata(), request()}
| {error, term()}.
read_body_chunked(#{body_reader := BS} = Req) ->
case roadrunner_conn:consume_body_reader(BS, next_chunk) of
{ok, Bytes, BS2} ->
{ok, Bytes, Req#{body_reader := BS2, body => Bytes}};
{more, Bytes, BS2} ->
{more, Bytes, Req#{body_reader := BS2}};
{error, _} = E ->
E
end;
read_body_chunked(Req) ->
{ok, body(Req), Req}.
-doc """
Read and parse a form-encoded request body. Inspects the request's
`Content-Type` and dispatches:
- `application/x-www-form-urlencoded[; …]` →
`{ok, urlencoded, [{Name, Value | true}], Req2}`. Values are
percent-decoded (and `+` → space) per `roadrunner_qs:parse/1`. Bare
flags come back as `{Name, true}`.
- `multipart/form-data; boundary=…` →
`{ok, multipart, [Part], Req2}` where each `Part` is the map
returned by `roadrunner_multipart:parse/2` (`#{headers, body}`).
`{error, no_boundary}` if the boundary parameter is missing.
Other content types return `{error, unsupported_content_type}`;
absent `Content-Type` returns `{error, no_content_type}`. Reads the
body via `read_body/1` (so works in both `auto` and `manual`
buffering modes), and threads `Req2` back so trailing body bytes
get drained on keep-alive.
""".
-spec read_form(request()) ->
{ok, urlencoded, [{binary(), binary() | true}], request()}
| {ok, multipart, [roadrunner_multipart:part()], request()}
| {error, no_content_type | unsupported_content_type | no_boundary | term()}.
read_form(Req) ->
case header(~"content-type", Req) of
undefined ->
{error, no_content_type};
ContentType ->
dispatch_form(content_type_kind(ContentType), ContentType, Req)
end.
-spec content_type_kind(binary()) -> urlencoded | multipart | unsupported.
content_type_kind(ContentType) ->
[Type | _] = binary:split(ContentType, persistent_term:get(?SEMI_CP_KEY)),
case roadrunner_bin:ascii_lowercase(roadrunner_bin:trim_ows(Type)) of
~"application/x-www-form-urlencoded" -> urlencoded;
~"multipart/form-data" -> multipart;
_ -> unsupported
end.
-spec dispatch_form(urlencoded | multipart | unsupported, binary(), request()) ->
{ok, urlencoded, [{binary(), binary() | true}], request()}
| {ok, multipart, [roadrunner_multipart:part()], request()}
| {error, term()}.
dispatch_form(urlencoded, _ContentType, Req) ->
case read_body(Req) of
{ok, Body, Req2} ->
%% Parser requires a flat binary; flatten only here, not at
%% every body read.
{ok, urlencoded, roadrunner_qs:parse(iolist_to_binary(Body)), Req2};
{error, _} = E ->
E
end;
dispatch_form(multipart, ContentType, Req) ->
maybe
{ok, Boundary} ?= roadrunner_multipart:boundary(ContentType),
{ok, Body, Req2} ?= read_body(Req),
{ok, Parts} ?= roadrunner_multipart:parse(iolist_to_binary(Body), Boundary),
{ok, multipart, Parts, Req2}
end;
dispatch_form(unsupported, _ContentType, _Req) ->
{error, unsupported_content_type}.
-doc """
Return the router-captured bindings for this request as a
`#{Name => Value}` map of binaries.
`roadrunner_conn` populates this from `roadrunner_router:match/2` before
invoking the handler. Empty map when the listener is in single-handler
mode (no router) or the matched route has no `:param` segments.
""".
-spec bindings(request()) -> roadrunner_router:bindings().
bindings(#{bindings := B}) -> B;
bindings(_) -> #{}.
-doc """
Return the TCP peer (`{IpAddress, Port}`) for the connection that
delivered this request.
`roadrunner_conn` populates this from `inet:peername/1` once per
connection. Returns `undefined` when the request map has no peer
field (e.g. constructed manually outside the connection pipeline) or
when the OS call failed at accept time.
""".
-spec peer(request()) ->
{inet:ip_address(), inet:port_number()} | undefined.
peer(#{peer := P}) -> P;
peer(_) -> undefined.
-doc """
Return the leftmost client identifier from the `Forwarded` header
(RFC 7239) or, if absent, from `X-Forwarded-For`. Returns `undefined`
when neither header is set or the `Forwarded` header has no `for=`
parameter.
The returned binary is whatever the proxy chose to put there — for
RFC 7239 that's typically an IP literal (`192.0.2.60`) or a quoted
IPv6+port (`[2001:db8::1]:4711`); for `X-Forwarded-For` it's
conventionally just the IP. The caller decides how to parse it.
**No trust list is enforced.** Anyone who can speak to the listener
directly can spoof these headers — only call this when the deploy
sits behind a trusted reverse proxy that strips/overwrites them.
""".
-spec forwarded_for(request()) -> binary() | undefined.
forwarded_for(Req) ->
case header(~"forwarded", Req) of
undefined ->
x_forwarded_for(Req);
Value ->
%% First forwarded-element wins; multiple proxies append
%% comma-separated entries with the original client leftmost.
[First | _] = binary:split(Value, persistent_term:get(?COMMA_CP_KEY)),
empty_to_undefined(
find_for_param(
binary:split(
roadrunner_bin:trim_ows(First), persistent_term:get(?SEMI_CP_KEY), [global]
),
persistent_term:get(?EQ_CP_KEY)
)
)
end.
%% Normalize empty values to `undefined` so callers can pattern-match
%% one shape regardless of which header path produced the result.
-spec empty_to_undefined(binary() | undefined) -> binary() | undefined.
empty_to_undefined(<<>>) -> undefined;
empty_to_undefined(Other) -> Other.
-spec x_forwarded_for(request()) -> binary() | undefined.
x_forwarded_for(Req) ->
case header(~"x-forwarded-for", Req) of
undefined ->
undefined;
Value ->
[First | _] = binary:split(Value, persistent_term:get(?COMMA_CP_KEY)),
case roadrunner_bin:trim_ows(First) of
<<>> -> undefined;
Trimmed -> Trimmed
end
end.
-spec find_for_param([binary()], binary:cp()) -> binary() | undefined.
find_for_param([], _EqCp) ->
undefined;
find_for_param([Pair | Rest], EqCp) ->
case binary:split(Pair, EqCp) of
[Key, Val] ->
case roadrunner_bin:ascii_lowercase(roadrunner_bin:trim_ows(Key)) of
~"for" -> unquote_param(roadrunner_bin:trim_ows(Val));
_ -> find_for_param(Rest, EqCp)
end;
_ ->
find_for_param(Rest, EqCp)
end.
-spec unquote_param(binary()) -> binary().
unquote_param(<<$", Rest/binary>>) ->
case binary:match(Rest, persistent_term:get(?QUOTE_CP_KEY)) of
{End, _} -> binary:part(Rest, 0, End);
nomatch -> Rest
end;
unquote_param(Bin) ->
Bin.
-doc """
Return the connection scheme — `http` for plain TCP, `https` for TLS.
`roadrunner_conn` sets this once per connection from the transport tag.
Defaults to `http` for request maps constructed manually outside the
connection pipeline.
""".
-spec scheme(request()) -> http | https.
scheme(#{scheme := S}) -> S;
scheme(_) -> http.
-doc """
Return the opaque per-route handler state attached at compile time.
Sources, listed by route shape:
- 3-tuple `{Path, Handler, State}` or map `#{path, handler, state}`
list entry.
- Listener single-handler `{Module, State}` tuple or
`#{handler, state, ...}` map.
Returns `undefined` for shapes that don't carry state (2-tuple route,
bare-atom single-handler, map without a `state` key).
""".
-spec state(request()) -> term().
state(#{state := S}) -> S;
state(_) -> undefined.
-doc """
Return the per-request correlation token attached by `roadrunner_conn`.
16 lowercase hex chars (8 bytes of CSPRNG output), unique per request
even on the same keep-alive connection. Mirrored into the conn
process's `logger` metadata, so any `?LOG_*` call in middleware or
the handler is automatically annotated with the same id.
`undefined` for manually-constructed request maps used in tests.
""".
-spec request_id(request()) -> binary() | undefined.
request_id(#{request_id := Id}) -> Id;
request_id(_) -> undefined.
%% `-on_load` callback. See `feedback_compile_pattern_convention`.
-spec init_patterns() -> ok.
init_patterns() ->
persistent_term:put(?QMARK_CP_KEY, binary:compile_pattern(~"?")),
persistent_term:put(?SEMI_CP_KEY, binary:compile_pattern(~";")),
persistent_term:put(?COMMA_CP_KEY, binary:compile_pattern(~",")),
persistent_term:put(?EQ_CP_KEY, binary:compile_pattern(~"=")),
persistent_term:put(?QUOTE_CP_KEY, binary:compile_pattern(~"\"")),
ok.