Philter.Handler behaviour (Philter v0.3.0)

Copy Markdown View Source

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
end

Callback 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

body_observation()

@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)

finished_result()

@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).

request_metadata()

@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.

response_metadata()

@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.

timing()

@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

handle_request_started(request_metadata, state)

(optional)
@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.

handle_response_finished(finished_result, state)

@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.

handle_response_started(response_metadata, state)

(optional)
@callback handle_response_started(response_metadata(), state :: term()) :: {:ok, term()}

Called when the first byte is received from upstream (TTFB).