aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/manual/cowboy_req.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc148
-rw-r--r--doc/src/manual/cowboy_req.read_body.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.read_part.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.read_part_body.asciidoc1
-rw-r--r--doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc1
-rw-r--r--src/cowboy_req.erl20
-rw-r--r--test/handlers/echo_h.erl20
-rw-r--r--test/req_SUITE.erl27
9 files changed, 218 insertions, 2 deletions
diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc
index 309aedf..2758e8f 100644
--- a/doc/src/manual/cowboy_req.asciidoc
+++ b/doc/src/manual/cowboy_req.asciidoc
@@ -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
diff --git a/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc b/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc
new file mode 100644
index 0000000..aa2704c
--- /dev/null
+++ b/doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc
@@ -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)]
diff --git a/doc/src/manual/cowboy_req.read_body.asciidoc b/doc/src/manual/cowboy_req.read_body.asciidoc
index 729b2f0..2b87405 100644
--- a/doc/src/manual/cowboy_req.read_body.asciidoc
+++ b/doc/src/manual/cowboy_req.read_body.asciidoc
@@ -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)]
diff --git a/doc/src/manual/cowboy_req.read_part.asciidoc b/doc/src/manual/cowboy_req.read_part.asciidoc
index ceed72a..9123b2e 100644
--- a/doc/src/manual/cowboy_req.read_part.asciidoc
+++ b/doc/src/manual/cowboy_req.read_part.asciidoc
@@ -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)]
diff --git a/doc/src/manual/cowboy_req.read_part_body.asciidoc b/doc/src/manual/cowboy_req.read_part_body.asciidoc
index 3a1af2f..dc634f3 100644
--- a/doc/src/manual/cowboy_req.read_part_body.asciidoc
+++ b/doc/src/manual/cowboy_req.read_part_body.asciidoc
@@ -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)]
diff --git a/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc b/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc
index 3b65fcf..e897a69 100644
--- a/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc
+++ b/doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc
@@ -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)]
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index ee21e98..ee40304 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -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)
diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl
index a116442..ec37a66 100644
--- a/test/handlers/echo_h.erl
+++ b/test/handlers/echo_h.erl
@@ -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) ->
diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl
index 9042f54..6ca4521 100644
--- a/test/req_SUITE.erl
+++ b/test/req_SUITE.erl
@@ -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).