From e8a1868033a75fa1f684a8b91ff7aa25e6c67676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 11 Feb 2025 12:00:03 +0100 Subject: [PATCH] Add support for lists in cowboy_req:set_resp_headers 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. --- .../manual/cowboy_req.set_resp_headers.asciidoc | 15 ++++++++++++--- src/cowboy_req.erl | 17 ++++++++++++++++- test/handlers/resp_h.erl | 13 +++++++++++++ test/req_SUITE.erl | 10 +++++++--- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/doc/src/manual/cowboy_req.set_resp_headers.asciidoc b/doc/src/manual/cowboy_req.set_resp_headers.asciidoc index 63fe42416..9ca590140 100644 --- a/doc/src/manual/cowboy_req.set_resp_headers.asciidoc +++ b/doc/src/manual/cowboy_req.set_resp_headers.asciidoc @@ -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. @@ -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:: @@ -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 diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 8d8cf8295..adee364b9 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -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.'}); @@ -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). diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl index 6e9b5f75d..61f714f49 100644 --- a/test/handlers/resp_h.erl +++ b/test/handlers/resp_h.erl @@ -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">>, diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index f6208a202..0ea1f96db 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -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) ->