Packages
mojito_fresha
0.7.13
Mojito client but with castore updated, forked from https://github.com/surgeventures/mojito
Current section
Files
Jump to
Current section
Files
lib/mojito/conn_server.ex
defmodule Mojito.ConnServer do
@moduledoc false
use GenServer
require Logger
alias Mojito.{Conn, Response, Utils}
@type state :: map
@doc ~S"""
Starts a `Mojito.ConnServer`.
`Mojito.ConnServer` is a GenServer that handles a single
`Mojito.Conn`. It supports automatic reconnection,
connection keep-alive, and request pipelining.
It's intended for usage through `Mojito.Pool`.
Example:
{:ok, pid} = Mojito.ConnServer.start_link()
:ok = GenServer.cast(pid, {:request, self(), :get, "http://example.com", [], "", []})
receive do
{:ok, response} -> response
after
1_000 -> :timeout
end
"""
@spec start_link(Keyword.t()) :: {:ok, pid} | {:error, any}
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args)
end
@doc ~S"""
Initiates a request. The `reply_to` pid will receive the response in a
message of the format `{:ok, %Mojito.Response{}} | {:error, any}`.
"""
@spec request(pid, Mojito.request(), pid, reference) :: :ok | {:error, any}
def request(server_pid, request, reply_to, response_ref) do
GenServer.call(server_pid, {:request, request, reply_to, response_ref})
end
#### GenServer callbacks
def init(_) do
{:ok,
%{
conn: nil,
protocol: nil,
hostname: nil,
port: nil,
responses: %{},
reply_tos: %{},
response_refs: %{}
}}
end
def terminate(_reason, state) do
close_connections(state)
end
def handle_call(
{:request, request, reply_to, response_ref},
_from,
state
) do
with {:ok, state, _request_ref} <-
start_request(state, request, reply_to, response_ref) do
{:reply, :ok, state}
else
err -> {:reply, err, close_connections(state)}
end
end
## `msg` is an incoming chunk of a response
def handle_info(msg, state) do
if !state.conn do
{:noreply, close_connections(state)}
else
case Mint.HTTP.stream(state.conn.conn, msg) do
{:ok, mint_conn, resps} ->
state_conn = state.conn |> Map.put(:conn, mint_conn)
state = %{state | conn: state_conn}
{:noreply, apply_resps(state, resps)}
{:error, _mint_conn, _error, _resps} ->
{:noreply, close_connections(state)}
:unknown ->
{:noreply, state}
end
end
end
#### Helpers
@spec close_connections(state) :: state
defp close_connections(state) do
Enum.each(state.reply_tos, fn {_request_ref, reply_to} ->
respond(reply_to, {:error, :closed})
end)
%{state | conn: nil, responses: %{}, reply_tos: %{}, response_refs: %{}}
end
defp apply_resps(state, []), do: state
defp apply_resps(state, [resp | rest]) do
apply_resp(state, resp) |> apply_resps(rest)
end
defp apply_resp(state, {:status, request_ref, _status} = msg) do
{:ok, response} =
Map.get(state.responses, request_ref)
|> Response.apply_resp(msg)
%{state | responses: Map.put(state.responses, request_ref, response)}
end
defp apply_resp(state, {:headers, request_ref, _headers} = msg) do
{:ok, response} =
Map.get(state.responses, request_ref)
|> Response.apply_resp(msg)
%{state | responses: Map.put(state.responses, request_ref, response)}
end
defp apply_resp(state, {:data, request_ref, _chunk} = msg) do
case Map.get(state.responses, request_ref) |> Response.apply_resp(msg) do
{:ok, response} ->
%{state | responses: Map.put(state.responses, request_ref, response)}
{:error, _} = err ->
halt(state, request_ref, err)
end
end
defp apply_resp(state, {:error, request_ref, err}) do
halt(state, request_ref, {:error, err})
end
defp apply_resp(state, {:done, request_ref}) do
r = Map.get(state.responses, request_ref)
body = :erlang.list_to_binary(r.body)
size = byte_size(body)
response = %{r | complete: true, body: body, size: size}
halt(state, request_ref, {:ok, response})
end
defp halt(state, request_ref, response) do
response_ref = state.response_refs |> Map.get(request_ref)
Map.get(state.reply_tos, request_ref) |> respond(response, response_ref)
%{
state
| responses: Map.delete(state.responses, request_ref),
reply_tos: Map.delete(state.reply_tos, request_ref),
response_refs: Map.delete(state.response_refs, request_ref)
}
end
defp respond(pid, message, response_ref \\ nil) do
send(pid, {:mojito_response, response_ref, message})
end
@spec start_request(
state,
Mojito.request(),
pid,
reference
) :: {:ok, state, reference} | {:error, any}
defp start_request(state, request, reply_to, response_ref) do
with {:ok, state} <- ensure_connection(state, request.url, request.opts),
{:ok, conn, request_ref, response} <- Conn.request(state.conn, request) do
case response do
%{complete: true} ->
## Request was completed by server during stream_request_body
respond(reply_to, {:ok, response}, response_ref)
{:ok, %{state | conn: conn}, request_ref}
_ ->
responses = state.responses |> Map.put(request_ref, response)
reply_tos = state.reply_tos |> Map.put(request_ref, reply_to)
response_refs =
state.response_refs |> Map.put(request_ref, response_ref)
state = %{
state
| conn: conn,
responses: responses,
reply_tos: reply_tos,
response_refs: response_refs
}
{:ok, state, request_ref}
end
end
end
@spec ensure_connection(state, String.t(), Keyword.t()) ::
{:ok, state} | {:error, any}
defp ensure_connection(state, url, opts) do
with {:ok, protocol, hostname, port} <- Utils.decompose_url(url) do
new_destination =
state.protocol != protocol || state.hostname != hostname ||
state.port != port
cond do
!state.conn || new_destination ->
connect(state, protocol, hostname, port, opts)
:else ->
{:ok, state}
end
end
end
@spec connect(state, String.t(), String.t(), non_neg_integer, Keyword.t()) ::
{:ok, state} | {:error, any}
defp connect(state, protocol, hostname, port, opts) do
with {:ok, conn} <- Mojito.Conn.connect(protocol, hostname, port, opts) do
{:ok,
%{state | conn: conn, protocol: protocol, hostname: hostname, port: port}}
end
end
end