Skip to main content

src/masque_errors.erl

%%% @doc RFC 9298 error to HTTP status code mapping.
%%%
%%% All MASQUE failures reported during the CONNECT-UDP handshake must
%%% be returned to the client as an HTTP status response. This module
%%% centralises the mapping so server code and tests agree on the exact
%%% status code for each failure mode.
-module(masque_errors).

-export([handshake_status/1, status_reason/1]).

-include("masque.hrl").

-type handshake_error() ::
    bad_method
  | bad_protocol
  | bad_path
  | bad_port
  | bad_host
  | resolution_failed
  | upstream_timeout
  | forbidden
  | loop_detected
  | overload
  | {other, 400..599}.

-export_type([handshake_error/0]).

%% @doc Map a handshake error to the status code MASQUE should return.
-spec handshake_status(handshake_error()) -> 400..599.
handshake_status(bad_method)         -> ?MASQUE_STATUS_METHOD_NOT_ALLOWED;
handshake_status(bad_protocol)       -> ?MASQUE_STATUS_NOT_IMPLEMENTED;
handshake_status(bad_path)           -> ?MASQUE_STATUS_NOT_FOUND;
handshake_status(bad_port)           -> ?MASQUE_STATUS_BAD_REQUEST;
handshake_status(bad_host)           -> ?MASQUE_STATUS_BAD_REQUEST;
handshake_status(resolution_failed)  -> ?MASQUE_STATUS_BAD_GATEWAY;
handshake_status(upstream_timeout)   -> ?MASQUE_STATUS_GATEWAY_TIMEOUT;
handshake_status(forbidden)          -> 403;
handshake_status(loop_detected)      -> ?MASQUE_STATUS_LOOP_DETECTED;
handshake_status(overload)           -> 503;
handshake_status({other, Status}) when Status >= 400, Status =< 599 -> Status.

%% @doc Short human-readable reason-phrase for the given error.
-spec status_reason(handshake_error()) -> binary().
status_reason(bad_method)         -> <<"method must be CONNECT">>;
status_reason(bad_protocol)       -> <<":protocol must be connect-udp">>;
status_reason(bad_path)           -> <<"path does not match template">>;
status_reason(bad_port)           -> <<"target_port out of range">>;
status_reason(bad_host)           -> <<"target_host empty or malformed">>;
status_reason(resolution_failed)  -> <<"could not resolve target">>;
status_reason(upstream_timeout)   -> <<"target did not respond in time">>;
status_reason(forbidden)          -> <<"target denied by policy">>;
status_reason(loop_detected)      -> <<"proxy loop detected">>;
status_reason(overload)           -> <<"proxy overloaded">>;
status_reason({other, _})         -> <<"request rejected">>.