Current section

Files

Jump to
indieweb lib indieweb http.ex
Raw

lib/indieweb/http.ex

defmodule IndieWeb.Http do
@default_retry_time 10
defmodule Error do
@moduledoc "Defines an error obtained when making a network request."
@enforce_keys ~w(message)a
@typedoc "Representative type of errors with `IndieWeb.Http`."
@type t :: %__MODULE__{message: any(), raw: any()}
defstruct ~w(message raw)a
end
defmodule Response do
@moduledoc "Defines a response obtained when making a network request."
@enforce_keys ~w(code body url headers raw)a
defstruct ~w(body code url headers rels raw)a
@type t :: %__MODULE__{
body: String.t(),
code: non_neg_integer,
url: String.t(),
headers: Access.t(),
rels: map(),
raw: any()
}
end
defmodule Client do
@moduledoc false
use Tesla
adapter(Tesla.Adapter.Hackney, timeout: 30_000)
plug(Tesla.Middleware.Logger)
plug(Tesla.Middleware.FollowRedirects)
plug(Tesla.Middleware.KeepRequest)
plug(Tesla.Middleware.MethodOverride)
plug(Tesla.Middleware.DecodeRels)
plug(Tesla.Middleware.Headers, [
{"user-agent", IndieWeb.Http.user_agent()}
])
end
def user_agent() do
version = Application.spec(:indieweb, :vsn)
"IndieWeb-Elixir/#{version} (https://git.jacky.wtf/indieweb/elixir/tree/v#{version})"
end
def make_absolute_uri(path, _) when path in ["", nil] do
path
end
def make_absolute_uri(path, base_uri)
when path == base_uri and is_binary(path) do
path
end
def make_absolute_uri(path, base_uri) when is_binary(path) do
base_uri |> URI.merge(path) |> URI.to_string()
end
def extract_link_header_values(%__MODULE__.Response{} = resp) do
resp
|> Map.get(:raw, %{})
|> Map.get(:opts, [])
|> Keyword.get(:rels, %{}) || %{}
end
def request(url, method \\ :get, opts \\ [])
def request(url, method, opts) when is_binary(url) do
case __MODULE__.Client.request([url: url, method: method] ++ opts) do
{:ok, %Tesla.Env{} = env} ->
{:ok,
%__MODULE__.Response{
raw: env,
code: env.status,
body: env.body,
rels: env.opts[:rels] || %{},
headers: env.headers,
url: env.url
}}
{:error, error} ->
{:error, %__MODULE__.Error{message: error, raw: nil}}
end
end
def request(_url, _method, _opts), do: {:error, :no_url_provided}
for method <- ~w(get post options head put patch delete)a do
@doc """
Sends a #{String.upcase(Atom.to_string(method))} request to the specified URL.
See `request/3` for more information about making requests.
"""
def unquote(method)(url, opts \\ []),
do: __MODULE__.request(url, unquote(method), opts)
end
def post_encoded(url, opts) do
post(
url,
opts ++
[
body: URI.encode_query(opts[:body]),
headers:
Keyword.get(opts, :headers, []) ++
[{"Content-Type", "application/x-www-form-urlencoded"}]
]
)
end
# TODO: Check the docs around 'Retry-After' to determine the possible formats.
@doc "Determines how many seconds from now does one have to wait to retry a request."
@spec extract_retry_time(IndieWeb.Http.Response.t()) :: integer()
def extract_retry_time(resp) do
resp.headers
|> Enum.find_value(0, fn
{"Retry-After", count} ->
if String.contains?(count, ":") do
case Calendar.DateTime.Parse.httpdate!(count) do
{:ok, retry_dt} ->
DateTime.diff(retry_dt, DateTime.utc_now(), :second)
_ ->
@default_retry_time
end
else
case Integer.parse(count) do
{seconds, _} -> seconds
_ -> @default_retry_time
end
end
_ ->
nil
end)
end
end
defimpl String.Chars, for: IndieWeb.Http.Response do
def to_string(resp) do
Jason.encode!(%{
"body" => resp.body,
"headers" => Map.new(resp.headers),
"url" => resp.url,
"code" => resp.code,
"rels" => resp.rels
})
end
end