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

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, 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 {.
  • 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.
  • Nested commands follow the same termination and phase rules as top-level commands.

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 403 "Forbidden: ${CLOUDFLARE_API_KEY}"
proxy https://${DOMAIN}/api
set header X-Tenant "tenant-${DOMAIN}"

To escape the $ character and prevent substitution, use $$:

error 404 "$${NON_EXISTENT}" # Results in literal "${NON_EXISTENT}"

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
}

# Post-phase log via status variable
path glob("/api/*") {
  proxy http://backend:8080
  log info /dev/stdout "$req_method $status_code"
}

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

Route File Rule Body (Block Syntax)

path glob("/api/*") {
  proxy http://api-server:8080
}

default {
  pass
}

In this example, default { pass } runs only when /api/* does not match.

Common Use Cases (Block Syntax)

API Gateway with Basic Auth

path regex("^/api/v[0-9]+/public/.*") {
  proxy http://api-server:8080
}

path glob("/api/v[0-9]/admin/*") &
basic_auth admin "$2y$10$hashed_password" {
  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

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

header Origin {
  set resp_header Access-Control-Allow-Origin $header(Origin)
  set resp_header Access-Control-Allow-Credentials true
}

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
}

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