186 lines
5.0 KiB
Elixir
186 lines
5.0 KiB
Elixir
defmodule Plausible.HTTPClient.Non200Error do
|
|
defstruct reason: nil
|
|
|
|
@type t :: %__MODULE__{reason: Finch.Response.t()}
|
|
|
|
@spec new(Finch.Response.t()) :: t()
|
|
def new(%Finch.Response{status: status} = response)
|
|
when is_integer(status) and (status < 200 or status >= 300) do
|
|
%__MODULE__{reason: response}
|
|
end
|
|
end
|
|
|
|
defmodule Plausible.HTTPClient.Interface do
|
|
@type finch_request_opts() :: Keyword.t()
|
|
@type url() :: Finch.Request.url()
|
|
@type headers() :: Finch.Request.headers()
|
|
@type params() :: Finch.Request.body() | map()
|
|
@type response() ::
|
|
{:ok, Finch.Response.t()}
|
|
| {:error, Mint.Types.error() | Finch.Error.t() | Plausible.HTTPClient.Non200Error.t()}
|
|
|
|
@callback get(url(), headers(), params()) :: response()
|
|
@callback get(url(), headers()) :: response()
|
|
@callback get(url()) :: response()
|
|
@callback post(url(), headers(), params()) :: response()
|
|
@callback post(url(), headers(), params(), finch_request_opts()) :: response()
|
|
end
|
|
|
|
defmodule Plausible.HTTPClient do
|
|
@moduledoc """
|
|
HTTP Client built on top of Finch.
|
|
|
|
By default, request parameters are json-encoded.
|
|
|
|
If a raw binary value is supplied, no encoding is performed.
|
|
If x-www-form-urlencoded content-type is set in headers,
|
|
URL encoding is invoked.
|
|
"""
|
|
|
|
require OpenTelemetry.Tracer
|
|
alias Plausible.HTTPClient.Non200Error
|
|
|
|
@doc """
|
|
Make a POST request
|
|
"""
|
|
@behaviour Plausible.HTTPClient.Interface
|
|
|
|
@impl Plausible.HTTPClient.Interface
|
|
def post(url, headers \\ [], params \\ nil, finch_req_opts \\ []) do
|
|
call(:post, url, headers, params, finch_req_opts)
|
|
end
|
|
|
|
@doc """
|
|
Make a GET request
|
|
"""
|
|
@impl Plausible.HTTPClient.Interface
|
|
def get(url, headers \\ [], params \\ nil) do
|
|
call(:get, url, headers, params)
|
|
end
|
|
|
|
# TODO: Is it possible to tell the type checker that we're returning a module that conforms to the
|
|
# Plausible.HTTPClient.Interface behaviour?
|
|
@spec impl() :: Plausible.HTTPClient
|
|
def impl() do
|
|
Application.get_env(:plausible, :http_impl, __MODULE__)
|
|
end
|
|
|
|
defp call(method, url, headers, params, finch_req_opts \\ []) do
|
|
OpenTelemetry.Tracer.with_span "http_client.request" do
|
|
{params, headers} = maybe_encode_params(params, headers)
|
|
|
|
method
|
|
|> build_request(url, headers, params)
|
|
|> trace_request()
|
|
|> do_request(finch_req_opts)
|
|
|> maybe_decode_body()
|
|
|> tag_error()
|
|
|> trace_response()
|
|
end
|
|
end
|
|
|
|
defp build_request(method, url, headers, params) do
|
|
Finch.build(method, url, headers, params)
|
|
end
|
|
|
|
defp do_request(request, finch_req_opts) do
|
|
Finch.request(request, Plausible.Finch, finch_req_opts)
|
|
end
|
|
|
|
defp maybe_encode_params(params, headers) when is_binary(params) or is_nil(params) do
|
|
{params, headers}
|
|
end
|
|
|
|
defp maybe_encode_params(params, headers) when is_map(params) do
|
|
content_type =
|
|
Enum.find_value(headers, "", fn {k, v} ->
|
|
if String.downcase(k) == "content-type" do
|
|
v
|
|
end
|
|
end)
|
|
|
|
case String.downcase(content_type) do
|
|
"application/x-www-form-urlencoded" ->
|
|
{URI.encode_query(params), headers}
|
|
|
|
"application/json" ->
|
|
{Jason.encode!(params), headers}
|
|
|
|
_ ->
|
|
{Jason.encode!(params), [{"content-type", "application/json"} | headers]}
|
|
end
|
|
end
|
|
|
|
defp tag_error({:ok, %Finch.Response{status: status}} = ok)
|
|
when is_integer(status) and status >= 200 and status < 300 do
|
|
ok
|
|
end
|
|
|
|
defp tag_error({:ok, %Finch.Response{status: _} = response}) do
|
|
{:error, Non200Error.new(response)}
|
|
end
|
|
|
|
defp tag_error({:error, _} = error) do
|
|
error
|
|
end
|
|
|
|
defp maybe_decode_body({:ok, %{headers: headers, body: body} = resp})
|
|
when is_binary(body) and body != "" do
|
|
if json?(headers) do
|
|
{:ok, update_in(resp.body, &Jason.decode!/1)}
|
|
else
|
|
{:ok, resp}
|
|
end
|
|
end
|
|
|
|
defp maybe_decode_body(response), do: response
|
|
|
|
defp json?(headers) do
|
|
found =
|
|
Enum.find(headers, fn
|
|
{"content-type", "application/json" <> _} ->
|
|
true
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|
|
is_tuple(found)
|
|
end
|
|
|
|
defp trace_request(%Finch.Request{} = request) do
|
|
OpenTelemetry.Tracer.set_attributes([
|
|
{"http_client.request.host", request.host},
|
|
{"http_client.request.method", request.method},
|
|
{"http_client.request.path", request.path}
|
|
])
|
|
|
|
request
|
|
end
|
|
|
|
defp trace_response(response) do
|
|
case response do
|
|
{:ok, %{status: status}} ->
|
|
OpenTelemetry.Tracer.set_attributes([
|
|
{"http_client.response.status", status}
|
|
])
|
|
|
|
{:error, %Non200Error{reason: %{status: status}}} ->
|
|
OpenTelemetry.Tracer.set_attributes([
|
|
{"http_client.request.failure", "non_200"},
|
|
{"http_client.response.status", status}
|
|
])
|
|
|
|
{:error, exception} when is_exception(exception) ->
|
|
OpenTelemetry.Tracer.set_attributes([
|
|
{"http_client.request.failure", Exception.message(exception)}
|
|
])
|
|
|
|
_any ->
|
|
:skip
|
|
end
|
|
|
|
response
|
|
end
|
|
end
|