Packages

Run WebAssembly from Elixir. Load WASM modules in Rust, Go, C — call them like native functions.

Current section

Files

Jump to
firebird lib firebird phoenix endpoint.ex
Raw

lib/firebird/phoenix/endpoint.ex

defmodule Firebird.Phoenix.Endpoint do
@moduledoc """
WASM-accelerated Phoenix Endpoint.
Provides a full request lifecycle handler that combines routing,
plug pipeline execution, controller dispatch, and response building
into a single WASM module for high-performance request handling.
## Architecture
The endpoint handles:
1. **Conn parsing** - Parse raw HTTP requests into structured connections
2. **Pipeline execution** - Run plug pipelines (security headers, CORS, etc.)
3. **Controller dispatch** - Route matching and action execution
4. **Response building** - Render HTML/JSON responses with templates
5. **Static file serving** - Serve static assets from WASM memory
## Pipeline Plugs
Built-in plugs available for pipeline execution:
- `accept` - Content type acceptance checking
- `method_override` - Override HTTP method from `_method` parameter
- `head_to_get` - Convert HEAD requests to GET
- `put_secure_headers` - Add security headers (X-Frame-Options, etc.)
- `set_header` - Set a specific response header
- `require_header` - Require a request header (halts with 400 if missing)
- `cors` - Add CORS headers and handle preflight requests
- `put_status` - Set response status code
- `assign` - Set an assign value on the connection
## Examples
# Parse a connection
{:ok, conn} = Firebird.Phoenix.Endpoint.parse_conn(instance,
"GET /api/users HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n"
)
# => {:ok, %{status: 200, method: "GET", path: "/api/users", ...}}
# Execute a plug pipeline
{:ok, conn} = Firebird.Phoenix.Endpoint.execute_pipeline(instance,
"GET /api/users HTTP/1.1\\r\\nHost: example.com\\r\\nAccept: application/json\\r\\n\\r\\n",
["put_secure_headers", "cors:*", "accept:application/json"]
)
# Dispatch a full request
{:ok, response} = Firebird.Phoenix.Endpoint.dispatch(instance,
"GET /users/42 HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n",
[{"GET", "/users/:id", "UserController.show"}],
%{"UserController.show" => {200, "application/json", ~s({"id":"{{id}}"})}
})
"""
alias Firebird.Phoenix.FastHelper
@doc """
Parse a raw HTTP request into a structured connection map.
Returns a map representing the Phoenix.Conn-like structure.
## Examples
{:ok, conn} = Endpoint.parse_conn(instance,
"GET /users?page=1 HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n"
)
conn.method # => "GET"
conn.path # => "/users"
conn.query # => "page=1"
"""
@spec parse_conn(pid(), String.t()) :: {:ok, map()} | {:error, term()}
def parse_conn(instance, raw_request) when is_binary(raw_request) do
case FastHelper.call_string(instance, "parse_conn", raw_request) do
{:ok, "error|" <> reason} ->
{:error, reason}
{:ok, result} ->
{:ok, deserialize_conn(result)}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Execute a plug pipeline on a request.
Takes a raw HTTP request and a list of plug definitions, runs each plug
in order, and returns the resulting connection state.
## Plug Formats
- `"put_secure_headers"` - Plug with no arguments
- `"cors:*"` - Plug with colon-separated arguments
- `"set_header:x-custom:value"` - Plug with multiple arguments
- `"assign:key=value"` - Assign plug
## Returns
A map with the connection state after pipeline execution, including:
- `:status` - HTTP status code
- `:halted` - Whether the pipeline was halted
- `:assigns` - Map of assigned values
- `:resp_headers` - Map of response headers
## Examples
{:ok, conn} = Endpoint.execute_pipeline(instance,
"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n",
["put_secure_headers", "cors:*"]
)
conn.resp_headers["x-frame-options"] # => "SAMEORIGIN"
conn.resp_headers["access-control-allow-origin"] # => "*"
"""
@spec execute_pipeline(pid(), String.t(), list(String.t())) :: {:ok, map()} | {:error, term()}
def execute_pipeline(instance, raw_request, plugs)
when is_binary(raw_request) and is_list(plugs) do
plug_lines = Enum.join(plugs, "\n")
input = "#{raw_request}\n---PIPELINE---\n#{plug_lines}"
case FastHelper.call_string(instance, "execute_pipeline", input) do
{:ok, "error|" <> reason} ->
{:error, reason}
{:ok, result} ->
{:ok, deserialize_conn(result)}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Dispatch a request through routing and controller actions.
Matches the request against routes, then executes the matched controller
action to produce a response.
## Parameters
- `instance` - WASM endpoint instance PID
- `raw_request` - Raw HTTP request string
- `routes` - List of route tuples `{method, pattern, handler}`
- `actions` - Map of handler names to `{status, content_type, body_template}`
## Returns
- `{:ok, response_string}` - Full HTTP response
- `{:error, reason}` - Dispatch failed
## Examples
routes = [
{"GET", "/", "PageController.index"},
{"GET", "/users/:id", "UserController.show"},
{"POST", "/users", "UserController.create"}
]
actions = %{
"PageController.index" => {200, "text/html", "<h1>Welcome</h1>"},
"UserController.show" => {200, "application/json", ~s({"id":"{{id}}"})},
"UserController.create" => {201, "application/json", ~s({"status":"created"})}
}
{:ok, resp} = Endpoint.dispatch(instance,
"GET /users/42 HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n",
routes,
actions
)
# Response contains {"id":"42"}
"""
@spec dispatch(pid(), String.t(), list(tuple()), map()) :: {:ok, String.t()} | {:error, term()}
def dispatch(instance, raw_request, routes, actions)
when is_binary(raw_request) and is_list(routes) and is_map(actions) do
route_lines =
Enum.map(routes, fn {method, pattern, handler} ->
"#{method}|#{pattern}|#{handler}"
end)
|> Enum.join("\n")
action_lines =
Enum.map(actions, fn {handler, {status, content_type, body}} ->
"#{handler}=#{status}:#{content_type}:#{body}"
end)
|> Enum.join("\n")
input = "#{raw_request}\n---ROUTES---\n#{route_lines}\n---ACTIONS---\n#{action_lines}"
case FastHelper.call_string(instance, "dispatch_request", input) do
{:ok, response} -> {:ok, response}
{:error, reason} -> {:error, reason}
end
end
@doc """
Build a JSON API response.
## Parameters
- `instance` - WASM endpoint instance PID
- `status` - HTTP status code
- `data` - Map of key-value pairs to encode as JSON
## Returns
- `{:ok, response_string}` - Full HTTP response with JSON body
- `{:error, reason}` - Failed
## Examples
{:ok, resp} = Endpoint.json_response(instance, 200, %{
"id" => "1",
"name" => "Alice",
"active" => "true"
})
"""
@spec json_response(pid(), integer(), map()) :: {:ok, String.t()} | {:error, term()}
def json_response(instance, status, data) when is_integer(status) and is_map(data) do
lines = ["#{status}" | Enum.map(data, fn {k, v} -> "#{k}=#{v}" end)]
input = Enum.join(lines, "\n")
FastHelper.call_string(instance, "json_response", input)
end
@doc """
Render a response with a template and assigns.
## Parameters
- `instance` - WASM endpoint instance PID
- `status` - HTTP status code
- `content_type` - Response content type
- `template` - Template string with `{{var}}` placeholders
- `assigns` - Map of variable names to values
## Returns
- `{:ok, response_string}` - Full HTTP response with rendered body
- `{:error, reason}` - Failed
## Examples
{:ok, resp} = Endpoint.render_response(instance, 200, "text/html",
"<h1>Hello {{name}}</h1>",
%{"name" => "World"}
)
"""
@spec render_response(pid(), integer(), String.t(), String.t(), map()) ::
{:ok, String.t()} | {:error, term()}
def render_response(instance, status, content_type, template, assigns \\ %{})
when is_integer(status) and is_binary(content_type) and is_binary(template) and
is_map(assigns) do
assign_lines = Enum.map(assigns, fn {k, v} -> "#{k}=#{v}" end) |> Enum.join("\n")
input = "#{status}\n#{content_type}\n#{template}\n---ASSIGNS---\n#{assign_lines}"
FastHelper.call_string(instance, "render_conn", input)
end
@doc """
Serve a static file from a virtual file system.
## Parameters
- `instance` - WASM endpoint instance PID
- `path` - Request path
- `files` - Map of file paths to `{content_type, body}` tuples
## Returns
- `{:ok, response}` - HTTP response (200 with file content or 404)
- `{:error, reason}` - Failed
## Examples
files = %{
"/css/app.css" => {"text/css", "body { color: red; }"},
"/js/app.js" => {"application/javascript", "console.log('hi');"}
}
{:ok, resp} = Endpoint.serve_static(instance, "/css/app.css", files)
# => 200 with CSS content
{:ok, resp} = Endpoint.serve_static(instance, "/missing.txt", files)
# => 404
"""
@spec serve_static(pid(), String.t(), map()) :: {:ok, String.t()} | {:error, term()}
def serve_static(instance, path, files) when is_binary(path) and is_map(files) do
file_lines =
Enum.map(files, fn {file_path, {content_type, body}} ->
"#{file_path}=#{content_type}:#{body}"
end)
|> Enum.join("\n")
input = "#{path}\n---FILES---\n#{file_lines}"
FastHelper.call_string(instance, "serve_static", input)
end
@doc """
Deserialize a WASM-serialized conn string into a structured map.
The serialized format uses newline-separated `key=value` pairs with
prefixed keys for nested maps (`assign:`, `param:`, `resp_header:`, `cookie:`).
This is a public API so that `Pipeline`, `Middleware`, and custom modules
can reuse the same fast deserialization logic.
## Performance
Uses `:binary.split` and `binary_part` for zero-copy parsing on the hot path,
consistent with `Router.parse_match_result/1` and `Plug.parse_request_result/1`.
## Examples
conn = Endpoint.deserialize_conn("method=GET\\npath=/users\\nstatus=200")
conn.method # => "GET"
conn.path # => "/users"
conn.status # => 200
"""
@spec deserialize_conn(binary()) :: map()
def deserialize_conn(serialized) when is_binary(serialized) do
conn = %{
status: 200,
method: "GET",
path: "/",
query: "",
body: "",
resp_body: nil,
halted: false,
state: "unset",
assigns: %{},
params: %{},
req_headers: %{},
resp_headers: %{},
cookies: %{}
}
serialized
|> :binary.split(<<"\n">>, [:global])
|> deserialize_lines(conn)
end
# Recursive line parser using binary matching — no intermediate list allocations
defp deserialize_lines([], conn), do: conn
defp deserialize_lines([<<>> | rest], conn), do: deserialize_lines(rest, conn)
defp deserialize_lines([line | rest], conn) do
conn =
case :binary.match(line, <<"=">>) do
{pos, 1} ->
key = binary_part(line, 0, pos)
val = binary_part(line, pos + 1, byte_size(line) - pos - 1)
apply_conn_field(conn, key, val)
:nomatch ->
conn
end
deserialize_lines(rest, conn)
end
# Field dispatch using binary pattern matching for zero-allocation key comparison
defp apply_conn_field(conn, <<"status">>, val) do
%{conn | status: parse_integer_safe(val, 200)}
end
defp apply_conn_field(conn, <<"method">>, val), do: %{conn | method: val}
defp apply_conn_field(conn, <<"path">>, val), do: %{conn | path: val}
defp apply_conn_field(conn, <<"query">>, val), do: %{conn | query: val}
defp apply_conn_field(conn, <<"body">>, val), do: %{conn | body: val}
defp apply_conn_field(conn, <<"halted">>, <<"true">>), do: %{conn | halted: true}
defp apply_conn_field(conn, <<"halted">>, <<"false">>), do: %{conn | halted: false}
defp apply_conn_field(conn, <<"state">>, val), do: %{conn | state: val}
defp apply_conn_field(conn, <<"assign:", key::binary>>, val) do
%{conn | assigns: Map.put(conn.assigns, key, val)}
end
defp apply_conn_field(conn, <<"param:", key::binary>>, val) do
%{conn | params: Map.put(conn.params, key, val)}
end
defp apply_conn_field(conn, <<"resp_header:", key::binary>>, val) do
%{conn | resp_headers: Map.put(conn.resp_headers, key, val)}
end
defp apply_conn_field(conn, <<"req_header:", key::binary>>, val) do
%{conn | req_headers: Map.put(conn.req_headers, key, val)}
end
defp apply_conn_field(conn, <<"cookie:", key::binary>>, val) do
%{conn | cookies: Map.put(conn.cookies, key, val)}
end
defp apply_conn_field(conn, <<"resp_body">>, val) do
%{conn | resp_body: val}
end
defp apply_conn_field(conn, _key, _val), do: conn
# ─── Batch Operations ──────────────────────────────────────
@doc """
Batch dispatch multiple requests against the same routes and actions
in a single WASM call. Amortizes the ~42μs per-call overhead across all
requests and parses routes/actions only once in WASM.
## Parameters
- `instance` - WASM endpoint instance PID
- `requests` - List of raw HTTP request strings
- `routes` - List of route tuples `{method, pattern, handler}` (shared across all requests)
- `actions` - Map of handler names to `{status, content_type, body_template}` (shared)
## Returns
- `{:ok, [response_strings]}` - List of HTTP response strings (same order as requests)
- `{:error, reason}` - Batch dispatch failed
## Examples
routes = [
{"GET", "/users/:id", "UserController.show"},
{"POST", "/users", "UserController.create"}
]
actions = %{
"UserController.show" => {200, "application/json", ~s({"id":"{{id}}"})},
"UserController.create" => {201, "application/json", ~s({"status":"created"})}
}
requests = [
"GET /users/1 HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n",
"GET /users/2 HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n",
"POST /users HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n"
]
{:ok, responses} = Endpoint.batch_dispatch(instance, requests, routes, actions)
# => 3 HTTP response strings
"""
@spec batch_dispatch(pid(), list(String.t()), list(tuple()), map()) ::
{:ok, list(String.t())} | {:error, term()}
def batch_dispatch(instance, requests, routes, actions)
when is_list(requests) and is_list(routes) and is_map(actions) do
route_lines =
Enum.map(routes, fn {method, pattern, handler} ->
[method, "|", pattern, "|", handler]
end)
|> Enum.intersperse("\n")
action_lines =
Enum.map(actions, fn {handler, {status, content_type, body}} ->
[handler, "=", Integer.to_string(status), ":", content_type, ":", body]
end)
|> Enum.intersperse("\n")
request_data = Enum.intersperse(requests, <<0>>)
input =
IO.iodata_to_binary([
"---ROUTES---\n",
route_lines,
"\n---ACTIONS---\n",
action_lines,
"\n---REQUESTS---\n",
request_data
])
case FastHelper.call_string(instance, "batch_dispatch", input) do
{:ok, <<"error|", reason::binary>>} ->
{:error, reason}
{:ok, result} ->
{:ok, :binary.split(result, <<0>>, [:global])}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Batch execute a plug pipeline on multiple requests in a single WASM call.
The plug list is shared and parsed once; each request is processed independently.
## Parameters
- `instance` - WASM endpoint instance PID
- `requests` - List of raw HTTP request strings
- `plugs` - List of plug definitions (shared across all requests)
## Returns
- `{:ok, [conn_maps]}` - List of deserialized connection maps (same order as requests)
- `{:error, reason}` - Batch pipeline failed
## Examples
plugs = ["put_secure_headers", "cors:*"]
requests = [
"GET /api/a HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n",
"GET /api/b HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n",
"OPTIONS /api/c HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n"
]
{:ok, conns} = Endpoint.batch_execute_pipeline(instance, requests, plugs)
# => 3 conn maps, last one halted (OPTIONS preflight)
"""
@spec batch_execute_pipeline(pid(), list(String.t()), list(String.t())) ::
{:ok, list(map())} | {:error, term()}
def batch_execute_pipeline(instance, requests, plugs)
when is_list(requests) and is_list(plugs) do
plug_lines = Enum.intersperse(plugs, "\n")
request_data = Enum.intersperse(requests, <<0>>)
input =
IO.iodata_to_binary([
plug_lines,
"\n---REQUESTS---\n",
request_data
])
case FastHelper.call_string(instance, "batch_execute_pipeline", input) do
{:ok, <<"error|", reason::binary>>} ->
{:error, reason}
{:ok, result} ->
conns =
result
|> :binary.split(<<0>>, [:global])
|> Enum.map(&deserialize_conn/1)
{:ok, conns}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Batch build multiple JSON API responses in a single WASM call.
Amortizes per-call overhead and reuses the JSON encoder across all responses.
## Parameters
- `instance` - WASM endpoint instance PID
- `responses` - List of `{status, data_map}` tuples
## Returns
- `{:ok, [response_strings]}` - List of full HTTP response strings
- `{:error, reason}` - Batch build failed
## Examples
{:ok, responses} = Endpoint.batch_json_response(instance, [
{200, %{"name" => "Alice"}},
{201, %{"id" => "1", "created" => "true"}},
{404, %{"error" => "not found"}}
])
"""
@spec batch_json_response(pid(), list({integer(), map()})) ::
{:ok, list(String.t())} | {:error, term()}
def batch_json_response(instance, responses) when is_list(responses) do
blocks =
Enum.map(responses, fn {status, data} ->
lines = [Integer.to_string(status) | Enum.map(data, fn {k, v} -> [k, "=", v] end)]
Enum.intersperse(lines, "\n")
end)
input = blocks |> Enum.intersperse(<<0>>) |> IO.iodata_to_binary()
case FastHelper.call_string(instance, "batch_json_response", input) do
{:ok, <<"error|", reason::binary>>} ->
{:error, reason}
{:ok, result} ->
{:ok, :binary.split(result, <<0>>, [:global])}
{:error, reason} ->
{:error, reason}
end
end
# Safe integer parsing with fallback — avoids ArgumentError on malformed input
defp parse_integer_safe(bin, default) do
case Integer.parse(bin) do
{int, <<>>} -> int
{int, _rest} -> int
:error -> default
end
end
end