defmodule Auth0.Common.Management.Http do @moduledoc """ Documentation for Auth0 Management HTTP. This module manages http request for Auth0 Management API. """ alias Auth0.Config alias Auth0.Common.Management.TokenManager @type endpoint :: String.t() @type body :: map | list @type multipart :: {:multipart, list} @type config :: Config.t() @type response :: {:ok, integer, String.t()} | {:error, integer, term} | {:error, term} @type method :: :get | :post | :put | :patch | :delete | :options | :head @type headers :: map @type raw_response :: {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t() | HTTPoison.MaybeRedirect.t()} | {:error, HTTPoison.Error.t()} @request_headers %{"Content-Type" => "application/json"} @post_headers %{"Content-Type" => "application/json"} @patch_headers %{"Content-Type" => "application/json"} @put_headers %{"Content-Type" => "application/json"} @get_headers %{} @delete_headers %{} @multipart_post_headers %{"Content-Type" => "multipart/form-data"} @max_request_retry_count 10 @max_request_retry_jitter 100 @max_request_retry_delay 1000 @min_request_retry_delay 100 @doc """ Request Auth0 management rest api simply. """ @spec raw_request(method, endpoint, body, headers, config) :: raw_response def raw_request(method, endpoint, body \\ %{}, headers \\ nil, %Config{} = config \\ %Config{}) do headers = if headers |> is_nil do case method do :get -> @get_headers :post -> @post_headers :put -> @put_headers :patch -> @patch_headers :delete -> @delete_headers end else headers end |> set_correlation_id(config) |> set_authorization(config) url = endpoint |> get_url(config) HTTPoison.request(method, url, body |> Jason.encode!(), headers) end @doc """ POST Auth0 management rest api. """ @spec post(endpoint, body, config) :: response def post(endpoint, body, %Config{} = config) do headers = @post_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry( fn url -> HTTPoison.post(url, body |> Jason.encode!(), headers) end, endpoint, config ) end @doc """ PATCH Auth0 management rest api. """ @spec patch(endpoint, body, config) :: response def patch(endpoint, body, %Config{} = config) do headers = @patch_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry( fn url -> HTTPoison.patch(url, body |> Jason.encode!(), headers) end, endpoint, config ) end @doc """ PUT Auth0 management rest api. """ @spec put(endpoint, body, config) :: response def put(endpoint, body, %Config{} = config) do headers = @put_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry( fn url -> HTTPoison.put(url, body |> Jason.encode!(), headers) end, endpoint, config ) end @doc """ GET Auth0 management rest api. """ @spec get(endpoint, config) :: response def get(endpoint, %Config{} = config) do headers = @get_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry(fn url -> HTTPoison.get(url, headers) end, endpoint, config) end @doc """ DELETE Auth0 management rest api. """ @spec delete(endpoint, config) :: response def delete(endpoint, %Config{} = config) do headers = @delete_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry(fn url -> HTTPoison.delete(url, headers) end, endpoint, config) end @doc """ DELETE Auth0 management rest api. (with body) """ @spec delete(endpoint, body, config) :: response def delete(endpoint, body, %Config{} = config) do headers = @request_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry( fn url -> HTTPoison.request(:delete, url, body |> Jason.encode!(), headers) end, endpoint, config ) end @doc """ MULTIPART POST Auth0 management rest api. """ @spec multipart_post(endpoint, multipart, config) :: response def multipart_post(endpoint, multipart, %Config{} = config) do headers = @multipart_post_headers |> set_correlation_id(config) |> set_authorization(config) request_with_retry( fn url -> HTTPoison.post(url, multipart, headers) |> IO.inspect() end, endpoint, config ) end defp request_with_retry(request_func, endpoint, %Config{} = config, retry_count \\ 0) do url = endpoint |> get_url(config) max_request_retry_count = config |> get_max_request_retry_count case request_func.(url) do {:ok, %HTTPoison.Response{status_code: 429}} when max_request_retry_count > retry_count -> retry_count = retry_count + 1 exponential_delay(retry_count) request_with_retry(request_func, endpoint, config, retry_count) {:ok, %HTTPoison.Response{status_code: status_code, body: body}} when status_code in [200, 201, 202, 204] -> {:ok, status_code, body} {:ok, %HTTPoison.Response{status_code: 302, headers: headers}} -> location = Enum.filter(headers, fn {key, _} -> key |> String.match?(~r/\Alocation\z/i) end) |> Enum.map(fn {_, value} -> value end) |> List.first() {:ok, 302, location} {:ok, %HTTPoison.Response{status_code: status_code, body: body}} -> {:error, status_code, body} {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason} end end defp get_url(endpoint, %Config{} = config) do "https://#{Config.get_domain(config)}#{endpoint}" end defp set_correlation_id(headers, %Config{correlation_id: nil}), do: headers defp set_correlation_id(headers, %Config{correlation_id: correlation_id}), do: headers |> Map.put("x-correlation-id", correlation_id) defp set_authorization(headers, %Config{} = config) do case config |> Config.get_api_token() do nil -> case config |> TokenManager.get_token() do nil -> headers access_token -> headers |> Map.put("Authorization", "Bearer #{access_token}") end api_token -> headers |> Map.put("Authorization", "Bearer #{api_token}") end end defp get_max_request_retry_count(%Config{} = config), do: min(Config.get_max_request_retry_count(config), @max_request_retry_count) defp exponential_delay(retry_count) do request_retry_jitter = Enum.random(0..@max_request_retry_jitter) max( @min_request_retry_delay, min(@max_request_retry_delay, 100 * Integer.pow(2, retry_count - 1) + request_retry_jitter) ) |> Process.sleep() end end