lib/github/plugin/httpoison_client.ex

if Code.ensure_loaded?(HTTPoison) do
  defmodule GitHub.Plugin.HTTPoisonClient do
    alias GitHub.Error
    alias GitHub.Operation

    @http_code_success 200..206
    @http_code_redirect [301, 302, 303, 307, 308]
    @http_code_server_error 500..511

    @spec request(Operation.t(), keyword) :: {:ok, Operation.t()} | {:error, Error.t()}
    def request(
          %Operation{
            request_body: body,
            request_headers: headers,
            request_method: method,
            request_params: params,
            request_server: server,
            request_url: url
          } = operation,
          _opts
        ) do
      url = Path.join(server, url)
      body = body || ""
      headers = modify_user_agent(headers)
      options = if params, do: [params: params], else: []

      case HTTPoison.request(method, url, body, headers, options) do
        {:ok, %HTTPoison.Response{} = response} ->
          process_response(operation, response)

        {:error, %HTTPoison.Error{} = error} ->
          message = "Error during HTTP request"
          step = {__MODULE__, :encode_body}

          {:error, Error.new(message: message, operation: operation, source: error, step: step)}
      end
    end

    @spec process_response(Operation.t(), HTTPoison.Response.t()) ::
            {:ok, Operation.t()} | {:error, Error.t()}
    defp process_response(operation, %HTTPoison.Response{status_code: code} = response)
         when code in @http_code_success do
      %HTTPoison.Response{body: body, headers: headers} = response

      operation = %Operation{
        operation
        | response_body: body,
          response_code: code,
          response_headers: headers
      }

      {:ok, operation}
    end

    defp process_response(operation, %HTTPoison.Response{status_code: code} = response)
         when code in @http_code_redirect do
      if location = get_redirect(response.headers) do
        server = URI.to_string(%URI{location | path: nil, query: nil, fragment: nil})
        url = location.path

        query =
          if location.query do
            URI.decode_query(location.query) |> Enum.to_list()
          end

        %Operation{
          operation
          | request_server: server,
            request_url: url,
            request_params: query
        }
        |> request([])
      else
        message = "Received redirect response with no Location header"
        step = {__MODULE__, :request}

        {:error,
         Error.new(
           code: code,
           message: message,
           operation: operation,
           source: response,
           step: step
         )}
      end
    end

    defp process_response(operation, %HTTPoison.Response{status_code: code} = response)
         when code in @http_code_server_error do
      message = "Received server error response (#{code})"
      step = {__MODULE__, :request}

      {:error,
       Error.new(
         code: code,
         message: message,
         operation: operation,
         source: response,
         step: step
       )}
    end

    defp process_response(operation, response) do
      %HTTPoison.Response{body: body, headers: headers, status_code: code} = response

      operation = %Operation{
        operation
        | response_body: body,
          response_code: code,
          response_headers: headers
      }

      {:ok, operation}
    end

    @spec get_redirect(HTTPoison.headers()) :: URI.t() | nil
    defp get_redirect([]), do: nil

    defp get_redirect([{header, value} | rest]) do
      if String.match?(header, ~r/^location$/i) do
        URI.parse(value)
      else
        get_redirect(rest)
      end
    end

    @spec modify_user_agent(HTTPoison.headers()) :: HTTPoison.headers()
    defp modify_user_agent(headers) do
      hackney_vsn = Application.spec(:hackney, :vsn)

      Enum.map(headers, fn {header, value} ->
        if String.match?(header, ~r/^user-agent$/) do
          {header, "#{value}; hackney #{hackney_vsn}"}
        else
          {header, value}
        end
      end)
    end
  end
end