Skip to content

Commit 09c1a24

Browse files
committed
Use a single header for Access-Control-Expose-Headers
This fixes the case where more than one header is exposed. Previously, only one header would be listed due to the way cowboy handles multiple response headers with the same name. If we had more control over the reply, we could perhaps add multiple headers (due to how cowboy_req:reply/4 works). However, this approach is much simpler and appears to be more consistent with its use in the wild.
1 parent 428293d commit 09c1a24

File tree

2 files changed

+29
-3
lines changed

2 files changed

+29
-3
lines changed

src/cowboy_cors.erl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ exposed_headers(Req, State) ->
139139

140140
set_exposed_headers(Req, []) ->
141141
Req;
142-
set_exposed_headers(Req, [Header|Tail]) ->
143-
Req1 = cowboy_req:set_resp_header(<<"access-control-expose-headers">>, Header, Req),
144-
set_exposed_headers(Req1, Tail).
142+
set_exposed_headers(Req, Headers) ->
143+
Bin = header_list(Headers),
144+
cowboy_req:set_resp_header(<<"access-control-expose-headers">>, Bin, Req).
145145

146146
%% allow_credentials/1 should return true or false.
147147
allow_credentials(Req, State) ->
@@ -195,3 +195,12 @@ terminate(Req, #state{env = Env}) ->
195195
-spec error_terminate(cowboy_req:req(), #state{}) -> no_return().
196196
error_terminate(_Req, _State) ->
197197
erlang:throw({?MODULE, error}).
198+
199+
%% create a comma-separated list for a header value
200+
header_list(Values) ->
201+
header_list(Values, <<>>).
202+
203+
header_list([Value], Acc) ->
204+
<<Acc/binary, Value/binary>>;
205+
header_list([Value | Rest], Acc) ->
206+
header_list(Rest, <<Acc/binary, Value/binary, ",">>).

test/cors_SUITE.erl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
-export([standard_options/1]).
1818
-export([simple_allowed_get/1]).
1919
-export([simple_allowed_credentials_get/1]).
20+
-export([simple_exposed_headers/1]).
2021
-export([actual_options/1]).
2122
-export([preflight_method/1]).
2223
-export([preflight_allowed_method/1]).
@@ -46,6 +47,7 @@ groups() ->
4647
standard_options,
4748
simple_allowed_get,
4849
simple_allowed_credentials_get,
50+
simple_exposed_headers,
4951
actual_options,
5052
preflight_method,
5153
preflight_allowed_method,
@@ -168,6 +170,21 @@ simple_allowed_credentials_get(Config) ->
168170
{_, Origin} = lists:keyfind(<<"access-control-allow-origin">>, 1, Headers),
169171
{_, <<"true">>} = lists:keyfind(<<"access-control-allow-credentials">>, 1, Headers).
170172

173+
simple_exposed_headers(Config) ->
174+
Origin = <<"http://example.com">>,
175+
Exposed = [<<"x-first">>, <<"x-second">>],
176+
{ok, 204, Headers, _} =
177+
request(<<"GET">>,
178+
[{<<"Origin">>, Origin}],
179+
[{allowed_origins, Origin},
180+
{allowed_methods, <<"GET">>},
181+
{exposed_headers, Exposed}],
182+
Config),
183+
{_, Origin} = lists:keyfind(<<"access-control-allow-origin">>, 1, Headers),
184+
{_, ExposedList} = lists:keyfind(<<"access-control-expose-headers">>, 1, Headers),
185+
Exposed = cowboy_http:nonempty_list(ExposedList, fun cowboy_http:token/2),
186+
false = lists:keyfind(<<"access-control-allow-credentials">>, 1, Headers).
187+
171188
actual_options(Config) ->
172189
%% OPTIONS request without Access-Control-Request-Method is not a pre-flight request.
173190
Origin = <<"http://example.com">>,

0 commit comments

Comments
 (0)