nhttp_cookie (nhttp_lib v1.1.1)

View Source

Cookie parsing and encoding utilities (RFC 6265).

This module provides symmetrical encode/decode functions for both Cookie and Set-Cookie headers.

The Cookie header contains simple name=value pairs separated by semicolons.

%% Decode incoming Cookie header
{ok, Cookies} = nhttp_cookie:decode_cookie(<<"session=abc123; user=john">>),
%% Cookies = [#{name => <<"session">>, value => <<"abc123">>},
%%            #{name => <<"user">>, value => <<"john">>}]

{ok, CookieHeader} = nhttp_cookie:encode_cookie([
    #{name => <<"session">>, value => <<"abc123">>},
    #{name => <<"user">>, value => <<"john">>}
]),
%% <<"session=abc123; user=john">>

The Set-Cookie header contains a single cookie with optional attributes.

%% Decode incoming Set-Cookie header
{ok, SetCookie} = nhttp_cookie:decode_set_cookie(
    <<"session=abc123; Path=/; HttpOnly; Secure">>
),
%% SetCookie = #{name => <<"session">>, value => <<"abc123">>,
%%               path => <<"/">>, http_only => true, secure => true, ...}

%% Encode Set-Cookie header for responses
{ok, SetCookieHeader} = nhttp_cookie:encode_set_cookie(#{
    name => <<"session">>,
    value => <<"abc123">>,
    path => <<"/">>,
    max_age => 3600,
    http_only => true,
    secure => true,
    same_site => strict
}),
%% <<"session=abc123; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Strict">>

Roundtrip Support

The decode output can be fed back to encode when it conforms to the RFC 6265 Section 4.1.1 grammar:

{ok, SetCookie} = nhttp_cookie:decode_set_cookie(Header),
%% Modify and re-encode
{ok, NewHeader} = nhttp_cookie:encode_set_cookie(SetCookie#{max_age => 7200}).

The roundtrip is not total, and it must not be. The Section 5.2 parsing algorithm is more permissive than the Section 4.1.1 grammar on purpose, so decode_set_cookie/1 accepts a value that carries internal whitespace, a comma, or a CR and LF pair. The encoders enforce the grammar and refuse such a value with {error, Reason}. Re-emitting it is the header injection that the validation exists to stop.

A forwarder must therefore pass the original field value through rather than decode it and encode it again. The decoded map is lossy in the same direction: decode_set_cookie/1 drops every attribute it does not name, so a re-encode also discards the rest.

Summary

Functions

Decode a Cookie header value into a list of cookies. Parses a semicolon-separated Cookie header and returns a list of cookie maps. Empty names are rejected with an error.

Decode a Set-Cookie header value into a set-cookie map. Parses a Set-Cookie header with attributes and returns a map containing the cookie name, value, and any parsed attributes.

Encode a list of cookies into a Cookie header value. Takes a list of cookie maps and produces a semicolon-separated string suitable for the Cookie header.

Encode a set-cookie map into a Set-Cookie header value. Takes a map with name and value (required) plus optional attributes, and produces a Set-Cookie header string.

Types

cookie_error()

-type cookie_error() ::
          empty_name | invalid_format |
          {invalid_cookie_name, name_violation()} |
          {invalid_cookie_value, value_violation()}.

domain_violation()

-type domain_violation() :: empty | empty_label | invalid_label.

name_violation()

-type name_violation() :: empty | non_token_octet.

path_violation()

-type path_violation() :: empty | no_leading_slash | control_char | semicolon | non_ascii.

set_cookie()

-type set_cookie() ::
          #{name := binary(),
            value := binary(),
            path => binary(),
            domain => binary(),
            expires => calendar:datetime(),
            max_age => integer(),
            secure => boolean(),
            http_only => boolean(),
            same_site => strict | lax | none}.

t()

-type t() :: #{name := binary(), value := binary()}.

value_violation()

-type value_violation() :: control_char | separator | non_ascii | unbalanced_quote.

Functions

decode_cookie/1

-spec decode_cookie(binary()) -> {ok, [t()]} | {error, cookie_error()}.

Decode a Cookie header value into a list of cookies. Parses a semicolon-separated Cookie header and returns a list of cookie maps. Empty names are rejected with an error.

{ok, Cookies} = nhttp_cookie:decode_cookie(<<"session=abc; user=john">>).
%% Cookies = [#{name => <<"session">>, value => <<"abc">>},
%%            #{name => <<"user">>, value => <<"john">>}]

decode_set_cookie(SetCookieHeader)

-spec decode_set_cookie(binary()) -> {ok, set_cookie()} | {error, set_cookie_error()}.

Decode a Set-Cookie header value into a set-cookie map. Parses a Set-Cookie header with attributes and returns a map containing the cookie name, value, and any parsed attributes.

{ok, SetCookie} = nhttp_cookie:decode_set_cookie(
    <<"session=abc; Path=/; Secure; HttpOnly">>
).
%% SetCookie = #{name => <<"session">>, value => <<"abc">>,
%%               path => <<"/">>, secure => true, http_only => true, ...}

Invalid attributes are silently ignored (per RFC 6265 recommendations).

encode_cookie/1

-spec encode_cookie([t()]) -> {ok, binary()} | {error, cookie_error()}.

Encode a list of cookies into a Cookie header value. Takes a list of cookie maps and produces a semicolon-separated string suitable for the Cookie header.

{ok, Header} = nhttp_cookie:encode_cookie([
    #{name => <<"session">>, value => <<"abc">>},
    #{name => <<"user">>, value => <<"john">>}
]).
%% Header = <<"session=abc; user=john">>

Every pair is validated against RFC 6265 Section 4.1.1, the grammar that Section 4.2.1 reuses for cookie-string. A name must be a token. A value must be *cookie-octet, or *cookie-octet inside a matched pair of double quotes, which admits %x21, %x23-2B, %x2D-3A, %x3C-5B, and %x5D-7E. That excludes CTLs, space, double quote, comma, semicolon, backslash, and every octet above %x7E. A value that leaves the set is refused with {error, {invalid_cookie_value, t:value_violation/0}}. No value is stripped, quoted, escaped, or truncated, and the error carries a class rather than the offending bytes, which are often a session token. Encode arbitrary data with Base64 before you put it in a cookie.

encode_set_cookie/1

-spec encode_set_cookie(set_cookie()) -> {ok, binary()} | {error, set_cookie_error()}.

Encode a set-cookie map into a Set-Cookie header value. Takes a map with name and value (required) plus optional attributes, and produces a Set-Cookie header string.

{ok, Header} = nhttp_cookie:encode_set_cookie(#{
    name => <<"session">>,
    value => <<"abc123">>,
    path => <<"/">>,
    max_age => 3600,
    secure => true
}).
%% Header = <<"session=abc123; Path=/; Max-Age=3600; Secure">>

Supported attributes: path, domain, expires, max_age, secure, http_only, same_site. The cookie pair is validated against the RFC 6265 Section 4.1.1 grammar. A name must be a token. A value must be *cookie-octet, or *cookie-octet inside a matched pair of double quotes, which admits %x21, %x23-2B, %x2D-3A, %x3C-5B, and %x5D-7E. That excludes CTLs, space, double quote, comma, semicolon, backslash, and every octet above %x7E. Encode arbitrary data with Base64 before you put it in a cookie. The two attributes that carry caller data are validated too. path must be <any CHAR except CTLs or ";"> and must start with /, because Section 5.2.4 makes a user agent discard any other value. domain must be a <subdomain> per RFC 1034 Section 3.5 and RFC 1123 Section 2.1: dot-separated labels of letters, digits, and hyphens, with an optional leading dot that Section 5.2.3 strips. A field that leaves its grammar is refused with {error, t:set_cookie_error/0}. Nothing is stripped, quoted, escaped, or truncated, and an error carries a class rather than the offending bytes, which are often a session token.