Sometimes it takes some digging to figure out how things work - for example HTTPoison is based on hackney, while HTTPotion is based on ibrowse. HTTPoison’s options are documented and found in the code here. The ones that stick out are:
-
:timeout
- timeout to establish a connection, in milliseconds. Default is 8000
-
:recv_timeout
- timeout used when receiving a connection. Default is 5000
-
:stream_to
- a PID to stream the response to
An asynchronous example is found here.
The following code demonstrates the beginnings of using HTTPoison’s asynchronous functionality within a GenServer
(without using Task
):
defmodule Poison do
use GenServer
defp handle_response(ref, response) do
IO.puts "Received all response parts for request #{inspect ref}"
IO.inspect response
end
defp handle_request_error(details) do
IO.puts "A request failed with reason: #{inspect details.reason}"
end
defp async_request(response_map, timeout, recv_timeout) do
url = "http://httparrot.herokuapp.com/get"
body = ""
headers = []
options =[stream_to: self(), timeout: timeout, recv_timeout: recv_timeout]
case HTTPoison.request :get, url, body, headers, options do
{:ok, result} ->
Map.put response_map, result.id, [] # start collecting a new response
{:error, details} ->
handle_request_error details
response_map
end
end
defp attach_response(response_map, response),
do: Map.update! response_map, response.id, &([response | &1])
def response_complete(response_map, ref) do
case Map.get response_map, ref, :none do
:none ->
{response_map, :none}
parts_in_reverse ->
response_parts = Enum.reverse parts_in_reverse
new_map = Map.delete response_map, ref
{new_map, response_parts}
end
end
defp response_error(response_map, error_msg) do
new_map = Map.delete response_map, error_msg.id
IO.puts "Request #{inspect error_msg.id} resulted in an error response with reason: #{inspect error_msg.reason}"
new_map
end
## callbacks: message handlers
def handle_cast(:regular, state) do
new_state = async_request state, 8000, 5000 # defaults
{:noreply, new_state}
end
def handle_cast(:short_connect, state) do
new_state = async_request state, 2, 5000
{:noreply, new_state}
end
def handle_cast(:short_receive, state) do
new_state = async_request state, 8000, 5
{:noreply, new_state}
end
def handle_info(%HTTPoison.AsyncStatus{} = msg, state) do
new_state = attach_response state, msg
{:noreply, new_state}
end
def handle_info(%HTTPoison.AsyncHeaders{} = msg, state) do
new_state = attach_response state, msg
{:noreply, new_state}
end
def handle_info(%HTTPoison.AsyncChunk{} = msg, state) do
new_state = attach_response state, msg
{:noreply, new_state}
end
def handle_info(%HTTPoison.AsyncEnd{} = msg, state) do
{new_state, response} = response_complete state, msg.id
handle_response msg.id, response
{:noreply, new_state}
end
def handle_info(%HTTPoison.Error{} = msg, state) do
new_state = response_error state, msg
{:noreply, new_state}
end
## callbacks: lifecycle
def init(_args) do
{:ok, %{}} # use map to collect the various parts of the response
end
def terminate(_reason, _state) do
:ok
end
## public interface
def start_link,
do: GenServer.start_link __MODULE__, []
def stop(pid),
do: GenServer.stop pid
## client interface
def short_connect(pid),
do: GenServer.cast pid, :short_connect
def short_receive(pid),
do: GenServer.cast pid, :short_receive
def regular(pid),
do: GenServer.cast pid, :regular
end
.
$ iex -S mix
iex(1)> {:ok,pid} = Poison.start_link
{:ok, #PID<0.398.0>}
iex(2)> Poison.short_connect pid
:ok
A request failed with reason: :connect_timeout
iex(3)> Poison.short_receive pid
:ok
Request #Reference<0.0.6.4279> resulted in an error response with reason: {:closed, :timeout}
iex(4)> Poison.regular pid
:ok
Received all response parts for request #Reference<0.0.6.4284>
[%HTTPoison.AsyncStatus{code: 200, id: #Reference<0.0.6.4284>},
%HTTPoison.AsyncHeaders{headers: [{"Connection", "keep-alive"},
{"Server", "Cowboy"}, {"Date", "Thu, 15 Jun 2017 03:04:49 GMT"},
{"Content-Length", "493"}, {"Content-Type", "application/json"},
{"Via", "1.1 vegur"}], id: #Reference<0.0.6.4284>},
%HTTPoison.AsyncChunk{chunk: "{\n \"args\": {},\n \"headers\": {\n \"host\": \"httparrot.herokuapp.com\",\n \"connection\": \"close\",\n \"user-agent\": \"hackney/1.8.6\",\n \"x-request-id\": \"db9e3183-036b-455f-bb9c-2c0dc379ce9a\",\n \"x-forwarded-for\": \"72.39.127.107\",\n \"x-forwarded-proto\": \"http\",\n \"x-forwarded-port\": \"80\",\n \"via\": \"1.1 vegur\",\n \"connect-time\": \"0\",\n \"x-request-start\": \"1497495889950\",\n \"total-route-time\": \"0\"\n },\n \"url\": \"http://httparrot.herokuapp.com/get\",\n \"origin\": \"10.171.119.12\"\n}",
id: #Reference<0.0.6.4284>}]
iex(5)> Poison.stop pid
:ok
iex(6)>
Now one interesting thing to note is that the error for the connection timeout (as opposed to the receive timeout) is reported as a return value of request
. This suggests that request
actually blocks until it has established a connection - it is only from that point on that asynchronous operation begins.
It is therefore in your best interest to make the :timeout
value much smaller than the default of 8000 (8 secs). The :recv_timeout
can be longer as this is entirely handled by hackney (HTTPoison) and won’t lock up your GenServer
.