Current section
Files
Jump to
Current section
Files
lib/dspace/api/error.ex
defmodule DSpace.API.Error do
@moduledoc """
Represents an API Error.
"""
import DSpace.Utils, only: [is_nonempty_binary: 1]
alias DSpace.API.HTTP.Response
# Expected error codes per DSpace API contract
@expected_client_errors %{
400 => :bad_request,
401 => :unauthorized,
403 => :forbidden,
404 => :not_found,
405 => :method_not_allowed,
422 => :unprocessable_entity,
429 => :too_many_requests
}
defexception [:type, :status, :message, :request_url, :response]
@type t :: %__MODULE__{
type: error_type(),
status: non_neg_integer(),
message: binary(),
request_url: URI.t(),
response: Response.t()
}
@type error_type ::
:bad_request
| :unauthorized
| :forbidden
| :not_found
| :method_not_allowed
| :unprocessable_entity
| :too_many_requests
| :server_error
| :api_csrf_invalid
| :api_unexpected_response
| :api_unexpected_payload
| :api_unexpected_client_error
# Public API
@doc """
Builds an error from a response structure based on the HTTP status code.
"""
@spec from_response(Response.t()) :: t()
def from_response(%Response{status: 403, body: body} = response) do
{type, message} = maybe_csrf_error(body)
exception(type: type, status: 403, message: message, response: response)
end
def from_response(%Response{status: status, body: body} = response) when status in 400..499 do
type = @expected_client_errors[status] || :api_unexpected_client_error
message = extract_message(body) || format_type(type)
exception(type: type, status: status, message: message, response: response)
end
def from_response(%Response{status: status, body: body} = response) when status >= 500 do
message = extract_message(body) || format_type(:server_error)
exception(type: :server_error, status: status, message: message, response: response)
end
def from_response(%Response{status: status, body: body} = response) do
message = extract_message(body) || format_type(:api_unexpected_response)
exception(type: :api_unexpected_response, status: status, message: message, response: response)
end
@doc """
Builds a validation error from a response structure.
"""
@spec response_validation_error(Response.t(), binary()) :: t()
def response_validation_error(
%Response{status: status} = response,
message \\ "API response data invalid for expected schema."
)
when is_nonempty_binary(message) do
exception(type: :api_unexpected_payload, status: status, message: message, response: response)
end
# Callbacks
@spec exception(keyword() | binary()) :: t()
@impl true
def exception(attributes) when is_list(attributes) do
attributes = Keyword.put_new(attributes, :request_url, attributes[:response].request_url)
struct(__MODULE__, attributes)
end
# Private helpers
defp extract_message(%{"message" => message}) when is_nonempty_binary(message), do: message
defp extract_message(_), do: nil
defp maybe_csrf_error(body) do
message = extract_message(body)
cond do
is_nonempty_binary(message) and String.contains?(message, "CSRF token") ->
{:api_csrf_invalid, message}
is_nonempty_binary(message) ->
{:forbidden, message}
true ->
{:forbidden, "Forbidden"}
end
end
defp format_type(type) when is_atom(type) do
type
|> Atom.to_string()
|> String.replace("_", " ")
|> String.capitalize()
end
end