Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

add option to automatically follow a redirection.

If the option `{follow_redirect, true}` is given to the request, the
client will be abble to automatically follow the redirection and
retrieve the body. The maximum number of connection can be set using the
`{max_redirect, Max}` option. Default is 5.

The client will follow redirection on 301, 302 & 307 if the method is
get or head. If another method is used the tuple
`{ok, maybe_redirect, Status, Headers, Client}` will be returned. It
only follow 303 redirection (see other) if the method is a POST.

Last Location is stored in the client state in the `location` property.

ex:

    Method = get,
    URL = "http://friendpaste.com/",
    ReqHeaders = [{<<"accept-encoding">>, <<"identity">>}],
    ReqBody = <<>>,
    Options = [{follow_redirect, true}, {max_redirect, true}],
    {ok, S, H, Client} = hackney:request(Method, URL, ReqHeaders,
                                         ReqBody, Options),
    {ok, Body, Client1} = hackney:body(Client).
  • Loading branch information...
commit 315d23061756dc29b152dc8ed2009d65043766ea 1 parent aa80e1e
@benoitc benoitc authored
View
4 include/hackney.hrl
@@ -43,6 +43,10 @@
options = [],
socket = nil,
timeout = infinity,
+ follow_redirect = false,
+ max_redirect = 5,
+ redirect = nil,
+ location,
state,
response_state = start,
req_type = normal,
View
147 src/hackney.erl
@@ -34,12 +34,29 @@ stop() ->
application:stop(hackney).
%% @doc connect a socket and create a client state.
-connect(#client{state=connected}=Client) ->
+connect(#client{state=connected, redirect=nil}=Client) ->
{ok, Client};
-connect(#client{state=closed}=Client) ->
+connect(#client{state=connected, redirect=Redirect}=Client) ->
+ #client{host=Host, port=Port, transport=Transport,
+ socket=Socket}=Client,
+
+ case pool(Client) of
+ undefined ->
+ close(Client);
+ Pool ->
+ hackney_pool:release(Pool, {Transport, Host, Port}, Socket)
+ end,
+ connect(Redirect);
+connect(#client{state=closed, redirect=nil}=Client) ->
#client{transport=Transport, host=Host, port=Port} = Client,
- connect(Transport, Host, Port, Client).
+ connect(Transport, Host, Port, Client);
+
+connect(#client{state=closed, redirect=Redirect}) ->
+ connect(Redirect);
+
+connect({Transport, Host, Port, Options}) ->
+ connect(Transport, Host, Port, #client{options=Options}).
connect(Transport, Host, Port) ->
connect(Transport, Host, Port, #client{options=[]}).
@@ -151,13 +168,16 @@ send_request(#client{response_state=done}=Client0 ,
Client = Client0#client{response_state=start, body_state=waiting},
send_request(Client, {Method, Path, Headers, Body});
-send_request(Client0, {Method, Path, Headers, Body}) ->
+send_request(Client0, {Method, Path, Headers, Body}=Req) ->
case connect(Client0) of
{ok, Client} ->
case {Client#client.response_state, Client#client.body_state} of
{start, waiting} ->
- hackney_request:perform(Client,
- {Method, Path, Headers, Body});
+ Resp = hackney_request:perform(Client, {Method,
+ Path,
+ Headers,
+ Body}),
+ maybe_redirect(Resp, Req, 0);
_ ->
{error, invalide_state}
@@ -219,20 +239,27 @@ pool(#client{options=Opts}) ->
%% internal functions
%%
-socket_from_pool(Pool, {Transport, Host, Port}=Key, Client) ->
+
+socket_from_pool(Pool, {Transport, Host, Port}=Key,
+ #client{options=Opts}=Client) ->
case hackney_pool:socket(Pool, Key) of
{ok, Skt} ->
+ FollowRedirect = proplists:get_value(follow_redirect,
+ Opts, false),
+ MaxRedirect = proplists:get_value(max_redirect, Opts, 5),
{ok, Client#client{transport=Transport,
host=Host,
port=Port,
socket=Skt,
- state = connected}};
+ state = connected,
+ follow_redirect=FollowRedirect,
+ max_redirect=MaxRedirect}};
no_socket ->
do_connect(Transport, Host, Port, Client)
end.
-do_connect(Transport, Host, Port, #client{options=Options}=Client) ->
- ConnectOpts0 = proplists:get_value(connect_options, Options, []),
+do_connect(Transport, Host, Port, #client{options=Opts}=Client) ->
+ ConnectOpts0 = proplists:get_value(connect_options, Opts, []),
%% handle ipv6
ConnectOpts = case hackney_util:is_ipv6(Host) of
@@ -244,11 +271,109 @@ do_connect(Transport, Host, Port, #client{options=Options}=Client) ->
case Transport:connect(Host, Port, ConnectOpts) of
{ok, Skt} ->
+ FollowRedirect = proplists:get_value(follow_redirect,
+ Opts, false),
+ MaxRedirect = proplists:get_value(max_redirect, Opts, 5),
{ok, Client#client{transport=Transport,
host=Host,
port=Port,
socket=Skt,
- state = connected}};
+ state = connected,
+ follow_redirect=FollowRedirect,
+ max_redirect=MaxRedirect}};
+ Error ->
+ Error
+ end.
+
+maybe_redirect({ok, _}=Resp, _Req, _Tries) ->
+ Resp;
+maybe_redirect({ok, S, H, #client{follow_redirect=true,
+ max_redirect=Max}=Client}=Resp, Req, Tries)
+ when Tries < Max ->
+
+ {Method, _Path, Headers, Body} = Req,
+ case lists:member(S, [301, 302, 307]) of
+ true ->
+ Location = redirect_location(H),
+ %% redirect the location if possible. If the method is
+ %% different from get or head it will return
+ %% `{ok, {maybe_redirect, Status, Headers, Client}}' to let
+ %% the user make his choice.
+ case {Location, lists:member(Method, [get, head])} of
+ {undefined, _} ->
+ {error, {invalid_redirection, Resp}};
+ {_, true} ->
+ NewReq = {Method, Location, Headers, Body},
+ maybe_redirect(redirect(Client, NewReq), Req,
+ Tries+1);
+ {_, _} ->
+ {ok, {maybe_redirect, S, H, Client}}
+ end;
+ false when S =:= 303 ->
+ %% see other. If methos is not POST we consider it as an
+ %% invalid redirection
+ Location = redirect_location(H),
+ case {Location, Method} of
+ {undefined, _} ->
+ {error, {invalid_redirection, Resp}};
+ {_, post} ->
+ NewReq = {get, Location, [], <<>>},
+ maybe_redirect(redirect(Client, NewReq), Req, Tries+1);
+ {_, _} ->
+
+ {error, {invalid_redirection, Resp}}
+ end;
+ _ ->
+ Resp
+ end;
+maybe_redirect({ok, S, _H, #client{follow_redirect=true}}=Resp,
+ _Req, _Tries) ->
+ case lists:member(S, [301, 302, 303, 307]) of
+ true ->
+ {error, {max_redirect_overflow, Resp}};
+ false ->
+ Resp
+ end;
+maybe_redirect(Resp, _Req, _Tries) ->
+ Resp.
+
+
+
+redirect(Client0, {Method, NewLocation, Headers, Body}) ->
+ %% skip the body
+ {ok, Client} = skip_body(Client0),
+
+
+ %% close the connection if we don't use a pool
+ Client1 = case Client#client.state of
+ closed -> Client;
+ _ -> close(Client)
+ end,
+
+ %% make a request without any redirection
+ #client{transport=Transport,
+ host=Host,
+ port=Port,
+ options=Opts0,
+ redirect=Redirect} = Client1,
+ Opts = lists:keystore(follow_redirect, 1, Opts0,
+ {follow_redirect, false}),
+
+
+ case request(Method, NewLocation, Headers, Body, Opts) of
+ {ok, S, H, RedirectClient} when Redirect /= nil ->
+ NewClient = RedirectClient#client{redirect=Redirect,
+ options=Opts0},
+ {ok, S, H, NewClient};
+ {ok, S, H, RedirectClient} ->
+ NewRedirect = {Transport, Host, Port, Opts0},
+ NewClient = RedirectClient#client{redirect=NewRedirect,
+ options=Opts0},
+ {ok, S, H, NewClient};
+
Error ->
Error
end.
+
+redirect_location(Headers) ->
+ hackney_headers:get_value(<<"location">>, hackney_headers:new(Headers)).
View
3  src/hackney_pool.erl
@@ -193,7 +193,7 @@ find_connection({Transport, _Host, _Port}=Key, Pid,
remove_socket(Socket, #state{connections=Conns, sockets=Sockets}=State) ->
case dict:find(Socket, Sockets) of
- {Key, Timer} ->
+ {ok, {Key, Timer}} ->
cancel_timer(Socket, Timer),
ConnSockets = lists:delete(Socket, dict:fetch(Key, Conns)),
NewConns = update_connections(ConnSockets, Key, Conns),
@@ -214,7 +214,6 @@ store_connection({Transport, _, _} = Key, Socket,
[Socket | OldSockets];
error -> [Socket]
end,
-
State#state{connections = dict:store(Key, ConnSockets, Conns),
sockets = dict:store(Socket, {Key, Timer}, Sockets)}.
View
23 src/hackney_response.erl
@@ -73,29 +73,26 @@ stream_headers(Client, Headers) ->
end.
-stream_header(Client) ->
- stream_header(Client, []).
-
-stream_header(#client{buffer=Buf}=Client, Acc) ->
+stream_header(#client{buffer=Buf}=Client) ->
case binary:split(Buf, <<"\r\n">>) of
[<<>>, Rest] ->
{headers_complete, Client#client{buffer=Rest,
response_state=on_body}};
[<< " ", Line/binary >>, Rest] ->
- stream_header(Client#client{buffer=Rest}, [ Line | Acc ]);
+ NewBuf = iolist_to_binary([Line, Rest]),
+ stream_header(Client#client{buffer=NewBuf});
[<< "\t", Line/binary >>, Rest] ->
- stream_header(Client#client{buffer=Rest}, [ Line | Acc ]);
- [_Line, _Rest] when Acc /= []->
- parse_header(iolist_to_binary(lists:reverse(Acc)), Client);
- [Line, Rest] ->
- stream_header(Client#client{buffer=Rest}, [Line]);
+ NewBuf = iolist_to_binary([Line, Rest]),
+ stream_header(Client#client{buffer=NewBuf});
+ [Line, Rest]->
+ parse_header(Line, Client#client{buffer=Rest});
[Buf] ->
case recv(Client) of
{ok, Data} ->
NewBuf = << Buf/binary, Data/binary >>,
- stream_header(Client#client{buffer=NewBuf}, Acc);
+ stream_header(Client#client{buffer=NewBuf});
{error, Reason} ->
- {error, Reason, Acc}
+ {error, Reason, Buf}
end
end.
@@ -112,6 +109,8 @@ parse_header(Line, Client) ->
Client#client{connection=hackney_util:to_lower(Value)};
<<"content-type">> ->
Client#client{ctype=hackney_util:to_lower(Value)};
+ <<"location">> ->
+ Client#client{location=Value};
_ ->
Client
end,
Please sign in to comment.
Something went wrong with that request. Please try again.