Packages

Twirp provides an Elixir implementation of the Twirp RPC framework.

Current section

Files

Jump to
twirp_elixir lib twirp client http.ex
Raw

lib/twirp/client/http.ex

defmodule Twirp.Client.HTTP do
@moduledoc false
# This module handles the internals of making RPC calls. We delegate to this
# from the actual client module cuz otherwise the client module is a pita
# to understand due to the macros and functions its creating.
alias Twirp.Encoder
alias Twirp.Error
def start_link(mod, opts) do
mod.start_link(opts)
end
def call(mod, client, ctx, rpc) do
path = "#{rpc.service_url}/#{rpc.method}"
content_type = ctx.content_type
encoded_payload = Encoder.encode(rpc.req, rpc.input_type, content_type)
case mod.request(client, ctx, path, encoded_payload) do
{:error, %{reason: :timeout}} ->
meta = %{error_type: "timeout"}
msg = "Deadline to receive data from the service was exceeded"
{:error, Error.deadline_exceeded(msg, meta)}
{:error, %{reason: reason}} ->
meta = %{error_type: "#{reason}"}
{:error, Error.unavailable("Service is down", meta)}
{:error, e} ->
meta = %{error_type: "#{inspect(e)}"}
{:error, Error.internal("Unhandled client error", meta)}
{:ok, %{status: status} = resp} when status != 200 ->
{:error, build_error(resp, rpc)}
{:ok, %{status: 200} = resp} ->
handle_success(resp, rpc, content_type)
end
end
defp handle_success(resp, rpc, content_type) do
resp_content_type = resp_header(resp, "Content-Type")
if resp_content_type && String.starts_with?(resp_content_type, content_type) do
Encoder.decode(resp.body, rpc.output_type, content_type)
else
{:error,
Error.internal(
~s|Expected response Content-Type "#{content_type}" but found #{resp_content_type || "nil"}|
)}
end
end
defp build_error(resp, _rpc) do
status = resp.status
cond do
http_redirect?(status) ->
location = resp_header(resp, "location")
meta = %{
http_error_from_intermediary: "true",
not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
status_code: Integer.to_string(status),
location: location
}
msg = "Unexpected HTTP Redirect from location=#{location}"
Error.internal(msg, meta)
true ->
case Encoder.decode_json(resp.body) do
{:ok, %{"code" => code, "msg" => msg} = error} ->
if Error.valid_code?(code) do
# Its safe to convert to an atom here since all the codes are already
# created and loaded. If we explode we explode.
Error.new(String.to_existing_atom(code), msg, error["meta"] || %{})
else
Error.internal("Invalid Twirp error code: #{code}",
invalid_code: code,
body: resp.body
)
end
{:ok, _} ->
msg = "Response is JSON but it has no \"code\" attribute"
intermediary_error(status, msg, resp.body)
{:error, _} ->
intermediary_error(status, "Response is not JSON", resp.body)
end
end
end
defp intermediary_error(status, reason, body) do
meta = %{
http_error_from_intermediary: "true",
not_a_twirp_error_because: reason,
status_code: Integer.to_string(status),
body: body
}
case status do
400 -> Error.internal("internal", meta)
401 -> Error.unauthenticated("unauthenticated", meta)
403 -> Error.permission_denied("permission denied", meta)
404 -> Error.bad_route("bad route", meta)
s when s in [429, 502, 503, 504] -> Error.unavailable("unavailable", meta)
_ -> Error.unknown("unknown", meta)
end
end
defp http_redirect?(status) do
300 <= status && status <= 399
end
defp resp_header(resp, header) do
case Enum.find(resp.headers, fn {h, _} -> String.downcase(h) == String.downcase(header) end) do
{_, value} ->
value
_ ->
nil
end
end
end