0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add cowboy_req:read_and_match_urlencoded_body/2,3

This commit is contained in:
Loïc Hoguin 2018-09-07 13:56:12 +02:00
parent dcc6f9326f
commit 4b385749f2
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
9 changed files with 218 additions and 2 deletions

View file

@ -66,6 +66,7 @@ Request body:
* link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)] - Body length
* link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] - Read the request body
* link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)] - Read and parse a urlencoded request body
* link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)] - Read, parse and match a urlencoded request body against constraints
* link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)] - Read the next multipart headers
* link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] - Read the current part's body

View file

@ -0,0 +1,148 @@
= cowboy_req:read_and_match_urlencoded_body(3)
== Name
cowboy_req:read_and_match_urlencoded_body - Read, parse
and match a urlencoded request body against constraints
== Description
[source,erlang]
----
read_and_match_urlencoded_body(Fields, Req)
-> read_and_match_urlencoded_body(Fields, Req, #{})
read_and_match_urlencoded_body(Fields, Req, Opts)
-> {ok, Body, Req}
Fields :: cowboy:fields()
Req :: cowboy_req:req()
Opts :: cowboy_req:read_body_opts()
Body :: #{atom() => any()}
----
Read, parse and match a urlencoded request body against
constraints.
This function reads the request body and parses it as
`application/x-www-form-urlencoded`. It then applies
the given field constraints to the urlencoded data
and returns the result as a map.
The urlencoded media type is used by Web browsers when
submitting HTML forms using the POST method.
Cowboy will only return the values specified
in the fields list, and ignore all others. Fields can be
either the key requested; the key along with a list of
constraints; or the key, a list of constraints and a
default value in case the key is missing.
This function will crash if the key is missing and no
default value is provided. This function will also crash
if a constraint fails.
The key must be provided as an atom. The key of the
returned map will be that atom. The value may be converted
through the use of constraints, making this function able
to extract, validate and convert values all in one step.
Cowboy needs to read the full body before parsing. By default
it will read bodies of size up to 64KB. It is possible to
provide options to read larger bodies if required.
Cowboy will automatically handle protocol details including
the expect header, chunked transfer-encoding and others.
Once the body has been read, Cowboy sets the content-length
header if it was not previously provided.
This function can only be called once. Calling it again will
result in undefined behavior.
== Arguments
Fields::
Fields to retrieve from the urlencoded body.
+
See link:man:cowboy(3)[cowboy(3)] for a complete description.
Req::
The Req object.
Opts::
A map of body reading options. Please refer to
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)]
for details about each option.
+
This function defaults the `length` to 64KB and the `period`
to 5 seconds.
== Return value
An `ok` tuple is returned.
Desired values are returned as a map. The key is the atom
that was given in the list of fields, and the value is the
optionally converted value after applying constraints.
The map contains the same keys that were given in the fields.
An exception is triggered when the match fails.
The Req object returned in the tuple must be used from that point
onward. It contains a more up to date representation of the request.
For example it may have an added content-length header once the
body has been read.
== Changelog
* *2.5*: Function introduced.
== Examples
.Match fields
[source,erlang]
----
%% ID and Lang are binaries.
#{id := ID, lang := Lang}
= cowboy_req:read_and_match_urlencoded_body(
[id, lang], Req).
----
.Match fields and apply constraints
[source,erlang]
----
%% ID is an integer and Lang a non-empty binary.
#{id := ID, lang := Lang}
= cowboy_req:read_and_match_urlencoded_body(
[{id, int}, {lang, nonempty}], Req).
----
.Match fields with default values
[source,erlang]
----
#{lang := Lang}
= cowboy_req:read_and_match_urlencoded_body(
[{lang, [], <<"en-US">>}], Req).
----
.Allow large urlencoded bodies
[source,erlang]
----
{ok, Body, Req} = cowboy_req:read_and_match_urlencoded_body(
Fields, Req0, #{length => 1000000}).
----
== See also
link:man:cowboy_req(3)[cowboy_req(3)],
link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)],
link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)],
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)],
link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)],
link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)],
link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)]

View file

@ -112,5 +112,6 @@ link:man:cowboy_req(3)[cowboy_req(3)],
link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)],
link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)],
link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)],
link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)],
link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)],
link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)]

View file

@ -131,4 +131,5 @@ link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)],
link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)],
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)],
link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)],
link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)],
link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)]

View file

@ -97,4 +97,5 @@ link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)],
link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)],
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)],
link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)],
link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)],
link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)]

View file

@ -90,5 +90,6 @@ link:man:cowboy_req(3)[cowboy_req(3)],
link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)],
link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)],
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)],
link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)],
link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)],
link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)]

View file

@ -54,7 +54,8 @@
-export([read_body/2]).
-export([read_urlencoded_body/1]).
-export([read_urlencoded_body/2]).
%% @todo read_and_match_urlencoded_body?
-export([read_and_match_urlencoded_body/2]).
-export([read_and_match_urlencoded_body/3]).
%% Multipart.
-export([read_part/1]).
@ -513,6 +514,23 @@ read_urlencoded_body(Req0, Opts) ->
end
end.
-spec read_and_match_urlencoded_body(cowboy:fields(), Req)
-> {ok, map(), Req} when Req::req().
read_and_match_urlencoded_body(Fields, Req) ->
read_and_match_urlencoded_body(Fields, Req, #{length => 64000, period => 5000}).
-spec read_and_match_urlencoded_body(cowboy:fields(), Req, read_body_opts())
-> {ok, map(), Req} when Req::req().
read_and_match_urlencoded_body(Fields, Req0, Opts) ->
{ok, Qs, Req} = read_urlencoded_body(Req0, Opts),
case filter(Fields, kvlist_to_map(Fields, Qs)) of
{ok, Map} ->
{ok, Map, Req};
{error, Errors} ->
exit({request_error, {read_and_match_urlencoded_body, Errors},
'Urlencoded request body validation constraints failed for the reasons provided.'})
end.
%% Multipart.
-spec read_part(Req)

View file

@ -46,6 +46,19 @@ echo(<<"read_urlencoded_body">>, Req0, Opts) ->
_ -> cowboy_req:read_urlencoded_body(Req0)
end,
{ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts};
echo(<<"read_and_match_urlencoded_body">>, Req0, Opts) ->
Path = cowboy_req:path(Req0),
case {Path, Opts} of
{<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2);
{_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2);
_ -> ok
end,
{ok, Body, Req} = case Path of
<<"/opts", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts);
<<"/crash", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts);
_ -> cowboy_req:read_and_match_urlencoded_body([], Req0)
end,
{ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts};
echo(<<"uri">>, Req, Opts) ->
Value = case cowboy_req:path_info(Req) of
[<<"origin">>] -> cowboy_req:uri(Req, #{host => undefined});
@ -61,7 +74,12 @@ echo(<<"match">>, Req, Opts) ->
Fields = [binary_to_atom(F, latin1) || F <- Fields0],
Value = case Type of
<<"qs">> -> cowboy_req:match_qs(Fields, Req);
<<"cookies">> -> cowboy_req:match_cookies(Fields, Req)
<<"cookies">> -> cowboy_req:match_cookies(Fields, Req);
<<"body_qs">> ->
%% Note that the Req should not be discarded but for the
%% purpose of this test this has no ill impacts.
{ok, Match, _} = cowboy_req:read_and_match_urlencoded_body(Fields, Req),
Match
end,
{ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts};
echo(What, Req, Opts) ->

View file

@ -564,6 +564,33 @@ do_read_urlencoded_body_too_long(Path, Body, Config) ->
end,
gun:close(ConnPid).
read_and_match_urlencoded_body(Config) ->
doc("Read and match an application/x-www-form-urlencoded request body."),
<<"#{}">> = do_body("POST", "/match/body_qs", [], "a=b&c=d", Config),
<<"#{a => <<\"b\">>}">> = do_body("POST", "/match/body_qs/a", [], "a=b&c=d", Config),
<<"#{c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/c", [], "a=b&c=d", Config),
<<"#{a => <<\"b\">>,c => <<\"d\">>}">>
= do_body("POST", "/match/body_qs/a/c", [], "a=b&c=d", Config),
<<"#{a => <<\"b\">>,c => true}">> = do_body("POST", "/match/body_qs/a/c", [], "a=b&c", Config),
<<"#{a => true,c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/a/c", [], "a&c=d", Config),
%% Ensure match errors result in a 400 response.
{400, _} = do_body_error("POST", "/match/body_qs/a/c", [], "a=b", Config),
%% Ensure parse errors result in a 400 response.
{400, _} = do_body_error("POST", "/match/body_qs", [], "%%%%%", Config),
%% Send a 10MB body, larger than the default length, to ensure a crash occurs.
ok = do_read_urlencoded_body_too_large(
"/no-opts/read_and_match_urlencoded_body",
string:chars($a, 10000000), Config),
%% We read any length for at most 1 second.
%%
%% The body is sent twice, first with nofin, then wait 1.1 second, then again with fin.
%% We expect the handler to crash because read_and_match_urlencoded_body expects the full body.
ok = do_read_urlencoded_body_too_long(
"/crash/read_and_match_urlencoded_body/period", <<"abc">>, Config),
%% The timeout value is set too low on purpose to ensure a crash occurs.
ok = do_read_body_timeout("/opts/read_and_match_urlencoded_body/timeout", <<"abc">>, Config),
ok.
multipart(Config) ->
doc("Multipart request body."),
do_multipart("/multipart", Config).