GoDoxy
Advanced Topics

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 with name: default or on: 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:8080

Rule Behavior

  • Rules are processed in pre and post phases.
  • A default rule (name: default or on: 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 when command is 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 { ... }, not notify info ntfy { ... }.
  • Logical headers can continue to the next line when the current line ends with | or &.
  • elif and else must be on the same line as the preceding }.
  • Multiple elif branches are allowed; only one else is allowed.
  • A nested block or elif/else chain 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 quotes

Supported Escape Sequences

SequenceDescriptionContext
\nNew lineString values, regex patterns
\tTabString values, regex patterns
\rCarriage returnString values, regex patterns
\\BackslashString values, regex patterns
\"Double quoteString values
\'Single quoteString values
\SpaceString values
$$Dollar sign (literal)Any
\bWord boundaryRegex patterns only
\BNon-word boundaryRegex patterns only
\sWhitespace characterRegex patterns only
\SNon-whitespace characterRegex patterns only
\wWord characterRegex patterns only
\WNon-word characterRegex patterns only
\dDigit characterRegex patterns only
\DNon-digit characterRegex 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 TypeSyntaxDescription
String"value" or valueExact string match
Globglob(pattern)Glob pattern matching (wildcards)
Regexregex(pattern)Regular expression matching
Quotes are optional for regex and glob

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}"
}

On this page