defmodule HTTPX do @moduledoc ~S""" Simple HTTP(s) client with integrated auth methods. """ alias HTTPX.Response @default_auth [ basic: HTTPX.Auth.Basic ] @auth_methods Application.get_env(:httpx, :auth_extensions, @default_auth) @default_settings [ ssl_options: [versions: [:'tlsv1.2']], pool: :default, connect_timeout: 5_000, recv_timeout: 15_000, ] @post_header_urlencoded {"Content-Type", "application/x-www-form-urlencoded"} @dialyzer { [:no_return, :no_match, :nowarn_function], get: 1, get: 2, request: 2, request: 3, } @doc ~S""" Performs a get request. For options see: `&request/3`. """ @spec get(String.t, keyword) :: {:ok, Response.t} | {:error, term} def get(url, options \\ []), do: :get |> request(url, options) @doc ~S""" Performs a post request, passing the body in the options. For options see: `&request/3`. """ @spec post(String.t, {:urlencoded, String.t} | String.t, keyword) :: {:ok, Response.t} | {:error, term} def post(url, body, options \\ []) def post(url, {:urlencoded, body}, options) do body = URI.encode_query(body) options = options |> Keyword.update(:headers, [], &([@post_header_urlencoded | &1])) post(url, body, options) end # Default def post(url, body, options), do: :post |> request(url, Keyword.put(options, :body, body)) @doc ~S""" Performs a request. The given `method` is used and the `url` is called. The following options can be set: * `:body`, the body to send with the request. * `:params`, a map containing query params. * `:headers`, list of header tuples. * `:settings`, options to pass along to `:hackney`. * `:fail`, will error out any request with a non 2xx response code, when set to true. * `:auth`, set authorization options. * `:format`, set to parse. (Like `:json`) """ @spec request(term, String.t, keyword) :: {:ok, Response.t} | {:error, term} def request(method, url, options \\ []) do full_url = generate_url(url, options) headers = options[:headers] || [] body = options[:body] || "" hackney_settings = @default_settings |> Keyword.merge(options[:settings] || []) |> Kernel.++([:with_body]) headers = case @auth_methods[options[:auth]] do nil -> headers method -> # 💖 Pipes method |> method.auth(full_url, headers, body, options) |> Kernel.++(headers) end method |> :hackney.request(full_url, headers, body, hackney_settings) |> parse_response(options[:format] || :text) |> handle_response(:fail in options) end ### Helpers ### defp generate_url(url, options) do full_url = cond do not Keyword.has_key?(options, :params) -> url URI.parse(url).query -> url <> "&" <> URI.encode_query(options[:params] || %{}) true -> url <> "?" <> URI.encode_query(options[:params] || %{}) end full_url |> to_string |> default_process_url end defp parse_response({:ok, status, resp_headers, resp_body}, format) do with {:ok, body} <- parse_body(resp_body, format) do response = %Response{ status: status, headers: resp_headers, body: body, } {:ok, response} else error -> error end end defp parse_response({:ok, status, resp_headers}, format) do parse_response({:ok, status, resp_headers, ""}, format) end defp parse_response(error, _format) do error end defp parse_body(body, format) defp parse_body(body, :text), do: {:ok, body} defp parse_body(body, :json), do: body |> Poison.decode defp parse_body(body, :json_atoms), do: body |> Poison.decode(keys: :atoms) defp parse_body(body, :json_atoms!), do: body |> Poison.decode(keys: :atoms!) defp handle_response({:ok, %{status: status}}, true) when status < 200 or status >= 300, do: {:error, :http_status_failure} defp handle_response(response, _), do: response defp default_process_url(url) do case url |> String.slice(0, 12) |> String.downcase do "http://" <> _ -> url "https://" <> _ -> url "http+unix://" <> _ -> url _ -> "http://" <> url end end ## Bangified ### @doc ~S""" Performs a get request. For options see: `&get/2`. """ @spec get!(String.t, keyword) :: Response.t def get!(url, options \\ []) do case request(:get, url, options) do {:ok, response} -> response {:error, reason} -> raise RuntimeError, to_string(reason) end end @doc ~S""" Performs a post request, passing the body in the options. For options see: `&post/3`. """ @spec post!(String.t, String.t, keyword) :: Response.t def post!(url, body, options \\ []) do case request(:post, url, Keyword.put(options, :body, body)) do {:ok, response} -> response {:error, reason} -> raise RuntimeError, to_string(reason) end end end