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

Document body reading in auto mode

It is now tested both via cowboy_req:read_body and
via cowboy_req:cast.

Removes a bad example from the guide of body reading
with period of infinity, which does not work.
This commit is contained in:
Loïc Hoguin 2024-01-08 15:13:18 +01:00
parent c1490d7d55
commit e4a78aaeb1
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
8 changed files with 137 additions and 21 deletions

View file

@ -74,18 +74,34 @@ only up to 1MB for up to 5 seconds:
#{length => 1000000, period => 5000}). #{length => 1000000, period => 5000}).
---- ----
You may also disable the length limit:
[source,erlang]
{ok, Data, Req} = cowboy_req:read_body(Req0, #{length => infinity}).
This makes the function wait 15 seconds and return with
whatever arrived during that period. This is not
recommended for public facing applications.
These two options can effectively be used to control These two options can effectively be used to control
the rate of transmission of the request body. the rate of transmission of the request body.
It is also possible to asynchronously read the request
body using auto mode:
[source,erlang]
----
Ref = make_ref(),
cowboy_req:cast({read_body, self(), Ref, auto, infinity}, Req).
----
Cowboy will wait indefinitely for data and then send a
`request_body` message as soon as it has data available,
regardless of length.
[source,erlang]
----
receive
{request_body, Ref, nofin, Data} ->
do_something(Data);
{request_body, Ref, fin, _BodyLen, Data} ->
do_something(Data)
end.
----
Asynchronous reading of data pairs well with loop handlers.
=== Streaming the body === Streaming the body
When the body is too large, the first call will return When the body is too large, the first call will return

View file

@ -120,8 +120,8 @@ request's URI.
[source,erlang] [source,erlang]
---- ----
read_body_opts() :: #{ read_body_opts() :: #{
length => non_neg_integer(), length => non_neg_integer() | auto,
period => non_neg_integer(), period => non_neg_integer() | infinity,
timeout => timeout() timeout => timeout()
} }
---- ----
@ -130,6 +130,10 @@ Body reading options.
The defaults are function-specific. The defaults are function-specific.
Auto mode can be enabled by setting `length` to `auto`
and `period` to `infinity`. The period cannot be set
to `infinity` when auto mode isn't used.
=== req() === req()
[source,erlang] [source,erlang]

View file

@ -36,6 +36,22 @@ The atom `ok` is always returned. It can be safely ignored.
== Examples == Examples
.Read the body using auto mode
[source,erlang]
----
read_body_auto_async(Req) ->
read_body_auto_async(Req, make_ref(), <<>>).
read_body_auto_async(Req, Ref, Acc) ->
cowboy_req:cast({read_body, self(), Ref, auto, infinity}, Req),
receive
{request_body, Ref, nofin, Data} ->
read_body_auto_async(Req, Ref, <<Acc/binary, Data/binary>>);
{request_body, Ref, fin, _BodyLen, Data} ->
{ok, <<Acc/binary, Data/binary>>, Req}
end.
----
.Increase the HTTP/1.1 idle timeout .Increase the HTTP/1.1 idle timeout
[source,erlang] [source,erlang]
---- ----

View file

@ -68,6 +68,13 @@ The `timeout` option is a safeguard in case the connection
process becomes unresponsive. The function will crash if no process becomes unresponsive. The function will crash if no
message was received in that interval. The timeout should be message was received in that interval. The timeout should be
larger than the period. It defaults to the period + 1 second. larger than the period. It defaults to the period + 1 second.
+
Auto mode can be enabled by setting the `length` to `auto` and
the `period` to `infinity`. When auto mode is used, Cowboy will
send data to the handler as soon as it receives it, regardless
of its size. It will wait indefinitely until data is available.
Auto mode's main purpose is asynchronous body reading using
link:man:cowboy_req:cast(3)[cowboy_req:cast(3)].
== Return value == Return value
@ -86,6 +93,9 @@ body has been read.
== Changelog == Changelog
* *2.11*: The `length` option now accepts `auto` and the
period now accepts `infinity`. This adds support for
reading the body in auto mode.
* *2.0*: Function introduced. Replaces `body/1,2`. * *2.0*: Function introduced. Replaces `body/1,2`.
== Examples == Examples

View file

@ -45,8 +45,49 @@ The default stream handler spawns the request process
and receives its exit signal when it terminates. It and receives its exit signal when it terminates. It
will stop the stream once its receives it. will stop the stream once its receives it.
// @todo It also implements the read_body mechanism. Because this stream handler converts events from the
// Note that cowboy_stream_h sends the 100-continue automatically. request process into commands, other stream handlers
may not work properly if they are executed after the
default stream handler. Always be mindful of in which
order stream handlers will get executed.
=== Request body
The default stream handler implements the `read_body`
mechanism. In addition to reading the body, the handler
will automatically handle the `expect: 100-continue`
header and send a 100 Continue response.
Normally one would use
link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)]
to read the request body. The default stream handler
will buffer data until the amount gets larger than the
requested length before sending it. Alternatively, it
will send whatever data it has when the period timeout
triggers. Depending on the protocol, the flow control
window is updated to allow receiving data for the
requested length.
The default stream handler also comes with an automatic
mode for reading the request body. This can be used by
sending the event message `{read_body, Pid, Ref, auto, infinity}`
using link:man:cowboy_req:cast(3)[cowboy_req:cast(3)].
The default stream handler will then send data as soon
as some becomes available using one of these two
messages depending on whether body reading was completed:
* `{request_body, Ref, nofin, Data}`
* `{request_body, Ref, fin, BodyLen, Data}`
Depending on the protocol, Cowboy will update the flow
control window using the size of the data that was read.
Auto mode automatically gets disabled after data has
been sent to the handler. Therefore in order to continue
reading data a `read_body` event message must be sent
after each `request_body` message.
=== Response
In addition it returns a command for any event message In addition it returns a command for any event message
looking like one of the following commands: `inform`, looking like one of the following commands: `inform`,
@ -54,14 +95,9 @@ looking like one of the following commands: `inform`,
`switch_protocol`. This is what allows the request `switch_protocol`. This is what allows the request
process to send a response. process to send a response.
// @todo Add set_options, which updates options dynamically.
Because this stream handler converts events from the
request process into commands, other stream handlers
may not work properly if they are executed
== Changelog == Changelog
* *2.11*: Introduce body reading using auto mode.
* *2.0*: Module introduced. * *2.0*: Module introduced.
== See also == See also
@ -71,4 +107,5 @@ link:man:cowboy_stream(3)[cowboy_stream(3)],
link:man:cowboy_compress_h(3)[cowboy_compress_h(3)], link:man:cowboy_compress_h(3)[cowboy_compress_h(3)],
link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)], link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)],
link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)], link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)],
link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)] link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)],
link:man:cowboy_req:cast(3)[cowboy_req:cast(3)]

View file

@ -521,7 +521,11 @@ read_body(Req=#{has_read_body := true}, _) ->
read_body(Req, Opts) -> read_body(Req, Opts) ->
Length = maps:get(length, Opts, 8000000), Length = maps:get(length, Opts, 8000000),
Period = maps:get(period, Opts, 15000), Period = maps:get(period, Opts, 15000),
Timeout = maps:get(timeout, Opts, Period + 1000), DefaultTimeout = case Period of
infinity -> infinity; %% infinity + 1000 = infinity.
_ -> Period + 1000
end,
Timeout = maps:get(timeout, Opts, DefaultTimeout),
Ref = make_ref(), Ref = make_ref(),
cast({read_body, self(), Ref, Length, Period}, Req), cast({read_body, self(), Ref, Length, Period}, Req),
receive receive

View file

@ -25,6 +25,8 @@ echo(<<"read_body">>, Req0, Opts) ->
timer:sleep(500), timer:sleep(500),
cowboy_req:read_body(Req0); cowboy_req:read_body(Req0);
<<"/full", _/bits>> -> read_body(Req0, <<>>); <<"/full", _/bits>> -> read_body(Req0, <<>>);
<<"/auto-sync", _/bits>> -> read_body_auto_sync(Req0, <<>>);
<<"/auto-async", _/bits>> -> read_body_auto_async(Req0, <<>>);
<<"/length", _/bits>> -> <<"/length", _/bits>> ->
{_, _, Req1} = read_body(Req0, <<>>), {_, _, Req1} = read_body(Req0, <<>>),
Length = cowboy_req:body_length(Req1), Length = cowboy_req:body_length(Req1),
@ -122,6 +124,25 @@ read_body(Req0, Acc) ->
{more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>) {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
end. end.
read_body_auto_sync(Req0, Acc) ->
Opts = #{length => auto, period => infinity},
case cowboy_req:read_body(Req0, Opts) of
{ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
{more, Data, Req} -> read_body_auto_sync(Req, << Acc/binary, Data/binary >>)
end.
read_body_auto_async(Req, Acc) ->
read_body_auto_async(Req, make_ref(), Acc).
read_body_auto_async(Req, ReadBodyRef, Acc) ->
cowboy_req:cast({read_body, self(), ReadBodyRef, auto, infinity}, Req),
receive
{request_body, ReadBodyRef, nofin, Data} ->
read_body_auto_async(Req, ReadBodyRef, <<Acc/binary, Data/binary>>);
{request_body, ReadBodyRef, fin, _, Data} ->
{ok, <<Acc/binary, Data/binary>>, Req}
end.
value_to_iodata(V) when is_integer(V) -> integer_to_binary(V); value_to_iodata(V) when is_integer(V) -> integer_to_binary(V);
value_to_iodata(V) when is_atom(V) -> atom_to_binary(V, latin1); value_to_iodata(V) when is_atom(V) -> atom_to_binary(V, latin1);
value_to_iodata(V) when is_list(V); is_tuple(V); is_map(V) -> io_lib:format("~999999p", [V]); value_to_iodata(V) when is_list(V); is_tuple(V); is_map(V) -> io_lib:format("~999999p", [V]);

View file

@ -64,6 +64,8 @@ init_dispatch(Config) ->
{"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}}, {"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}},
{"/100-continue/:key", echo_h, []}, {"/100-continue/:key", echo_h, []},
{"/full/:key", echo_h, []}, {"/full/:key", echo_h, []},
{"/auto-sync/:key", echo_h, []},
{"/auto-async/:key", echo_h, []},
{"/spawn/:key", echo_h, []}, {"/spawn/:key", echo_h, []},
{"/no/:key", echo_h, []}, {"/no/:key", echo_h, []},
{"/direct/:key/[...]", echo_h, []}, {"/direct/:key/[...]", echo_h, []},
@ -524,6 +526,12 @@ do_read_body_timeout(Path, Body, Config) ->
{response, _, 500, _} = gun:await(ConnPid, Ref, infinity), {response, _, 500, _} = gun:await(ConnPid, Ref, infinity),
gun:close(ConnPid). gun:close(ConnPid).
read_body_auto(Config) ->
doc("Read the request body using auto mode."),
<<0:80000000>> = do_body("POST", "/auto-sync/read_body", [], <<0:80000000>>, Config),
<<0:80000000>> = do_body("POST", "/auto-async/read_body", [], <<0:80000000>>, Config),
ok.
read_body_spawn(Config) -> read_body_spawn(Config) ->
doc("Confirm we can use cowboy_req:read_body/1,2 from another process."), doc("Confirm we can use cowboy_req:read_body/1,2 from another process."),
<<"hello world!">> = do_body("POST", "/spawn/read_body", [], "hello world!", Config), <<"hello world!">> = do_body("POST", "/spawn/read_body", [], "hello world!", Config),