Behaviour for handling proxy lifecycle events.
Handlers receive lifecycle events, carry state between callbacks, and can influence the proxy outcome (e.g., rejecting requests before forwarding).
State flows: initial args → handle_request_started → handle_response_started → handle_response_finished.
Usage
defmodule MyHandler do
use Philter.Handler
@impl true
def handle_request_started(metadata, state) do
# Can reject: {:reject, 413, "Too Large", state}
{:ok, state}
end
@impl true
def handle_response_finished(result, state) do
{:ok, state}
end
endCallback Order
handle_request_started/2 # Request received, before upstream call
↓
handle_response_started/2 # First byte from upstream (TTFB)
↓
handle_response_finished/2 # Response complete (or error occurred)All callbacks are synchronous. Keep them fast to avoid blocking the response stream.
handle_response_finished/2 is always called, even on error — check the :error field.
Summary
Types
Observation data for a request or response body.
Result passed to handle_response_finished/2.
Metadata passed to handle_request_started/2.
Metadata passed to handle_response_started/2.
Per-phase timing breakdown for a proxy request.
Callbacks
Called before sending the request to upstream.
Called when the response is complete (or an error occurred).
Called when the first byte is received from upstream (TTFB).
Types
@type body_observation() :: %{ hash: String.t(), size: non_neg_integer(), body: binary() | nil, preview: binary() }
Observation data for a request or response body.
:hash- SHA256 hex digest of complete body:size- Total body size in bytes:body- Full body content if accumulated, nil otherwise:preview- First 64KB of body (always present)
@type finished_result() :: %{ request_observation: body_observation(), response_observation: body_observation(), error: term() | nil, upstream_url: String.t(), method: String.t(), status: non_neg_integer() | nil, timing: timing() }
Result passed to handle_response_finished/2.
Contains observations for both request and response bodies, plus any error
that occurred during proxying. The :status field is nil when the error
occurred before receiving a response from upstream (e.g., connection refused,
pool checkout timeout).
@type request_metadata() :: %{ upstream_url: String.t(), method: String.t(), headers: [{String.t(), String.t()}], content_type: String.t() | nil, started_at: integer() }
Metadata passed to handle_request_started/2.
@type response_metadata() :: %{ status: non_neg_integer(), headers: [{String.t(), String.t()}], content_type: String.t() | nil, time_to_first_byte_us: non_neg_integer() }
Metadata passed to handle_response_started/2.
@type timing() :: %{ total_us: non_neg_integer(), queue_us: non_neg_integer() | nil, connect_us: non_neg_integer() | nil, send_us: non_neg_integer() | nil, recv_us: non_neg_integer() | nil, idle_time_us: non_neg_integer() | nil, reused_connection?: boolean() | nil }
Per-phase timing breakdown for a proxy request.
When collect_timing: true is set, phase fields are populated from HTTP
client telemetry. When timing capture is off, phase fields are nil and
reused_connection? is nil.
Callbacks
@callback handle_request_started(request_metadata(), state :: term()) :: {:ok, term()} | {:reject, status :: non_neg_integer(), body :: binary(), term()}
Called before sending the request to upstream.
Return {:ok, state} to proceed, or {:reject, status, body, state} to
abort with a handler-controlled response code and body.
@callback handle_response_finished(finished_result(), state :: term()) :: {:ok, term()}
Called when the response is complete (or an error occurred).
Always called, even on error. Check :error field for failures.
@callback handle_response_started(response_metadata(), state :: term()) :: {:ok, term()}
Called when the first byte is received from upstream (TTFB).