Current section
Files
Jump to
Current section
Files
lib/uof/api/utils/http.ex
defmodule UOF.API.Utils.HTTP do
@moduledoc false
alias UOF.API.Error
alias UOF.Schemas.XML
def get(path, params \\ [], opts \\ []) do
opts
|> client()
|> Req.get(url: Enum.join(path, "/"), params: params)
|> handle_response()
end
def post(path, body \\ "", params \\ [], opts \\ []) do
opts
|> client()
|> Req.post(url: Enum.join(path, "/"), params: params, body: body)
|> handle_response()
end
# `opts` is merged last (via Req.merge/2, so headers/params combine rather
# than clobber) letting a single call override the `:req_options` app config.
defp client(opts) do
[
base_url: Application.fetch_env!(:uof_api, :base_url) <> "/",
# Req only strips the standard `authorization` header on cross-origin
# redirects, not our custom x-access-token one, so redirects are
# disabled by default to keep the token from leaking to another origin.
redirect: false,
headers: %{
"x-access-token" => Application.fetch_env!(:uof_api, :auth_token),
"content-type" => "application/xml",
"accept" => "application/xml"
}
]
|> Req.new()
|> Req.merge(Application.get_env(:uof_api, :req_options, []))
|> Req.merge(opts)
end
# Classify the HTTP status before decoding: successful endpoints are polymorphic
# on their XML root element, while errors may be XML, JSON, HTML, or empty.
defp handle_response({:ok, %Req.Response{status: status} = response}) when status in 200..299 do
decode_success(response)
end
defp handle_response({:ok, %Req.Response{} = response}) do
{:error, http_error(response)}
end
defp handle_response({:error, reason}) do
{:error,
%Error{
type: :transport,
reason: reason,
message: "UOF API transport error: #{format_reason(reason)}"
}}
end
defp decode_success(%Req.Response{body: body}) when body in [nil, ""], do: {:ok, nil}
defp decode_success(%Req.Response{body: body} = response) when is_binary(body) do
if String.trim(body) == "" do
{:ok, nil}
else
decode_xml(response)
end
end
defp decode_success(%Req.Response{} = response) do
{:error, decode_error(response, {:unexpected_body, response.body})}
end
defp decode_xml(%Req.Response{body: body} = response) do
case XML.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, reason} -> {:error, decode_error(response, reason)}
end
rescue
exception -> {:error, decode_error(response, exception)}
end
defp http_error(%Req.Response{} = response) do
body = decode_error_body(response.body)
response_code = body_value(body, :response_code)
detail = body_value(body, :message) || body_value(body, :errors)
%Error{
type: :http,
status: response.status,
response_code: response_code,
headers: response.headers,
body: body,
message: http_error_message(response.status, response_code, detail)
}
end
defp decode_error_body(body) when body in [nil, ""], do: body
defp decode_error_body(body) when is_binary(body) do
if String.trim(body) == "" do
body
else
case XML.decode(body) do
{:ok, decoded} -> decoded
{:error, _reason} -> body
end
end
rescue
_exception -> body
end
defp decode_error_body(body), do: body
defp decode_error(%Req.Response{} = response, reason) do
%Error{
type: :decode,
status: response.status,
headers: response.headers,
body: response.body,
reason: reason,
message: "Unable to decode UOF API response: #{format_reason(reason)}"
}
end
defp body_value(body, key) when is_map(body) do
Map.get(body, key) || Map.get(body, Atom.to_string(key))
end
defp body_value(_body, _key), do: nil
defp http_error_message(status, response_code, detail) do
suffix = detail || response_code
if suffix do
"UOF API returned HTTP #{status}: #{format_detail(suffix)}"
else
"UOF API returned HTTP #{status}"
end
end
defp format_detail(detail) when is_binary(detail), do: detail
defp format_detail(detail), do: inspect(detail)
defp format_reason(%{__exception__: true} = exception), do: Exception.message(exception)
defp format_reason(reason), do: inspect(reason)
end