Packages
hackney
0.14.0
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/hackney_client/hackney_manager.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
-module(hackney_manager).
-behaviour(gen_server).
-export([new_request/1,
start_async_response/1,
stop_async_response/1,
cancel_request/1,
close_request/1,
controlling_process/2]).
-export([get_state/1, get_state/2,
update_state/1, update_state/2,
take_control/2,
handle_error/1]).
-export([async_response_pid/1,
with_async_response_pid/2]).
-export([start_link/0]).
%% private gen_server api
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("hackney.hrl").
-record(request, {ref,
pid,
async_pid=nil,
state}).
new_request(InitialState) ->
gen_server:call(?MODULE, {new_request, self(), InitialState}).
cancel_request(#client{request_ref=Ref}) ->
cancel_request(Ref);
cancel_request(Ref) when is_reference(Ref) ->
gen_server:call(?MODULE, {cancel_request, Ref, self()}).
close_request(Ref) ->
gen_server:call(?MODULE, {close_request, Ref}).
controlling_process(Ref, Pid) ->
Reply = gen_server:call(?MODULE, {controlling_process, Ref, Pid}),
case Reply of
ok ->
ok;
{ok, {Transport, Socket}} ->
Transport:controlling_process(Socket, Pid);
Error ->
Error
end.
start_async_response(Ref) ->
case ets:lookup(?MODULE, Ref) of
[] ->
req_not_found;
[{Ref, #request{state=Client}=Req}] ->
#client{transport=Transport,
socket=Socket} = Client,
case gen_server:call(?MODULE, {start_async_response, Req}) of
{ok, Pid} ->
Transport:controlling_process(Socket, Pid);
Error ->
Error
end
end.
stop_async_response(Ref) ->
case ets:lookup(?MODULE, Ref) of
[] ->
{error, req_not_found};
[{_Ref, #request{async_pid=nil}}] ->
{error, req_not_async};
[{_Ref, #request{async_pid=Pid}=Req}] ->
Pid ! {Ref, stop_async, self()},
receive
{Ref, ok} ->
ets:insert(?MODULE, {Ref, Req#request{async_pid=nil}}),
{ok, Ref};
Error ->
Error
after 5000 ->
{error, timeout}
end
end.
async_response_pid(Ref) ->
case ets:lookup(?MODULE, Ref) of
[] ->
{error, req_not_found};
[{_Ref, #request{async_pid=nil}}] ->
{error, req_not_async};
[{_Ref, #request{async_pid=Pid}}] ->
{ok, Pid}
end.
with_async_response_pid(Ref, Fun) ->
case async_response_pid(Ref) of
{ok, Pid} ->
Fun(Pid);
Error ->
Error
end.
get_state(#client{request_ref=Ref}) ->
get_state(Ref);
get_state(Ref) ->
case ets:lookup(?MODULE, Ref) of
[] ->
req_not_found;
[{Ref, #request{state=State}}] ->
State
end.
get_state(Ref, Fun) ->
case get_state(Ref) of
req_not_found -> {error, req_not_found};
State -> Fun(State)
end.
update_state(#client{request_ref=Ref}=NState) ->
update_state(Ref, NState).
update_state(Ref, NState) ->
case ets:lookup(?MODULE, Ref) of
[] -> ok;
[{Ref, Req}] ->
true = ets:insert(?MODULE, {Ref, Req#request{state=NState}}),
ok
end.
take_control(Ref, NState) ->
case ets:lookup(?MODULE, Ref) of
[] -> {error, req_not_found};
[{Ref, Req}] ->
true = ets:insert(?MODULE, {Ref, Req#request{pid=self(),
state=NState}}),
ok
end.
handle_error(#client{request_ref=Ref, dynamic=true}) ->
close_request(Ref);
handle_error(#client{request_ref=Ref, transport=Transport,
socket=Socket}) ->
case ets:lookup(?MODULE, Ref) of
[] -> ok;
[{Ref, #request{state=State}}] ->
Transport:controlling_process(Socket, self()),
Transport:close(Socket),
NState = State#client{socket=nil,
state=closed},
true = ets:insert(?MODULE, {Ref, NState}),
ok
end.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init(_) ->
ets:new(hackney_pool, [
named_table,
set,
public
]),
ets:new(?MODULE, [
set,
{keypos, 1},
public,
named_table,
{write_concurrency, true}
]),
process_flag(trap_exit, true),
{ok, dict:new()}.
handle_call({new_request, Pid, InitialState}, _From, Children) ->
{Ref, NChildren} = make_request(Pid, InitialState, Children),
{reply, Ref, NChildren};
handle_call({start_async_response, #request{ref=Ref}=Req}, _From, Children) ->
case do_start_async_response(Req, Children) of
{ok, #request{async_pid=Pid}=NReq, Children2} ->
true = ets:insert(?MODULE, {Ref, NReq}),
NChildren = dict:store(Pid, Ref, Children2),
{reply, {ok, Pid}, NChildren};
Error ->
{reply, Error, Children}
end;
handle_call({controlling_process, Ref, Pid}, _From, Children) ->
Self = self(),
case ets:lookup(?MODULE, Ref) of
[] ->
{reply, req_not_found, Children};
[{Ref, #request{pid=Owner, state=St}=Req}]
when Owner =:= Self ->
%% the manager is actually controlling the process
ets:insert(?MODULE, Req#request{pid=Pid}),
NChildren = dict:store(Pid, Ref, Children),
link(Pid),
%% pas the control to the new client process
#client{transport=Transport, socket=Socket} = St,
Reply = Transport:controlling_process(Socket, Pid),
{reply, Reply, NChildren};
[{Ref, #request{pid=Owner, state=St}=Req}] ->
Children1 = case dict:is_key(Owner, Children) of
true ->
unlink(Owner),
dict:erase(Owner, Children);
false ->
Children
end,
ets:insert(?MODULE, Req#request{pid=Pid}),
NChildren = dict:store(Pid, Ref, Children1),
link(Pid),
#client{transport=Transport, socket=Socket} = St,
{reply, {ok, {Transport, Socket}}, NChildren}
end;
handle_call({cancel_request, Ref, Owner}, _From, Children) ->
case ets:lookup(?MODULE, Ref) of
[] ->
{reply, req_not_found, Children};
[{Ref, #request{pid=Pid, state=St}=Req}] ->
ets:delete(?MODULE, Ref),
Children1 = case dict:is_key(Pid, Children) of
true ->
unlink(Pid),
dict:erase(Pid, Children);
false ->
Children
end,
case maybe_close_async_response(Req, Children1) of
{ok, NChildren} ->
#client{transport=Transport,
socket=Socket,
buffer=Buffer,
response_state=RespState}=St,
case Transport:controlling_process(Socket, Owner) of
ok ->
{reply, {Transport, Socket, Buffer,
RespState}, NChildren};
Error ->
{reply, Error, NChildren}
end;
{Error, NChildren} ->
{reply, Error, NChildren}
end
end;
handle_call({close_request, Ref}, _From, Children) ->
case ets:lookup(?MODULE, Ref) of
[] ->
{reply, req_not_found, Children};
[{Ref, #request{pid=Pid, state=St}=Req}] ->
ets:delete(?MODULE, Ref),
Children1 = case dict:is_key(Pid, Children) of
true ->
unlink(Pid),
dict:erase(Pid, Children);
false ->
Children
end,
%% close the async request if any
{Reply, NChildren} = maybe_close_async_response(Req, Children1),
%% make sure we close the socket
#client{transport=Transport, socket=Socket,
state=Status}=St,
case Status of
done ->
ok;
_ when Socket /= nil ->
catch Transport:controlling_process(Socket, self()),
catch Transport:close(Socket);
_ ->
ok
end,
{reply, Reply, NChildren}
end.
handle_cast(_Msg, Children) ->
{noreply, Children}.
handle_info({'EXIT', Pid, Reason}, Children) ->
NChildren = case dict:find(Pid, Children) of
{value, Ref} ->
case ets:lookup(?MODULE, Ref) of
[] ->
dict:erase(Pid, Children);
[{Ref, Req}] ->
handle_exit(Pid, Req, Reason, Children)
end,
dict:erase(Pid, Children);
_ ->
Children
end,
{noreply, NChildren};
handle_info(_Info, Children) ->
{noreply, Children}.
code_change(_OldVsn, Ring, _Extra) ->
{ok, Ring}.
terminate(_Reason, _Ring) ->
ok.
make_request(Pid, InitialState, Children) ->
Ref = make_ref(),
Req = #request{ref=Ref,
pid=Pid,
state=InitialState#client{request_ref=Ref}},
link(Pid),
NChildren = dict:store(Pid, Ref, Children),
ets:insert(?MODULE, {Ref, Req}),
{Ref, NChildren}.
do_start_async_response(#request{ref=Ref, pid=Owner, state=Client}=Req,
Children) ->
{StreamTo, NChildren} = case Client#client.stream_to of
false ->
{Owner, Children};
To ->
link(To),
Children1 = case dict:is_key(Owner, Children) of
true ->
unlink(Owner),
dict:erase(Owner, Children);
false ->
Children
end,
{To, dict:store(To, Ref, Children1)}
end,
case catch hackney_stream:start_link(StreamTo, Ref, Client) of
{ok, Pid} when is_pid(Pid) ->
{ok, Req#request{pid=StreamTo, async_pid=Pid}, NChildren};
{error, What} ->
{error, What};
What ->
{error, What}
end.
%% handle ext.
handle_exit(AsyncPid, #request{ref=Ref, pid=Pid, async_pid=AsyncPid}=Req,
Reason, Children) ->
%% the async request exited, tell it to the parent.
case Reason of
normal ->
ok;
_ ->
Pid ! {'DOWN', Ref, Reason}
end,
%% delete the async pid from the children
Children1 = dict:erase(AsyncPid, Children),
%% delete the pid from the childrent
NChildren = dict:erase(Pid, Children1),
%% store the new request reference, the manager is the new owner of
%% the request.
ets:insert(?MODULE, {Ref, Req#request{pid=self(), async_pid=nil}}),
NChildren;
handle_exit(Pid, #request{ref=Ref, pid=Pid, async_pid=nil}, _Reason,
Children) ->
%% delete the reference, the parent exited
ets:delete(?MODULE, Ref),
%% delete the pid from the children
dict:erase(Pid, Children);
handle_exit(Pid, #request{pid=Pid, async_pid=AsyncPId}=Req,
Reason, Children) ->
%% delete the async response process from the children
NChildren = dict:erase(AsyncPId, Children),
%% terminate the async request
terminate_async_response(Req),
%% go deleting the reference
handle_exit(Pid, Req#request{async_pid=nil}, Reason, NChildren).
monitor_child(Pid) ->
erlang:monitor(process, Pid),
unlink(Pid),
receive
{'EXIT', Pid, Reason} ->
receive
{'DOWN', _, process, Pid, _} ->
{error, Reason}
end
after 0 ->
ok
end.
terminate_async_response(Req) ->
terminate_async_response(Req, shutdown).
terminate_async_response(#request{async_pid=Pid}=Req, Reason) ->
case monitor_child(Pid) of
ok ->
exit(Pid, Reason),
wait_async_response(Req, Pid);
Error ->
Error
end.
wait_async_response(_Req, Pid) ->
receive
{'DOWN', _MRef, process, Pid, _Reason} ->
ok
end.
maybe_close_async_response(#request{async_pid=nil}, Children) ->
{ok, Children};
maybe_close_async_response(#request{async_pid=Pid}=Req, Children)
when is_pid(Pid) ->
case dict:is_key(Pid, Children) of
true ->
NChildren = dict:erase(Pid, Children),
case terminate_async_response(Req) of
ok ->
{ok, NChildren};
Error ->
{Error, NChildren}
end;
false ->
{ok, Children}
end.