Skip to content

Commit eca5fbb

Browse files
authored
Merge pull request #742 from bszaf/async_redirect_fix
Handle chunked responses in the context of redirect requests
2 parents 7ae3831 + 5c3846e commit eca5fbb

File tree

2 files changed

+159
-42
lines changed

2 files changed

+159
-42
lines changed

src/hackney_stream.erl

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -166,45 +166,70 @@ maybe_continue(Parent, Owner, Ref, #client{transport=Transport,
166166
%% - {redirect, To, Headers}
167167
%% - {see_other, To, Headers} for status 303 and POST requests.
168168
maybe_redirect(Parent, Owner, Ref, StatusInt, Reason,
169-
#client{transport=Transport,
170-
socket=Socket,
171-
method=Method,
172-
follow_redirect=true}=Client) ->
169+
#client{method=Method, follow_redirect=true}=Client) ->
173170
case lists:member(StatusInt, [301, 302, 307, 308]) of
174171
true ->
175-
Transport:setopts(Socket, [{active, false}]),
176-
case parse(Client) of
177-
{loop, Client2} ->
178-
maybe_redirect(Parent, Owner, Ref, StatusInt,
179-
Reason, Client2);
180-
{ok, {headers, Headers}, Client2} ->
181-
Location = hackney:redirect_location(Headers),
182-
case {Location, lists:member(Method, [get, head])} of
183-
{undefined, _} ->
172+
maybe_redirect_1(Parent, Owner, Ref, Reason, Client);
173+
false when StatusInt =:= 303, Method =:= <<"POST">> ->
174+
maybe_redirect_2(Parent, Owner, Ref, Reason, Client);
175+
_ ->
176+
Owner ! {hackney_response, Ref, {status, StatusInt, Reason}},
177+
maybe_continue(Parent, Owner, Ref, Client)
178+
end;
179+
maybe_redirect(Parent, Owner, Ref, StatusInt, Reason, Client) ->
180+
Owner ! {hackney_response, Ref, {status, StatusInt, Reason}},
181+
maybe_continue(Parent, Owner, Ref, Client).
182+
183+
%% The first case for redirections are status codes 301, 302, 307 and 308.
184+
%% In this case {redirect, Location, Headers} is sent to the owner if
185+
%% the request is valid.
186+
maybe_redirect_1(Parent, Owner, Ref, Reason,
187+
#client{transport=Transport, socket=Socket}=Client) ->
188+
Transport:setopts(Socket, [{active, false}]),
189+
case parse(Client) of
190+
{loop, Client2} ->
191+
maybe_redirect_1(Parent, Owner, Ref, Reason, Client2);
192+
{more, Client2, Rest} ->
193+
Continuation = fun(Client3) ->
194+
maybe_redirect_1(Parent, Owner, Ref, Reason, Client3)
195+
end,
196+
async_recv(Parent, Owner, Ref, Client2, Rest, Continuation);
197+
{ok, {headers, Headers}, Client2} ->
198+
Location = hackney:redirect_location(Headers),
199+
case Location of
200+
undefined ->
201+
Owner ! {hackney_response, Ref, {error,invalid_redirection}},
202+
hackney_manager:handle_error(Client2);
203+
_ ->
204+
case hackney_response:skip_body(Client2) of
205+
{skip, Client3} ->
206+
hackney_manager:store_state(Client3),
184207
Owner ! {hackney_response, Ref,
185-
{error,invalid_redirection}},
186-
hackney_manager:handle_error(Client2);
187-
{_, _} ->
188-
case hackney_response:skip_body(Client2) of
189-
{skip, Client3} ->
190-
hackney_manager:store_state(Client3),
191-
Owner ! {hackney_response, Ref,
192-
{redirect, Location, Headers}};
193-
Error ->
194-
Owner ! {hackney_response, Ref, Error},
195-
hackney_manager:handle_error(Client2)
208+
{redirect, Location, Headers}};
209+
Error ->
210+
Owner ! {hackney_response, Ref, Error},
211+
hackney_manager:handle_error(Client2)
196212
end
197213
end;
198-
{error, Error} ->
199-
Owner ! {hackney_response, Ref, {error, Error}},
200-
hackney_manager:handle_error(Client)
201-
end;
202-
false when StatusInt =:= 303, Method =:= post ->
214+
{error, Error} ->
215+
Owner ! {hackney_response, Ref, {error, Error}},
216+
hackney_manager:handle_error(Client)
217+
end.
218+
219+
%% The second case is for status code 303 and POST requests.
220+
%% This results in sending {see_other, Location, Headers} to the owner.
221+
maybe_redirect_2(Parent, Owner, Ref, Reason,
222+
#client{transport=Transport,
223+
socket=Socket}=Client) ->
203224
Transport:setopts(Socket, [{active, false}]),
204225
case parse(Client) of
205226
{loop, Client2} ->
206-
maybe_redirect(Parent, Owner, Ref, StatusInt,
207-
Reason, Client2);
227+
maybe_redirect_2(Parent, Owner, Ref, Reason, Client2);
228+
{more, Client2, Rest} ->
229+
Continuation = fun(Client3) ->
230+
maybe_redirect_2(Parent, Owner, Ref, Reason, Client3)
231+
end,
232+
async_recv(Parent, Owner, Ref, Client2, Rest, Continuation);
208233
{ok, {headers, Headers}, Client2} ->
209234
case hackney:redirect_location(Headers) of
210235
undefined ->
@@ -225,21 +250,19 @@ maybe_redirect(Parent, Owner, Ref, StatusInt, Reason,
225250
{error, Error} ->
226251
Owner ! {hackney_response, Ref, {error, Error}},
227252
hackney_manager:handle_error(Client)
228-
end;
229-
_ ->
230-
Owner ! {hackney_response, Ref, {status, StatusInt, Reason}},
231-
maybe_continue(Parent, Owner, Ref, Client)
232-
end;
233-
maybe_redirect(Parent, Owner, Ref, StatusInt, Reason, Client) ->
234-
Owner ! {hackney_response, Ref, {status, StatusInt, Reason}},
235-
maybe_continue(Parent, Owner, Ref, Client).
253+
end.
236254

237255

256+
async_recv(Parent, Owner, Ref, Client, Buffer) ->
257+
Continuation = fun(Client2) ->
258+
stream_loop(Parent, Owner, Ref, Client2)
259+
end,
260+
async_recv(Parent, Owner, Ref, Client, Buffer, Continuation).
261+
238262
async_recv(Parent, Owner, Ref,
239263
#client{transport=Transport,
240264
socket=TSock,
241-
recv_timeout=Timeout}=Client, Buffer) ->
242-
265+
recv_timeout=Timeout}=Client, Buffer, Continuation) ->
243266
{OK, Closed, Error} = Transport:messages(TSock),
244267
Sock = raw_sock(TSock),
245268
Transport:setopts(TSock, [{active, once}]),
@@ -263,7 +286,7 @@ async_recv(Parent, Owner, Ref,
263286
Transport:controlling_process(TSock, From),
264287
From ! {Ref, ok};
265288
{OK, Sock, Data} ->
266-
stream_loop(Parent, Owner, Ref, Client#client{buffer=Data});
289+
Continuation(Client#client{buffer=Data});
267290
{Closed, Sock} ->
268291
case Client#client.response_state of
269292
on_body when (Version =:= {1, 0} orelse Version =:= {1, 1})
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
-module(hackney_integration_tests_async_long_headers).
2+
-include_lib("eunit/include/eunit.hrl").
3+
-include("hackney_lib.hrl").
4+
5+
all_tests() ->
6+
[fun absolute_redirect_request_follow/1].
7+
8+
http_requests_test_() ->
9+
TestMatrix = [
10+
#{status_code => <<"301">>, method => get},
11+
#{status_code => <<"303">>, method => post}
12+
],
13+
lists:map(fun(Props) ->
14+
{foreach,
15+
fun() -> start(Props) end,
16+
fun(StartResult) -> stop(StartResult, Props) end,
17+
all_tests()
18+
}
19+
end,
20+
TestMatrix).
21+
22+
start(#{status_code := StatusCode, method := Method}) ->
23+
{ok, _} = application:ensure_all_started(hackney),
24+
{ok, LSock} = gen_tcp:listen(0, [{active, false}]),
25+
{ok, {_, Port}} = inet:sockname(LSock),
26+
Pid = spawn_link(fun () -> dummy_server_loop(LSock, Port, StatusCode) end),
27+
gen_tcp:controlling_process(LSock, Pid),
28+
#{
29+
dummy_http_pid => Pid,
30+
method => Method,
31+
port => Port
32+
}.
33+
34+
stop(#{dummy_http_pid := Pid}, _Props) ->
35+
exit(Pid, normal),
36+
application:stop(hackney),
37+
error_logger:tty(true),
38+
ok.
39+
40+
41+
absolute_redirect_request_follow(#{method := Method, port := Port}) ->
42+
PortBin = list_to_binary(integer_to_list(Port)),
43+
URL = <<"http://localhost:", PortBin/binary>>,
44+
ExpectedRedirectUrl = <<"http://localhost:", PortBin/binary, "/redirected">>,
45+
Options = [{follow_redirect, true}, {async, true}],
46+
{ok, Ref} = hackney:request(Method, URL, [], <<>>, Options),
47+
case Method of
48+
get ->
49+
[
50+
receive
51+
{hackney_response, Ref, {redirect, RedirectUrl, _headers}} ->
52+
?_assertEqual(ExpectedRedirectUrl, RedirectUrl);
53+
Other ->
54+
throw({error, Other})
55+
after 1000 ->
56+
throw(timeout)
57+
end
58+
];
59+
post ->
60+
[
61+
receive
62+
{hackney_response, Ref, {see_other, RedirectUrl, _headers}} ->
63+
?_assertEqual(ExpectedRedirectUrl, RedirectUrl);
64+
Other ->
65+
throw({error, Other})
66+
after 1000 ->
67+
throw(timeout)
68+
end
69+
]
70+
end.
71+
72+
dummy_server_loop(LSock, Port, StatusCode) ->
73+
{ok, Sock} = gen_tcp:accept(LSock),
74+
PortBin = list_to_binary(integer_to_list(Port)),
75+
RedirectUrl = <<"http://localhost:", PortBin/binary, "/redirected">>,
76+
Response = iolist_to_binary([
77+
"HTTP/1.1 ",
78+
StatusCode,
79+
" Moved Permanently\r\nLocation: ",
80+
RedirectUrl,
81+
"\r\nExtra-Header:",
82+
binary:copy(<<"a">>, 1024),
83+
"\r\n\r\n"
84+
]),
85+
send(Sock, Response),
86+
ok = gen_tcp:shutdown(Sock, read_write),
87+
dummy_server_loop(LSock, RedirectUrl, StatusCode).
88+
89+
send(Sock, << Data :128/binary, Rest/binary>>) ->
90+
ok = gen_tcp:send(Sock, Data),
91+
send(Sock, Rest);
92+
93+
send(Sock, Data) ->
94+
ok = gen_tcp:send(Sock, Data).

0 commit comments

Comments
 (0)