Skip to content

Commit

Permalink
Add support for lists in cowboy_req:set_resp_headers
Browse files Browse the repository at this point in the history
This is meant to be used with clients such as Gun to simplify
proxying and similar operations. The set-cookie header must
not be set this way so there is still some extra processing
to be done to fully translate a Gun response into a Cowboy
response.
  • Loading branch information
essen committed Feb 11, 2025
1 parent f316a65 commit e8a1868
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 7 deletions.
15 changes: 12 additions & 3 deletions doc/src/manual/cowboy_req.set_resp_headers.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ cowboy_req:set_resp_headers - Set several response headers
set_resp_headers(Headers, Req :: cowboy_req:req())
-> Req
Headers :: cowboy:http_headers()
Headers :: cowboy:http_headers() | [{binary(), iodata()}]
----

Set several headers to be sent with the response.
Expand All @@ -32,8 +32,16 @@ instead of this function to set cookies.

Headers::

Headers as a map with keys being lowercase binary strings,
and values as binary strings.
Headers as a map with names being lowercase binary strings,
and values as iodata; or as a list with the same requirements
for names and values.
+
When a list is given it is converted to its equivalent map,
with duplicate headers concatenated with a comma inserted
in-between. Support for lists is meant to simplify using
data from clients or other applications.
+
The set-cookie header must not be set using this function.

Req::

Expand All @@ -48,6 +56,7 @@ otherwise the headers will not be sent in the response.

== Changelog

* *2.13*: The function now accepts a list of headers.
* *2.0*: Function introduced.

== Examples
Expand Down
17 changes: 16 additions & 1 deletion src/cowboy_req.erl
Original file line number Diff line number Diff line change
Expand Up @@ -726,8 +726,10 @@ set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) ->
set_resp_header(Name,Value, Req) ->
Req#{resp_headers => #{Name => Value}}.

-spec set_resp_headers(cowboy:http_headers(), Req)
-spec set_resp_headers(cowboy:http_headers() | [{binary(), iodata()}], Req)
-> Req when Req::req().
set_resp_headers(Headers, Req) when is_list(Headers) ->
set_resp_headers_list(Headers, Req, #{});
set_resp_headers(#{<<"set-cookie">> := _}, _) ->
exit({response_error, invalid_header,
'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
Expand All @@ -736,6 +738,19 @@ set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) ->
set_resp_headers(Headers, Req) ->
Req#{resp_headers => Headers}.

set_resp_headers_list([], Req, Acc) ->
set_resp_headers(Acc, Req);
set_resp_headers_list([{<<"set-cookie">>, _}|_], _, _) ->
exit({response_error, invalid_header,
'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
set_resp_headers_list([{Name, Value}|Tail], Req, Acc) ->
case Acc of
#{Name := ValueAcc} ->
set_resp_headers_list(Tail, Req, Acc#{Name => [ValueAcc, <<", ">>, Value]});
_ ->
set_resp_headers_list(Tail, Req, Acc#{Name => Value})
end.

-spec resp_header(binary(), req()) -> binary() | undefined.
resp_header(Name, Req) ->
resp_header(Name, Req, undefined).
Expand Down
13 changes: 13 additions & 0 deletions test/handlers/resp_h.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,25 @@ do(<<"set_resp_headers">>, Req0, Opts) ->
<<"content-encoding">> => <<"compress">>
}, Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_list">>, Req0, Opts) ->
Req = cowboy_req:set_resp_headers([
{<<"content-type">>, <<"text/plain">>},
{<<"content-encoding">>, <<"compress">>}
], Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_cookie">>, Req0, Opts) ->
ct_helper:ignore(cowboy_req, set_resp_headers, 2),
Req = cowboy_req:set_resp_headers(#{
<<"set-cookie">> => <<"name=value">>
}, Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_list_cookie">>, Req0, Opts) ->
ct_helper:ignore(cowboy_req, set_resp_headers_list, 3),
Req = cowboy_req:set_resp_headers([
{<<"set-cookie">>, <<"name=value">>},
{<<"set-cookie">>, <<"name2=value2">>}
], Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_http11">>, Req0, Opts) ->
Req = cowboy_req:set_resp_headers(#{
<<"connection">> => <<"custom-header, close">>,
Expand Down
10 changes: 7 additions & 3 deletions test/req_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -858,11 +858,15 @@ set_resp_header(Config) ->

set_resp_headers(Config) ->
doc("Response using set_resp_headers."),
{200, Headers, <<"OK">>} = do_get("/resp/set_resp_headers", Config),
true = lists:keymember(<<"content-type">>, 1, Headers),
true = lists:keymember(<<"content-encoding">>, 1, Headers),
{200, Headers1, <<"OK">>} = do_get("/resp/set_resp_headers", Config),
true = lists:keymember(<<"content-type">>, 1, Headers1),
true = lists:keymember(<<"content-encoding">>, 1, Headers1),
{200, Headers2, <<"OK">>} = do_get("/resp/set_resp_headers_list", Config),
true = lists:keymember(<<"content-type">>, 1, Headers2),
true = lists:keymember(<<"content-encoding">>, 1, Headers2),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_list_cookie", Config)),
ok.

resp_header(Config) ->
Expand Down

0 comments on commit e8a1868

Please sign in to comment.