Streaming HTTP proxy library with O(1) memory body observation for Elixir.
Philter — an alchemical potion or charm; from Greek philtron (φίλτρον), "love potion."
- Zero buffering: Stream requests and responses without memory accumulation
- Body observation: Capture SHA256, size, preview, timing without buffering
- Plug integration: Use as Plug or call directly from controllers
- Configurable: Per-request overrides for all settings
- Observable: Lifecycle callbacks for monitoring and logging
Add philter to your list of dependencies in mix.exs:
def deps do [ {:philter, "~> 0.1.0"} ] end
- Add Finch to your supervision tree:
children = [ {Finch, name: MyApp.Finch} ]
- Configure Philter:
# config/config.exs config :philter, finch_name: MyApp.Finch
- Use in your controller:
def proxy(conn, _params) do Philter.proxy(conn, upstream: "https://api.example.com") end
Or as a Plug in your router:
forward "/api", Philter.ProxyPlug, upstream: "https://api.example.com"
Philter captures observations about request and response bodies without buffering:
conn = Philter.proxy(conn, upstream: "https://api.example.com") # Access observations from conn.private req_obs = conn.private[:philter_request_observation] resp_obs = conn.private[:philter_response_observation] # Each observation contains: # - :hash - SHA256 hash of the body # - :size - Total body size in bytes # - :preview - First 64KB of the body (UTF-8 safe truncation) # - :body - Full body (only if under max_payload_size and content-type matches)
Implement Philter.Handler to hook into the proxy lifecycle:
defmodule MyApp.ProxyHandler do use Philter.Handler @impl true def handle_request_started(metadata, state) do Logger.info("Proxying #{metadata.method} #{metadata.upstream_url}") {:ok, state} end @impl true def handle_response_started(metadata, state) do Logger.info("TTFB: #{metadata.time_to_first_byte_us}us") {:ok, state} end @impl true def handle_response_finished(result, state) do Logger.info("Completed: #{result.status} in #{result.timing.total_us}us") # result contains :request_observation and :response_observation {:ok, state} end end # Use it: Philter.proxy(conn, upstream: "https://api.example.com", handler: {MyApp.ProxyHandler, %{}} )
| Option | Default | Description |
|---|---|---|
:finch_name |
Philter.Finch |
Name of the Finch pool to use |
:receive_timeout |
15_000 |
Response timeout in milliseconds |
:max_payload_size |
1_048_576 |
Max body size for full accumulation (1MB) |
:persistable_content_types |
JSON/XML/text | Content types eligible for body storage |
Override per-request:
Philter.proxy(conn, upstream: "https://api.example.com", receive_timeout: 60_000, max_payload_size: 5_242_880 )
Or set application defaults:
# config/config.exs config :philter, finch_name: MyApp.Finch, receive_timeout: 30_000, max_payload_size: 5_242_880, persistable_content_types: ["application/json", "text/*"]
Full documentation: https://hexdocs.pm/philter
Apache-2.0