Actions¶
Actions define the behavior executed when a route matches. Actions can be referenced by name or inlined directly in a route.
proxy — Reverse Proxy¶
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | ✓ | "proxy" |
upstream |
string | ✓ | "host:port", "http://host:port", or template |
rewrite |
string | Replace the incoming request path entirely | |
timeout |
string | "5s", "30s", "1m" |
|
headers |
object | Extra headers to send to upstream | |
fallback |
string | Named action to invoke when the primary action fails | |
proto |
string | Upstream protocol: "h2" for HTTP/2 cleartext (h2c) |
|
stream |
bool | Use raw HTTP/1.1 tunnel for bidirectional streaming |
The upstream field supports template placeholders: {target} (from the route's balancer) and any key from the route's set field. For example, "{target}:{port}" resolves both the balancer target and a route-level variable.
The fallback action is invoked when no balancer target is available or the upstream is unreachable, enabling graceful degradation without returning 502.
Path rewrite (rewrite)¶
When rewrite is set, the incoming request path is replaced entirely with the specified value before forwarding to the upstream. The original path is discarded — no concatenation with the upstream path occurs.
{
match: { path: "/webhook" },
action: {
type: "proxy",
upstream: "https://backend.internal",
rewrite: "/api/v2/hook",
},
}
A request to /webhook is forwarded to https://backend.internal/api/v2/hook.
Upstream protocol (proto)¶
Controls the HTTP protocol version used to communicate with the upstream server.
| Value | Description |
|---|---|
| (empty) | HTTP/1.1 (default) |
"h2" |
HTTP/2 cleartext (h2c) — HTTP/2 over plain TCP, no TLS |
HTTP/2 enables full-duplex streaming: the client can upload data while simultaneously receiving a response. This is required for protocols that use long-lived POST/GET pairs for bidirectional communication.
Tip
Combine proto: "h2" with a service-level config that increases timeouts and enables immediate flushing for streaming workloads.
// HTTP/2 upstream — full-duplex streaming support.
{
match: { domain: "*.**" },
action: {
type: "proxy",
upstream: "localhost:3501",
proto: "h2",
},
}
Streaming mode (stream)¶
When stream is true, the proxy bypasses httputil.ReverseProxy and establishes a raw HTTP/1.1 tunnel. The request is forwarded over a raw TCP connection with the body streamed in a background goroutine, and the response is flushed immediately.
Note
Prefer proto: "h2" for bidirectional streaming when the upstream supports HTTP/2. Use stream: true only for HTTP/1.1 upstreams that require raw tunnel behavior.
// Route with variables and shared action.
{
match: { domain: "*.**", path: "/ws" },
plugins: ["./plugins/resolver"],
balancer: { type: "leastconn" },
set: { port: "8080" },
action: "dynamic_proxy",
},
// Shared action definition.
actions: {
dynamic_proxy: {
type: "proxy",
upstream: "{target}:{port}",
fallback: "default_backend",
},
default_backend: {
type: "proxy",
upstream: "https://fallback.internal",
},
}
Headers¶
Headers are injected into every request forwarded to the upstream. This applies to custom Host headers, authentication tokens, or any other headers required by the upstream.
Note
prox automatically sets X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto on every proxied request. You do not need to add these manually.
{
match: { domain: "*.**" },
action: {
type: "proxy",
upstream: "https://backend.internal",
headers: {
Host: "public.example.com",
"X-Forwarded-Proto": "https",
},
},
}
WebSocket support¶
WebSocket connections are detected and handled automatically — no additional configuration is required. When a client sends an Upgrade: websocket request, prox:
- Dials the upstream directly via TCP
- Forwards the full HTTP upgrade handshake (including all configured
headers) - Establishes a bidirectional tunnel after the
101 Switching Protocolsresponse - Relays raw bytes transparently until either side closes
This is compatible with any WebSocket library or protocol (RFC 6455). The proxy does not interpret WebSocket frames — raw bytes are relayed transparently. The timeout setting applies to the initial upstream dial.
// WebSocket-capable proxy — no extra config needed.
{
match: { domain: "ws.example.com", path: "/ws/*" },
action: {
type: "proxy",
upstream: "localhost:8080",
timeout: "10s",
},
}
If the upstream rejects the upgrade (e.g., returns 403), the rejection response is forwarded to the client as-is.
Note
On TLS services, HTTP/2 is enabled by default. For WebSocket support on a TLS service, set h2: false to force HTTP/1.1 — Go's HTTP/2 strips the Connection and Upgrade headers required for WebSocket detection.
static — Static Response¶
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | ✓ | "static" |
status |
int | ✓ | HTTP status code |
headers |
object | Response headers | |
body_ref |
string / object | Ref to resource or inline { text: "..." } / { json: {...} } |
Template variables¶
Static response bodies support {variable} placeholders that are interpolated at request time:
| Variable | Description | Example |
|---|---|---|
{domain} |
Actual request host (no port) | sub.example.com |
{domain.pattern} |
Domain pattern from config | *.example.com |
{match.domain} |
Captured * wildcard value(s) |
sub |
{match.glob} |
Captured ** glob suffix |
example.com |
{path} |
Actual request path | /api/users |
{match.path} |
Path pattern from config | /api/* |
{method} |
HTTP method | GET |
{host} |
Full Host header (with port) | sub.example.com:443 |
For multiple * wildcards, captured values are joined with . — e.g., pattern *.*.example.com matching a.b.example.com produces {match.domain} = a.b. The ** glob suffix is captured separately into {match.glob} — e.g., pattern *.storage.** matching cdn.storage.example.com produces {match.domain} = cdn and {match.glob} = example.com.
{
match: { domain: "test.*.example.com" },
action: {
type: "static",
status: 200,
headers: { "Content-Type": "text/plain" },
body_ref: { text: "Env: {match.domain}, full host: {domain}" },
},
}
// GET http://test.staging.example.com/ → "Env: staging, full host: test.staging.example.com"
Bodies without { are served as-is with no interpolation overhead.
serve — File Server¶
Serves files from a directory or a single file.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | ✓ | "serve" |
root |
string | ✗† | Directory to serve (e.g. "./public") |
file |
string | ✗† | Single file to serve (e.g. "./app.html") |
† Exactly one of root or file is required.
Directory mode (root):
- Automatically serves
index.htmlfor directory requests GET /→root/index.htmlGET /css/app.css→root/css/app.css- Directory listings are disabled (404 if no
index.html) - Route prefix is stripped automatically: route
/static/*with root./publicmaps/static/app.css→./public/app.css
File mode (file):
- Always serves the same file regardless of the request path
- Useful for SPA fallbacks
// Directory serving
{
match: { path: "/*" },
action: {
type: "serve",
root: "./public",
},
}
// Single file
{
match: { path: "/app/*" },
action: {
type: "serve",
file: "./dist/index.html", // SPA fallback
},
}
pass — L4 TCP Pass-through¶
Relays raw TCP connections to an upstream without TLS termination. The proxy peeks the TLS ClientHello to extract the SNI hostname for routing, then forwards all bytes (including the ClientHello) to the upstream. The upstream handles TLS directly.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | ✓ | "pass" |
upstream |
string | ✓ | "host:port" — TCP dial address |
Constraints:
passroutes must have adomainpattern (SNI matching)passroutes cannot usepathormethods(these are HTTP-level concepts unavailable before TLS termination)
See L4 Dispatching for details on how pass routes interact with L7 routes.
drop — Drop Connection¶
Silently closes the connection without sending a response. Useful as a catch-all to reject unknown domains or unwanted traffic.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | ✓ | "drop" |
At L7 (HTTP), the TCP connection is hijacked and closed immediately — no HTTP response is sent. At L4 (when combined with pass routes), the raw TCP connection is closed before the TLS handshake.
Resources¶
Named, reusable content blobs referenced by actions via body_ref.
| Field | Type | Description |
|---|---|---|
text |
string | Raw text content |
json |
any | JSON value — auto-marshaled to a JSON string |
file |
string | File path to read content from |
Use text for plain strings, json for structured data (avoids manual escaping), and file to load content from disk.
{
resources: {
greeting: {
text: "Hello, World!",
},
health: {
json: { status: "ok", version: "1.0" },
},
},
}
Inline resources follow the same format: