src/cowmachine_req.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2016-2019 Marc Worrell
%%
%% @doc Request functions for cowmachine.
%% @reference See more information related to Cowboy HTTP server for Erlang/OTP at 
%% <a href="https://github.com/ninenines/cowboy">Cowboy</a>.
%% @end

%% Copyright 2016-2019 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(cowmachine_req).
-author("Marc Worrell <marc@worrell.nl").

-include("cowmachine_log.hrl").

-export([
    init_env/2
    ]).

-export([
    init_context/3,
    set_req/2,
    req/1,
    set_env/2,
    env/1
    ]).

-export([
    controller/1,
    controller_options/1,

    site/1,
    method/1,
    version/1,
    base_uri/1,
    scheme/1,
    host/1,
    port/1,
    is_ssl/1,
    is_proxy/1,
    peer/1,
    peer_ip/1,

    raw_path/1,
    path/1,
    qs/1,
    req_qs/1,

    path_info/1,

    set_disp_path/2,
    disp_path/1,

    get_req_header/2,
    get_req_headers/1,
    set_resp_content_type/2,
    resp_content_type/1,

    set_range_ok/2,
    is_range_ok/1,

    set_resp_header/3,
    set_resp_headers/2,
    get_resp_header/2,
    get_resp_headers/1,
    remove_resp_header/2,

    set_resp_cookie/4,
    get_resp_cookies/1,

    get_cookie_value/2,
    req_cookie/1,

    set_response_code/2,
    response_code/1,

    set_resp_chosen_charset/2,
    resp_chosen_charset/1,

    set_resp_transfer_encoding/2,
    resp_transfer_encoding/1,

    set_resp_content_encoding/2,
    resp_content_encoding/1,
    encode_content/2,

    set_resp_body/2,
    resp_body/1,
    has_resp_body/1,

    set_resp_redirect/2,
    resp_redirect/1,

    has_req_body/1,
    req_body/1,
    req_body/2,

    stream_req_body/2,

    set_metadata/3,
    get_metadata/2
    ]).


-type context_map() :: #{
    cowreq := cowboy_req:req(),
    cowenv := cowboy_middleware:env(),
    any() => any()
}.

-type context() :: context_map() | tuple(). 
%% The request context stores everything needed for the request handling.
%%
%% Inside Zotonic all functions work with a site specific context, this
%% context has optionally a request. That is why the context is wrapped around
%% the cowboy request.
%%
%% The cowmachine context must be the cowreq record, a tuple or a map.
%% If it is a tuple then it is assumed to be a record then the cowreq
%% is at position 2 and the cowenv at position 3.


-type halt() :: {error, term()} | {halt, 200..599}.
%% Used to stop a request with a specific HTTP status code

-type resp_body() :: iodata()
                   | {device, Size::non_neg_integer(), file:io_device()}
                   | {device, file:io_device()}
                   | {file, Size::non_neg_integer(), file:filename_all()}
                   | {file, file:filename_all()}
                   | {stream, {streamdata(), streamfun()}}
                   | {stream, Size::non_neg_integer(), {streamdata(), streamfun()}}
                   | {stream, streamfun()}
                   | {stream, Size::non_neg_integer(), streamfun()}
                   | {writer, writerfun()}
                   | undefined. 
%% Response body, can be data, a file, device or streaming functions.

-type streamfun() :: fun( ( parts(), context() ) -> {streamdata(), streamfun_next()} )
                   | fun( ( context() ) -> {streamdata(), streamfun_next()} )
                   | fun( () -> {streamdata(), streamfun_next()} )
                   | done.
%% Streaming function, repeatedly called to fetch the next chunk

-type streamfun_next() :: fun( ( context() ) -> {streamdata(), streamfun_next()} )
                        | fun( () -> {streamdata(), streamfun_next()} )
                        | done.

-type streamdata() :: iodata()
                    | {file, non_neg_integer(), file:filename_all()}
                    | {file, file:filename_all()}.

-type writerfun() :: fun( (outputfun(), context()) -> context() ).
%% Writer function, calls output function till finished

-type outputfun() :: fun( (iodata(), IsFinal::boolean(), context()) -> context() ).

-type media_type() :: binary()
                    | {binary(), binary(), list( {binary(), binary()} )}
                    | {binary(), binary()}
                    | {binary(), list( {binary(), binary()} )}.
%% Media types for accepted and provided content types

-type parts() :: all
               | {ranges(), Size :: non_neg_integer(), Boundary :: binary(), ContentType :: binary()}.
-type ranges() :: [ {Offset :: non_neg_integer(), Length :: non_neg_integer()} ].

-export_type([
    context/0,
    context_map/0,
    halt/0,
    resp_body/0,
    streamfun/0,
    streamfun_next/0,
    streamdata/0,
    media_type/0,
    parts/0,
    ranges/0
]).

%% @doc Set some intial metadata in the cowboy req.

-spec init_env(Req, Env) -> Result when
	Req :: cowboy_req:req(), 
	Env :: cowboy_middleware:env(), 
	Result :: cowboy_middleware:env().
init_env(Req, Env) ->
    Bindings = maps:get(bindings, Req, #{}),
    Env1 = lists:foldl(
                fun(K, Acc) -> maps:remove(K, Acc) end,
                Env,
                [ site, context ]),
    EnvProxy = ensure_proxy_args(Req, Env1),
    EnvProxy#{
        cowmachine_site => maps:get(site, Env, undefined),

        cowmachine_controller => maps:get(cowmachine_controller, Env),
        cowmachine_controller_options => maps:get(cowmachine_controller_options, Env, []),

        cowmachine_resp_code => 500,
        cowmachine_resp_redirect => false,
        cowmachine_resp_content_encoding => <<"identity">>,
        cowmachine_resp_transfer_encoding => undefined,
        cowmachine_resp_content_type => <<"binary/octet-stream">>,
        cowmachine_resp_chosen_charset => undefined,
        cowmachine_resp_body => undefined,

        cowmachine_disp_path => maps:get('*', Bindings, undefined),
        cowmachine_range_ok => true,

        cowmachine_cookies => cowboy_req:parse_cookies(Req)
    }.

-spec ensure_proxy_args(Req, Env) -> Result when
	Req :: cowboy_req:req(), 
	Env :: cowboy_middleware:env(), 
	Result :: cowboy_middleware:env().
ensure_proxy_args(Req, Env) ->
    case maps:get(cowmachine_proxy, Env, undefined) of
        undefined -> cowmachine_proxy:update_env(Req, Env);
        IsProxy when is_boolean(IsProxy) -> Env
    end.

%% @doc Initialize the context with the `Req' and `Env'.

-spec init_context(Req, Env, Options) -> Result when
	Req :: cowboy_req:req(),
	Env :: cowboy_middleware:env(),
	Options :: undefined | map() | tuple(),
	Result :: context().
init_context( Req, Env, undefined ) ->
    init_context(Req, Env, #{});
init_context( Req, Env, M ) when is_map(M) ->
    M#{ cowreq => Req, cowenv => Env };
init_context( Req, Env, Tuple) when is_tuple(Tuple) ->
    T1 = setelement(2, Tuple, Req),
    setelement(3, T1, Env).

%% @doc Update the cowboy request in the context.

-spec set_req(Req, Context) -> Result when
	Req :: cowboy_req:req(),
	Context :: context(),
	Result :: context().
set_req(Req, Context) when is_tuple(Context) ->
    erlang:setelement(2, Context, Req);
set_req(Req, #{ cowreq := _ } = Map) ->
    Map#{ cowreq => Req }.

%% @doc Fetch the cowboy request from the context.

-spec req(Context) -> Result when
	Context :: context(),
	Result :: cowboy_req:req().
req(Context) when is_tuple(Context) ->
    erlang:element(2, Context);
req(#{ cowreq := Req }) ->
    Req.

%% @doc Update the cowboy middleware env in the context.

-spec set_env(Env, Context) -> Result when
	Env :: cowboy_middleware:env(), 
	Context :: context(),
	Result :: context().
set_env(Env, Context) when is_tuple(Context) ->
    erlang:setelement(3, Context, Env);
set_env(Env, #{ cowenv := _ } = Map) ->
    Map#{ cowenv => Env }.

%% @doc Fetch the cowboy middleware env from the context.

-spec env(Context) -> Result when
	Context :: context(),
	Result :: cowboy_middleware:env().
env(Context) when is_tuple(Context) ->
    erlang:element(3, Context);
env(#{ cowenv := Env }) ->
    Env.

%% @doc Return the current cowmachine controller.

-spec controller(Context) -> Result when
	Context :: context(),
	Result :: module().
controller(Context) ->
    maps:get(cowmachine_controller, env(Context)).

%% @doc Return the current cowmachine controller options.

-spec controller_options(Context) -> Result when
	Context :: context(),
	Result :: list().
controller_options(Context) ->
    maps:get(cowmachine_controller_options, env(Context), []).

%% @doc Return the cowmachine site.

-spec site(Context) -> Site when
	Context :: context(),
	Site :: atom().
site(Context) ->
    maps:get(cowmachine_site, env(Context)).

%% @doc Return the request Method.

-spec method(Context) -> Result when
	Context :: context(),
	Result :: binary().
method(Context) ->
    cowboy_req:method(req(Context)).

%% @doc Return the http version as a tuple `{major, minor}'.

-spec version(Context) -> Result when
	Context :: context(),
	Result :: {Major, Minor},
	Major :: integer(), 
	Minor :: integer().
version(Context) ->
    case cowboy_req:version(req(Context)) of
        'HTTP/1.0' -> {1,0};
        'HTTP/1.1' -> {1,1};
        'HTTP/2'   -> {2,0}
    end.

%% @doc Return the base uri of the request.

-spec base_uri(Context) -> Result when
	Context :: context(),
	Result :: binary().
base_uri(Context) ->
    Scheme = scheme(Context),
    Uri = [
        z_convert:to_binary(Scheme),
        <<"://">>,
        host(Context),
        case port(Context) of
            80 when Scheme =:= http -> [];
            433 when Scheme =:= https -> [];
            Port -> [ $:, integer_to_binary(Port) ]
        end,
        $/
    ],
    iolist_to_binary(Uri).

%% @doc Return the scheme used (`https' or `http').

-spec scheme(Context) -> Result when
	Context :: context(),
	Result :: http | https.
scheme(Context) ->
    case maps:get(cowmachine_forwarded_proto, env(Context)) of
        <<"http">> -> http;
        <<"https">> -> https
    end.

%% @doc Return the http host.

-spec host(Context) -> Result when
	Context :: context(),
	Result :: binary().
host(Context) ->
    maps:get(cowmachine_forwarded_host, env(Context)).

%% @doc Return the http port.

-spec port(Context) -> Result when
	Context :: context(),
	Result :: integer().
port(Context) ->
    maps:get(cowmachine_forwarded_port, env(Context)).

%% @doc Check if the connection is secure (SSL).

-spec is_ssl(Context) -> Result when
	Context :: context(),
	Result :: boolean().
is_ssl(Context) ->
    https =:= scheme(Context).

%% @doc Check if the request is forwarded by a proxy.

-spec is_proxy(Context) -> Result when
	Context :: context(),
	Result :: boolean().
is_proxy(Context) ->
    maps:get(cowmachine_proxy, env(Context)).

%% @doc Return the peer of this request, take `x-forwarded-for' into account if the peer
%%      is an ip4 LAN address.

-spec peer(Context) -> Result when
	Context :: context(),
	Result :: binary().
peer(Context) ->
    maps:get(cowmachine_remote, env(Context)).

%% @doc Return the peer of this request, take `x-forwarded-for' into account if the peer
%%      is an ip4 LAN address.

-spec peer_ip(Context) -> Result when
	Context :: context(),
	Result :: tuple().
peer_ip(Context) ->
    maps:get(cowmachine_remote_ip, env(Context)).


%% @doc Return the undecoded request path as-is, including the query string.

-spec raw_path(Context) -> Result when
	Context :: context(),
	Result :: binary().
raw_path(Context) ->
    Path = cowboy_req:path(req(Context)),
    case qs(Context) of
        <<>> -> Path;
        Qs -> <<Path/binary, $?, Qs/binary>>
    end.

%% @doc Return the undecoded request path as-is.

-spec path(Context) -> Result when
	Context :: context(),
	Result :: binary().
path(Context) ->
    cowboy_req:path(req(Context)).

%% @doc Return the undecoded query string, `<<>>' when no query string.

-spec qs(Context) -> Result when
	Context :: context(),
	Result :: binary().
qs(Context) ->
    cowboy_req:qs(req(Context)).

%% @doc Return the decoded query string, `[]' when no query string.

-spec req_qs(Context) -> Result when
	Context :: context(),
	Result :: list({binary(), binary()}).
req_qs(Context) ->
    Qs = qs(Context),
    cowmachine_util:parse_qs(Qs).

%% @doc Fetch all bindings from the dispatcher.

-spec path_info(Context) -> Result when
	Context :: context(),
	Result :: cowboy_router:bindings().
path_info(Context) ->
    maps:get(bindings, req(Context), #{}).

%% @doc Fetch a request header, the header must be a lowercase binary.

-spec get_req_header(Header, Context) -> Result when
	Header :: binary(),
	Context :: context(),
	Result :: undefined | binary().
get_req_header(H, Context) when is_binary(H) ->
    cowboy_req:header(H, req(Context)).

%% @doc Fetch all request headers.

-spec get_req_headers(Context) -> Result when
	Context :: context(),
	Result :: #{ binary() => binary() }.
get_req_headers(Context) ->
    cowboy_req:headers(req(Context)).

%% @doc Set the content type of the response

-spec set_resp_content_type(cow_http_hd:media_type() | binary(), context()) -> context().
set_resp_content_type(CT, Context) when is_binary(CT) ->
    set_resp_content_type(cow_http_hd:parse_content_type(CT), Context);
set_resp_content_type(CT, Context) when is_tuple(CT) ->
    Env = env(Context),
    set_env(Env#{ cowmachine_resp_content_type => CT }, Context).

%% @doc Fetch the content type of the response.

-spec resp_content_type(Context) -> Result when
	Context :: context(),
	Result :: cow_http_hd:media_type().
resp_content_type(Context) ->
    maps:get(cowmachine_resp_content_type, env(Context)).

%% @doc Set the `is_range_ok' flag.

-spec set_range_ok(IsRangeOk, Context) -> Result when
	IsRangeOk :: boolean(),
	Context :: context(),
	Result :: context().
set_range_ok(IsRangeOk, Context) ->
    Env = env(Context),
    set_env(Env#{cowmachine_range_ok => IsRangeOk}, Context).

%% @doc Fetch the `is_range_ok' flag.

-spec is_range_ok(context()) -> boolean().
is_range_ok(Context) ->
    maps:get(cowmachine_range_ok, env(Context)).

%% @doc Add a response header, replacing an existing header with the same name.<br/>
%% The header must be a lowercased binary. If the value is a list of binaries then
%% they are joined with a comma as separator.
%% @throws {http_invalid_location, Header, Value}

-spec set_resp_header(Header, Value, Context) -> Result when
	Header :: binary(),
	Value :: binary() | [binary()] |string(),
	Context :: context(),
	Result :: context().
set_resp_header(Header, [V|Rs], Context) when is_binary(Header), is_binary(V) ->
    V1 = iolist_to_binary([V, [ [", ", R] || R <- Rs] ]),
    set_resp_header(Header, V1, Context);
set_resp_header(Header, Value, Context)
    when Header =:= <<"location">>;
         Header =:= <<"content-location">> ->
    case cowmachine_util:valid_location(Value) of
        {true, Loc} ->
            set_resp_header_1(Header, Loc, Context);
        false ->
            throw({http_invalid_location, Header, Value})
    end;
set_resp_header(Header, Value, Context) when is_binary(Header) ->
    set_resp_header_1(Header, Value, Context).

%% @throws {http_invalid_location, Header, Value}

-spec set_resp_header_1(Header, Value, Context) -> Result when
	Header :: binary(),
	Value :: binary() | [binary()] |string(),
	Context :: context(),
	Result :: context().
set_resp_header_1(Header, Value, Context) ->
    V = z_convert:to_binary(Value),
    case cowmachine_util:is_valid_header(Header) andalso cowmachine_util:is_valid_header_value(V) of
        true ->
            Req = cowboy_req:set_resp_header(Header, z_convert:to_binary(Value), req(Context)),
            set_req(Req, Context);
        false ->
            throw({http_invalid_header, Header, V})
    end.

%% @doc Set multiple response headers.

-spec set_resp_headers(Headers, Context) -> Result when
	Headers :: [{binary(), binary()}], 
	Context :: context(),
	Result :: context().
set_resp_headers([], Context) ->
    Context;
set_resp_headers([{Header, Value}|Hs], Context) ->
    Context1 = set_resp_header(Header, Value, Context),
    set_resp_headers(Hs, Context1).

%% @doc Fetch the response header, `undefined' if not set.

-spec get_resp_header(Header, Context) -> Result when
	Header :: binary(),
	Context :: context(),
	Result :: undefined | binary().
get_resp_header(Header, Context) when is_binary(Header) ->
    Hs = maps:get(resp_headers, req(Context), #{}),
    maps:get(Header, Hs, undefined).

%% @doc Fetch all response headers.

-spec get_resp_headers(Context) -> Result when
	Context :: context(),
	Result :: map().
get_resp_headers(Context) ->
    maps:get(resp_headers, req(Context), #{}).

%% @doc Remove the response header from the list for response headers.

-spec remove_resp_header(Header, Context) -> Result when
	Header :: binary(),
	Context :: context(),
	Result :: context().
remove_resp_header(Header, Context) when is_binary(Header) ->
    Req = cowboy_req:delete_resp_header(Header, req(Context)),
    set_req(Req, Context).

%% @doc Add a cookie to the response cookies.

-spec set_resp_cookie(Key, Value, Options, Context) -> Result when
	Key :: binary(),
	Value :: binary(),
	Options :: list(),
	Context :: context(),
	Result :: context().
set_resp_cookie(Key, Value, Options, Context) when is_binary(Key), is_binary(Value) ->
    Options1 = [ {K,V} || {K,V} <- Options, V =/= undefined ],
    Req = cowboy_req:set_resp_cookie(Key, Value, req(Context), maps:from_list(Options1)),
    set_req(Req, Context).

%% @doc Fetch all response cookies.

-spec get_resp_cookies(Context) -> Result when
	Context :: context(),
	Result :: [{binary(), binary()}].
get_resp_cookies(Context) ->
    maps:to_list(maps:get(resp_cookies, req(Context))).

%% @doc Fetch the value of a cookie.

-spec get_cookie_value(Name, Context) -> Result when
	Name :: binary(),
	Context :: context(),
	Result :: undefined | binary().
get_cookie_value(Name, Context) when is_binary(Name) ->
    Cookies = maps:get(cowmachine_cookies, env(Context)),
    proplists:get_value(Name, Cookies).

%% @doc Fetch all cookies.

-spec req_cookie(Context) -> Result when
	Context :: context(),
	Result :: list().
req_cookie(Context) ->
    maps:get(cowmachine_cookies, env(Context)).

%% @doc Set the preliminary HTTP response code for the request. This can be changed.

-spec set_response_code(Code, Context) -> Result when
	Code :: integer(),
	Context :: context(),
	Result :: context().
set_response_code(Code, Context) when is_integer(Code) ->
    Env = env(Context),
    set_env(Env#{cowmachine_resp_code => Code}, Context).

%% @doc Fetch the preliminary HTTP response code for the request. This can be changed.

-spec response_code(Context) -> Result when
	Context :: context(),
	Result :: integer().
response_code(Context) ->
    maps:get(cowmachine_resp_code, env(Context)).

%% @doc Set the chosen charset.

-spec set_resp_chosen_charset(CharSet, Context) -> Result when
	CharSet :: undefined | binary(), 
	Context :: context(),
	Result :: context().
set_resp_chosen_charset(CharSet, Context) when is_binary(CharSet); CharSet =:= undefined ->
    Env = env(Context),
    set_env(Env#{cowmachine_resp_chosen_charset => CharSet}, Context).

%% @doc Get the chosen charset.

-spec resp_chosen_charset(Context) -> Result when
	Context :: context(),
	Result :: undefined | binary().
resp_chosen_charset(Context) ->
    maps:get(cowmachine_resp_chosen_charset, env(Context)).

%% @doc Set the transfer encoding.

-spec set_resp_transfer_encoding(Enc, Context) -> Result when
	Enc :: {binary(), function()},
	Context :: context(),
	Result :: context().
set_resp_transfer_encoding(Enc, Context) ->
    Env = env(Context),
    set_env(Env#{cowmachine_resp_transfer_encoding => Enc}, Context).

%% @doc Get the transfer encoding.

-spec resp_transfer_encoding(Context) -> Result when
	Context :: context(),
	Result :: undefined | {binary(), function()}.
resp_transfer_encoding(Context) ->
    maps:get(cowmachine_resp_transfer_encoding, env(Context)).

%% @doc Set the content encoding.

-spec set_resp_content_encoding(Enc, Context) -> Result when
	Enc :: binary(),
	Context :: context(),
	Result :: context().
set_resp_content_encoding(Enc, Context) when is_binary(Enc) ->
    Env = env(Context),
    set_env(Env#{cowmachine_resp_content_encoding => Enc}, Context).

%% @doc Get the content encoding.

-spec resp_content_encoding(Context) -> Result when
	Context :: context(),
	Result :: binary().
resp_content_encoding(Context) ->
    maps:get(cowmachine_resp_content_encoding, env(Context)).

%% @doc Encode the content according to the selected content encoding.

-spec encode_content(Content, Context) -> Result when
	Content :: iodata(),
	Context :: context(),
	Result :: iodata().
encode_content(Content, Context) ->
    encode_content_1(resp_content_encoding(Context), Content).

-spec encode_content_1(Enc, Content) -> Result when
	Enc :: binary(),
	Content :: iodata(),
	Result :: iodata().
encode_content_1(<<"gzip">>, Content) -> zlib:gzip(Content);
encode_content_1(<<"identity">>, Content) -> Content.


%% @doc Set the `redirect' flag, used during POST processing to check if a `303' should be returned.

-spec set_resp_redirect(Location, Context) -> Result when
	Location :: boolean() | binary(), 
	Context :: context(),
	Result :: context().
set_resp_redirect(Location, Context) when is_binary(Location) ->
    Env = env(Context),
    Context1 = set_resp_header(<<"location">>, Location, Context),
    set_env(Env#{ cowmachine_resp_redirect => true }, Context1);
set_resp_redirect(IsRedirect, Context) when is_boolean(IsRedirect) ->
    Env = env(Context),
    set_env(Env#{ cowmachine_resp_redirect => IsRedirect }, Context).

%% @doc Return the `redirect' flag, used during POST processing to check if a `303' should be returned.

-spec resp_redirect(Context) -> Result when
	Context :: context(),
	Result :: boolean().
resp_redirect(Context) ->
    maps:get(cowmachine_resp_redirect, env(Context)).

%% @doc Set the dispatch path of the request.

-spec set_disp_path(Path, Context) -> Result when
	Path :: binary(),
	Context :: context(),
	Result :: context().
set_disp_path(Path, Context) ->
    Env = env(Context),
    set_env(Env#{cowmachine_disp_path => Path}, Context).

%% @doc Return the dispatch path of the request.

-spec disp_path(Context) -> Result when
	Context :: context(),
	Result :: undefined | binary().
disp_path(Context) ->
    maps:get(cowmachine_disp_path, env(Context)).

%% @doc Set the response body, this must be converted to a response body that Cowboy can handle.

-spec set_resp_body(RespBody, Context) -> Result when
	RespBody :: resp_body(),
	Context :: context(),
	Result :: context().
set_resp_body(RespBody, Context) ->
    Env = env(Context),
    set_env(Env#{cowmachine_resp_body => RespBody}, Context).

%% @doc Return the response body, this must be converted to a response body that Cowboy can handle.

-spec resp_body(Context) -> Result when
	Context :: context(),
	Result :: resp_body().
resp_body(Context) ->
    maps:get(cowmachine_resp_body, env(Context)).

%% @doc Check if a response body has been set.

-spec has_resp_body(Context) -> Result when
	Context :: context(),
	Result :: boolean().
has_resp_body(Context) ->
    case maps:get(cowmachine_resp_body, env(Context)) of
        undefined -> false;
        [] -> false;
        <<>> -> false;
        _ -> true
    end.

%% @doc Check if the request has a body

-spec has_req_body(Context) -> Result when
	Context :: context(),
	Result :: boolean().
has_req_body(Context) ->
    cowboy_req:has_body(req(Context)).

%% @doc Fetch the request body as a single binary.<br/>
%% Per default we don't receive more than `~128K' bytes.
%% @equiv req_body(128*1024, Context)

-spec req_body(Context) -> Result when
	Context :: context(),
	Result :: {undefined | binary(), context()}.
req_body(Context) ->
    req_body(128*1024, Context).

-spec req_body(MaxLength, Context) -> Result when
	MaxLength :: non_neg_integer(),
	Context :: context(),
	Result :: {undefined | binary(), context()}.
req_body(MaxLength, Context) when MaxLength > 0 ->
    Req = req(Context),
    Opts = #{
        length => MaxLength,
        timeout => 10000
    },
    case cowboy_req:read_body(Req, Opts) of
        {ok, Body, Req2} ->
            {Body, set_req(Req2, Context)};
        {more, _Body, Req2} ->
            cowmachine:log(#{ level => warning,
                              at => ?AT,
                              text => lists:flatten(
                                        io_lib:format("Dropped request body, as it is larger than ~p bytes.",
                                                      [MaxLength]))
                              }, Req2),
            {undefined, set_req(Req2, Context)}
    end.

-spec stream_req_body(ChunkSize, Context) -> Result when
	ChunkSize :: non_neg_integer(), 
	Context :: context(),
	Result :: {ok | more, binary(), context()}.
stream_req_body(ChunkSize, Context) ->
    Opts = #{
        % length => 1024*1024*1024,
        length => ChunkSize,
        timeout => 10000
    },
    {Next, Chunk, Req1} = cowboy_req:read_body(req(Context), Opts),
    {Next, Chunk, set_req(Req1, Context)}.


-spec set_metadata(Key, Value, Context) -> Result when
	Key :: atom(), 
	Value :: term(),
	Context :: context(),
	Result :: context().
set_metadata(Key, Value, Context) ->
    Env = env(Context),
    Env1 = Env#{ {cowmachine, Key} => Value },
    set_env(Env1, Context).

-spec get_metadata(Key, Context) -> Result when
	Key :: atom(), 
	Context :: context(),
	Result :: undefined | term().
get_metadata('chosen-charset', Context) ->
    resp_chosen_charset(Context);
get_metadata('content-encoding', Context) ->
    resp_content_encoding(Context);
get_metadata('transfer-encoding', Context) ->
    resp_transfer_encoding(Context);
get_metadata('content-type', Context) ->
    resp_content_type(Context);
get_metadata(Key, Context) ->
    maps:get({cowmachine, Key}, env(Context), undefined).