Current section

Files

Jump to
exkeycdn lib http.ex
Raw

lib/http.ex

defmodule ExKeyCDN.HTTP do
@moduledoc """
Base client for all server interaction, used by all endpoint specific
modules.
This request wrapper coordinates the remote server, headers, authorization
and SSL options.
Using `ExKeyCDN.HTTP` requires the presence of one config value:
* `api_key` - ExKeyCDN api key
Value must be set or a `ExKeyCDN.ConfigError` will be raised at
runtime. All those config values support the `{:system, "VAR_NAME"}` as a
value - in which case the value will be read from the system environment with
`System.get_env("VAR_NAME")`.
"""
require Logger
alias ExKeyCDN.ErrorResponse, as: Error
alias ExKeyCDN.{Decoder, FormEncoder, JsonEncoder}
@behaviour ExKeyCDN.HTTPBehaviour
@type response ::
{:ok, map, list | {:error, atom}}
| {:error, Error.t()}
| {:error, binary}
@headers [
{"Accept", "application/json"},
{"User-Agent", "ExKeyCDN Elixir/0.1"},
{"X-ApiVersion", "1"},
{"Content-Type", "application/x-www-form-urlencoded"}
]
@statuses %{
200 => :ok,
400 => :bad_request,
401 => :unauthorized,
403 => :forbidden,
404 => :not_found,
413 => :request_entity_too_large,
429 => :too_many_requests,
451 => :illegal,
500 => :server_error,
502 => :bad_gateway,
503 => :service_unavailable,
504 => :connect_timeout
}
@doc """
Centralized request handling function. All convenience structs use this
function to interact with the ExKeyCDN servers. This function can be used
directly to supplement missing functionality.
## Example
defmodule MyApp.ExKeyCDN do
alias ExKeyCDN.HTTP
def zones(params \\ %{}) do
HTTP.request(:get, "zones.json", params)
end
end
"""
@impl ExKeyCDN.HTTPBehaviour
@spec request(atom, binary, binary | map) :: response
def request(method, path, body \\ %{}) do
emit_start(method, path)
start_time = System.monotonic_time()
try do
case method do
:get ->
:hackney.request(
method,
build_url(path, body),
build_headers(),
encode_body(%{}, :form),
build_options()
)
:delete ->
case encode_body(body, :json) do
{:error, message} ->
{:error, message}
encoded_body ->
:hackney.request(
method,
build_url(path),
build_headers(),
encoded_body,
build_options()
)
end
_ ->
case encode_body(body, :form) do
{:error, message} ->
{:error, message}
encoded_body ->
:hackney.request(
method,
build_url(path),
build_headers(),
encoded_body,
build_options()
)
end
end
catch
kind, reason ->
duration = System.monotonic_time() - start_time
emit_exception(duration, method, path, %{
kind: kind,
reason: reason,
stacktrace: __STACKTRACE__
})
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:ok, code, headers, body} when code in 200..400 ->
duration = System.monotonic_time() - start_time
emit_stop(duration, method, path, code)
{:ok, decode_body(body), headers}
{:ok, code, _headers, _body} when code in 401..504 ->
duration = System.monotonic_time() - start_time
emit_stop(duration, method, path, code)
{:error, code_to_reason(code)}
{:error, reason} ->
duration = System.monotonic_time() - start_time
emit_error(duration, method, path, reason)
{:error, reason}
end
end
for method <- ~w(get delete post put)a do
@spec unquote(method)(binary) :: response
@spec unquote(method)(binary, map | list) :: response
def unquote(method)(path) do
request(unquote(method), path, %{})
end
def unquote(method)(path, payload) when is_map(payload) do
request(unquote(method), path, payload)
end
def unquote(method)(path, payload) do
request(unquote(method), path, payload)
end
end
## Helper Functions
@doc false
@spec build_url(binary, map) :: binary
def build_url(path, params \\ %{}) do
url = get_lazy_env([], :url) <> "/" <> path
case params == %{} do
true ->
url
false ->
url <> "?" <> FormEncoder.encode(params)
end
end
@doc false
@spec encode_body(binary | map, atom) :: binary | {:error, binary}
def encode_body(body, _type) when body == "" or body == %{}, do: ""
def encode_body(body, :form), do: FormEncoder.encode(body)
def encode_body(body, :json), do: JsonEncoder.encode(body)
@doc false
@spec decode_body(binary) :: map
def decode_body(body) do
body
|> String.trim()
|> Decoder.decode()
rescue
Jason.DecodeError ->
Logger.error("unprocessable response")
%{}
end
@doc false
@spec build_headers() :: [tuple]
def build_headers do
auth_header = "Basic " <> :base64.encode(get_lazy_env([], :api_key) <> ":")
[{"Authorization", auth_header} | @headers]
end
def get_lazy_env(opts, key, default \\ nil) do
Keyword.get_lazy(opts, key, fn -> ExKeyCDN.get_env(key, default) end)
end
@doc false
@spec build_options() :: [...]
def build_options do
http_opts = ExKeyCDN.get_env(:http_options, [])
[:with_body] ++ http_opts
end
@doc false
@spec code_to_reason(integer) :: atom
def code_to_reason(integer)
for {code, status} <- @statuses do
def code_to_reason(unquote(code)), do: unquote(status)
end
defp emit_start(method, path) do
:telemetry.execute(
[:exkeycdn, :request, :start],
%{system_time: System.system_time()},
%{method: method, path: path}
)
end
defp emit_exception(duration, method, path, error_data) do
:telemetry.execute(
[:exkeycdn, :request, :exception],
%{duration: duration},
%{method: method, path: path, error: error_data}
)
end
defp emit_error(duration, method, path, error_reason) do
:telemetry.execute(
[:exkeycdn, :request, :error],
%{duration: duration},
%{method: method, path: path, error: error_reason}
)
end
defp emit_stop(duration, method, path, code) do
:telemetry.execute(
[:exkeycdn, :request, :stop],
%{duration: duration},
%{method: method, path: path, http_status: code}
)
end
end