Rule-Based Routing
Configure rule-based routing for GoDoxy
Experimental Feature
This feature is experimental and not fully tested.
Everything, including syntax, may change until it is considered complete and stable.
Overview
Rule-based routing allows you to define custom routing logic for incoming requests. Rules can match requests based on various conditions (headers, query parameters, cookies, etc.) and execute specific actions (serve files, dispatch internal handlers, proxy requests, redirect, etc.).
Rule Structure
GoDoxy supports both YAML and block syntax.
Recommended Syntax
Block syntax is the recommended syntax.
Block DSL At A Glance
Each block is one rule. There are 3 rule forms:
# Default rule (fallback)
default {
remove resp_header X-Internal
}
# Conditional rule
path glob("/api/*") {
proxy http://api:8080
}
# Unconditional rule (always matches)
{
log info /dev/stdout "$req_method $req_path"
}Equivalent YAML mapping:
- block header condition ->
on: ... - block body commands ->
do: ... default { ... }-> YAML rule withname: defaultoron: default
Rule grammar (simplified):
rule := "default" "{" do_body "}"
| on_expr "{" do_body "}"
| "{" do_body "}"Block Syntax
default {
remove resp_header X-Internal
}
path glob("/api/*") {
proxy http://api:8080
}Commands inside a rule body can use either inline positional arguments or named option blocks:
path /api {
# Inline positional form
rewrite /api /backend
# Equivalent named-option form
rewrite {
from: /api
to: /backend
}
}YAML Compatibility (legacy)
- name: default
do: remove resp_header X-Internal
- on: path glob("/api/*")
do: proxy http://api:8080Rule Behavior
- Rules are processed in pre and post phases.
- A default rule (
name: defaultoron: default) is a fallback rule and runs only when no non-default pre rule matches. - Pre-phase terminating commands stop remaining pre commands.
- Post-only commands from already-matched rules can still run in post phase.
- Response-based matchers (
status,resp_header) are evaluated in post phase. - If an earlier rule always terminates for the same matcher, later equivalent rules are rejected as dead rules.
Nested Block Syntax
Inside a rule body, a { ... } block can mean either a command option block or
a nested conditional block:
command { key: value }is a named option block whencommandis an action and the body is a YAML mapping.<condition> { ... }is a nested conditional block.
You can nest conditional blocks with elif and else.
Syntax
path /example {
set header X-Mode outer
method GET {
set header X-Mode get
} elif method POST {
set header X-Mode post
} else {
set header X-Mode other
}
}Chain grammar (simplified):
nested_block := on_expr "{" do_body "}"
chain := nested_block { "elif" on_expr "{" do_body "}" } [ "else" "{" do_body "}" ]Notes
<condition> { ... }runs nested commands only when the condition matches.- Nested blocks are recognized when a logical header ends with an unquoted
{. - Action option blocks do not accept inline arguments. Use
notify { ... }, notnotify info ntfy { ... }. - Logical headers can continue to the next line when the current line ends with
|or&. elifandelsemust be on the same line as the preceding}.- Multiple
elifbranches are allowed; only oneelseis allowed. - A nested block or
elif/elsechain runs entirely in post phase if any matcher or action requires response data. Keep request mutations in separate pre-phase blocks.require_auth,require_basic_auth, and request middleware cannot run in post phase.
Syntax Reference
Block Delimiters And Comments
- Structural braces (
{and}) are parsed only outside quotes/comments. - Supported comments:
// line comment# line comment/* block comment */
Quoting and Escaping
Like in Linux shell, values containing spaces and quotes should be quoted or escaped:
header Some-Header "foo bar" # Double quotes
header Some-Header 'foo bar' # Single quotes
header Some-Header foo\ bar # Escaped space
header Some-Header 'foo \"bar\"' # Escaped quotes inside single quotesSupported Escape Sequences
| Sequence | Description | Context |
|---|---|---|
\n | New line | String values, regex patterns |
\t | Tab | String values, regex patterns |
\r | Carriage return | String values, regex patterns |
\\ | Backslash | String values, regex patterns |
\" | Double quote | String values |
\' | Single quote | String values |
\ | Space | String values |
$$ | Dollar sign (literal) | Any |
\b | Word boundary | Regex patterns only |
\B | Non-word boundary | Regex patterns only |
\s | Whitespace character | Regex patterns only |
\S | Non-whitespace character | Regex patterns only |
\w | Word character | Regex patterns only |
\W | Non-word character | Regex patterns only |
\d | Digit character | Regex patterns only |
\D | Non-digit character | Regex patterns only |
\$ | Dollar sign (literal) | Regex patterns only |
\. | Dot (literal) | Regex patterns only |
Environment Variable Substitution
Environment variables can be substituted using ${VARIABLE_NAME} syntax:
error 503 "${SERVICE_NAME} is unavailable"
proxy https://${DOMAIN}/api
set header X-Tenant "tenant-${DOMAIN}"Rule expressions own environment expansion, including inline rules in YAML or
JSON configuration. Variables are expanded once while parsing the command; their
values are not reparsed as rule syntax. To keep a literal ${...}, use $$:
error 404 "$${NON_EXISTENT}" # Results in literal "${NON_EXISTENT}"Docker Compose adds an earlier interpolation layer. Double each $ in Compose
labels so the rule expression reaches GoDoxy unchanged.
Pattern Matching
Conditions that match values now support three types of patterns:
| Pattern Type | Syntax | Description |
|---|---|---|
| String | "value" or value | Exact string match |
| Glob | glob(pattern) | Glob pattern matching (wildcards) |
| Regex | regex(pattern) | Regular expression matching |
Pattern Examples
# String matching (default)
header X-API-Key "secret-key"
# Glob pattern matching
header User-Agent glob(Mozilla*)
path glob(/api/v[0-9]/*)
# Regular expression matching
header X-API-Key regex("^sk-[a-zA-Z0-9]{32}$")
path regex("^/api/v[0-9]+/users/[a-f0-9-]{36}$")Valid Block-Syntax Sequences
# Multiline OR in header (line continuation)
method GET |
method POST |
method PUT {
pass
}
# Multiline AND in header
header Connection Upgrade &
header Upgrade websocket {
bypass
}
# Non-terminating then terminating
method GET {
rewrite / /index.html
serve /static
}
# Request mutation chain
method POST {
set header X-Custom value
add query debug true
remove header X-Secret
}
# Schedule post-phase logging before the terminating proxy rule
path glob("/api/*") {
log info /dev/stdout "$req_method $status_code"
}
path glob("/api/*") {
proxy http://backend:8080
}
# Dispatch to a built-in GoDoxy handler
path glob("/api/v1/*") {
handle api
}
# Pass-through alias
header X-Bypass true {
bypass
}Configuration Examples
Docker Compose Label (YAML container config + block rule body)
services:
app:
labels:
proxy.app.rules: |
header Connection Upgrade &
header Upgrade websocket {
pass
}
default {
rewrite / /report.html
serve /tmp/access
}When embedding the whole route in a label, both the label and its nested rules
value need a literal block (|) to preserve command line breaks:
services:
app:
labels:
proxy.app: |
port: 8080
rules: |
header Origin {
set resp_header Access-Control-Allow-Origin $$header(Origin)
}Using rules: without | folds the commands into one line and can produce
expect 2 args provider errors. In Docker Compose labels, escape rule variables
as $$header(...) so Compose passes $header(...) to GoDoxy instead of
substituting a Compose environment variable. Standalone rule files use a single
$.
Route File Rule Body (Block Syntax)
path glob("/api/v1/*") {
handle api
}
path glob("/api/*") {
proxy http://api-server:8080
}
default {
pass
}Put the more specific API rule first so the broad proxy rule does not intercept it.
In this example, handle api dispatches to GoDoxy's registered in-process API handler, while default { pass } runs only when neither API rule matches.
Common Use Cases (Block Syntax)
API Gateway with Basic Auth
Set ADMIN_PASSWORD_HASH to the admin password’s bcrypt hash, as described in
Basic Authentication.
path regex("^/api/v[0-9]+/public/.*") {
proxy http://api-server:8080
}
path glob("/api/v[0-9]/admin/*") &
basic_auth admin "${ADMIN_PASSWORD_HASH}" {
set header X-Admin true
proxy http://admin-server:8080
}
path glob("/api/v[0-9]/admin/*") {
require_basic_auth "Admin Access"
}Security + Allowlist
header User-Agent glob(*bot*) |
header User-Agent regex(.+crawler.+) |
remote 192.168.1.0/24 {
error 403 "Access denied"
}
host glob(*.example.com) | host example.com {
pass
}
default {
proxy http://main-server:8080
}CORS Preflight
Keep response-header actions in a separate rule from error: terminating in pre
phase skips the terminating rule's post-phase actions. The header-only rules
below still run for the locally generated preflight response. error 204 ""
returns No Content without contacting the upstream or writing a response body.
When placing this block in Docker Compose, use the label syntax above,
including rules: | for a whole-route label and $$ for variables.
method OPTIONS &
header Origin &
header Access-Control-Request-Method {
set resp_header Access-Control-Allow-Origin $header(Origin)
set resp_header Access-Control-Allow-Methods GET,POST,PUT,PATCH,DELETE,OPTIONS
set resp_header Access-Control-Allow-Headers $header(Access-Control-Request-Headers)
set resp_header Access-Control-Allow-Credentials true
}
method OPTIONS &
header Origin &
header Access-Control-Request-Method {
error 204 ""
}
header Origin {
set resp_header Access-Control-Allow-Origin $header(Origin)
set resp_header Access-Control-Allow-Credentials true
}This example reflects any supplied origin and allows credentials. Restrict the
header Origin conditions to trusted origins before using it with private APIs.
Request Mutation + Proxy
path glob("/api/**") {
set header X-Request-Id $header(X-Request-Id)
add header X-Forwarded-For $remote_host
remove header X-Secret
add query debug true
set cookie locale en-US
proxy http://api-server:8080
}In-Process Handler Dispatch
path glob("/api/v1/*") {
handle api
}
path glob("/auth/*") {
rewrite /auth /api/v1/auth
handle api
}Use handle for handlers already registered inside the current GoDoxy process, such as the built-in API. Use proxy for external upstream services.
Response-Conditional Logging
path glob("/api/**") {
proxy http://api-server:8080
}
status 4xx | status 5xx {
log error /dev/stderr "Status=$status_code CT=$resp_header(Content-Type)"
}Environment Variables
path glob("/service/**") {
proxy https://${SERVICE_HOST}:${SERVICE_PORT}
}
path /secret {
error 403 "Forbidden: ${REDACT_REASON}"
}