Skip to main content

src/roadrunner_resp.erl

-module(roadrunner_resp).
-moduledoc """
Convenience builders for the buffered `{Status, Headers, Body}`
triple — one of the five shapes a roadrunner handler can return.

Each helper sets `Content-Type` and `Content-Length` so handlers
don't repeat the boilerplate. Body framing (`Connection: close`,
`Transfer-Encoding`, etc.) is still the connection layer's job —
these helpers only fill in what the handler can know.

These helpers only construct the buffered form
(`t:buffered_response/0`). Streaming, loop, sendfile, and
WebSocket-upgrade handlers build their own tuple directly; see
`t:roadrunner_handler:response/0` for the full union.
""".

-export([
    text/2,
    html/2,
    json/2,
    redirect/2,
    add_header/3,
    set_cookie/4,
    no_content/0,
    bad_request/0,
    unauthorized/0,
    forbidden/0,
    not_found/0,
    internal_error/0,
    status/1
]).

-export_type([buffered_response/0]).

-type buffered_response() :: {roadrunner_http:status(), roadrunner_http:headers(), iodata()}.

-doc "Plain-text response with `text/plain; charset=utf-8`.".
-spec text(StatusCode :: roadrunner_http:status(), Body :: iodata()) -> buffered_response().
text(Status, Body) ->
    with_length(Status, ~"text/plain; charset=utf-8", Body).

-doc """
HTML response with `text/html; charset=utf-8`.

The charset suffix is intentional — modern browsers default to
ISO-8859-1 absent an explicit charset in the response header,
which is wrong for ~all current content. To match a server that
emits bare `text/html` (e.g. cowboy's default), override after the
build:

```erlang
roadrunner_resp:add_header(
    roadrunner_resp:html(200, Body), ~"content-type", ~"text/html"
).
```

`add_header/3` prepends, so the bare value wins on header lookup.
""".
-spec html(StatusCode :: roadrunner_http:status(), Body :: iodata()) -> buffered_response().
html(Status, Body) ->
    with_length(Status, ~"text/html; charset=utf-8", Body).

-doc """
JSON response — the term is encoded via the stdlib `json` module
(OTP 27+) and `Content-Type` is set to `application/json`.
""".
-spec json(StatusCode :: roadrunner_http:status(), Term :: term()) -> buffered_response().
json(Status, Term) ->
    Body = json:encode(Term),
    with_length(Status, ~"application/json", Body).

-doc """
Redirect response — sets the `Location` header and an empty body.
Use a 3xx status (typically 301, 302, 303, 307, or 308).
""".
-spec redirect(StatusCode :: roadrunner_http:redirect_status(), Location :: binary()) ->
    buffered_response().
redirect(Status, Location) when is_binary(Location) ->
    {Status,
        [
            {~"location", Location},
            {~"content-length", ~"0"}
        ],
        ~""}.

-spec with_length(roadrunner_http:status(), binary(), iodata()) -> buffered_response().
with_length(Status, ContentType, Body) ->
    {Status,
        [
            {~"content-type", ContentType},
            {~"content-length", integer_to_binary(iolist_size(Body))}
        ],
        Body}.

-doc """
Prepend a header to an existing response triple.

The header is added to the front of the list — last-write-wins for
any subsequent lookup. `Value` may be iodata; it is flattened into a
binary so the wire encoder doesn't have to.
""".
-spec add_header(buffered_response(), Name :: binary(), Value :: iodata()) -> buffered_response().
add_header({Status, Headers, Body}, Name, Value) when is_binary(Name) ->
    {Status, [{Name, iolist_to_binary(Value)} | Headers], Body}.

-doc """
Add a `Set-Cookie` header to a response — wraps `roadrunner_cookie:serialize/3`
so handlers don't have to.
""".
-spec set_cookie(
    buffered_response(), Name :: binary(), Value :: binary(), roadrunner_cookie:serialize_opts()
) ->
    buffered_response().
set_cookie(Resp, Name, Value, Opts) ->
    add_header(Resp, ~"set-cookie", roadrunner_cookie:serialize(Name, Value, Opts)).

-doc "Empty 204 No Content response.".
-spec no_content() -> buffered_response().
no_content() -> empty_status(204).

-doc "Empty 400 Bad Request response.".
-spec bad_request() -> buffered_response().
bad_request() -> empty_status(400).

-doc "Empty 401 Unauthorized response.".
-spec unauthorized() -> buffered_response().
unauthorized() -> empty_status(401).

-doc "Empty 403 Forbidden response.".
-spec forbidden() -> buffered_response().
forbidden() -> empty_status(403).

-doc "Empty 404 Not Found response.".
-spec not_found() -> buffered_response().
not_found() -> empty_status(404).

-doc "Empty 500 Internal Server Error response.".
-spec internal_error() -> buffered_response().
internal_error() -> empty_status(500).

-doc """
Empty-body response with an arbitrary status code — handy for
statuses outside the named shortcut set (e.g., 418, 503).
""".
-spec status(roadrunner_http:status()) -> buffered_response().
status(Code) -> empty_status(Code).

-spec empty_status(roadrunner_http:status()) -> buffered_response().
empty_status(Status) ->
    {Status, [{~"content-length", ~"0"}], ~""}.