Configuration: URL rewriting

This page documents the rewrite directive for transforming request URLs using regular expression patterns. Ferron applies rewrites in the request pipeline, before proxying or static file serving, so later stages use the rewritten URL.

Important

Rewrites do not trigger a new round of location matching. Ferron selects the location block once, on the original URL, and strips the matched prefix before rewrite rules run. Rules therefore see the location-stripped path. Use match blocks with if or if_not for regex routing. See Request pipeline order.

Info

For url_sanitize interaction, see Routing and URL processing. For static file serving, see Static file serving.

Directives⁠#

rewrite⁠#

  • rewrite <regex: string> <replacement: string>
    • This directive specifies a regular expression pattern and replacement string for URL rewriting. You can reference regex capture groups in the replacement string ($1, $2, and so on). Default: none

Block options⁠#

OptionArgumentsDescriptionDefault
last<bool>When true, stop processing further rewrite rules after this one matches.false
directory<bool>When true, apply this rule when the URL corresponds to a directory.true
file<bool>When true, apply this rule when the URL corresponds to a file.true
allow_double_slashes<bool>When true, preserve double slashes (//) in the URL instead of collapsing them.false
name<string>Operator-chosen rule identifier, surfaced in rewrite observability output. Must be a plain string (no interpolation).derived from rule position

Configuration example:

example.com {
    rewrite r"^/old-path/(.*)" "/new-path/$1"
}

Simple rewrite⁠#

example.com {
    rewrite r"^/old-path/(.*)" "/new-path/$1"
}

Ferron internally rewrites all /old-path/anything requests to /new-path/anything. The client sees no redirect. The rewrite is transparent.

Tip

If you get unexpected routing behavior, check the order of the rewrite rules. Rules with last stop further processing.

Stop processing with last⁠#

example.com {
    rewrite r"^/api/v1/(.*)" "/api/v2/$1" {
        last
    }
    rewrite r"^/api/v2/(.*)" "/api/v3/$1"
}

Ferron rewrites /api/v1/users requests to /api/v2/users and then stops. The second rule never sees the /api/v2/ prefix.

Chained rules without last⁠#

example.com {
    rewrite r"^/legacy/(.*)" "/modern/$1"
    rewrite r"^/modern/(.*)" "/current/$1"
}

Ferron first rewrites a /legacy/foo request to /modern/foo. Then the second rule rewrites it to /current/foo.

When several rules share a host, give them name values so observability output tells them apart:

example.com {
    rewrite r"^/legacy/(.*)" "/modern/$1" {
        name "legacy-to-modern"
    }
    rewrite r"^/modern/(.*)" "/current/$1" {
        name "modern-to-current"
    }
}

File/directory-specific rules⁠#

file false and directory false skip the rule when the URL maps to a real file or directory under root. Use both guards for fallback patterns such as single-page apps and front controllers. Without the guards, a broad pattern rewrites static asset requests too, and scripts, styles, and images break.

example.com {
    root /var/www

    rewrite r"^/static/(.*)" "/assets/$1" {
        file
        directory false
    }
}
Tip

Start every catch-all fallback rule with file false and directory false. Add last to stop rule processing after the fallback matches. Enable rewrite_log true while you test the guards.

rewrite_log⁠#

  • rewrite_log <bool>
    • This directive specifies whether Ferron logs each URL rewrite operation to the error log. Default: rewrite_log false

Configuration example:

example.com {
    rewrite_log
}

Example log output:

URL rewritten from "/old-path/users" to "/new-path/users"

Regex syntax⁠#

The regular expression engine used is regex, which executes regular expressions in linear time (no support for backtracking). This means that regular expressions are executed efficiently without backtracking, mitigating potential ReDoS and catastrophic backtracking attacks.

However, this also means you cannot use syntax listed below:

  • Lookahead statements ((?= ...), (?! ...))
  • Lookbehind statements ((?<= ...), (?<! ...))
  • Backreferences to capture groups (\1)

If you have to negate a condition, you can use negated variants of some directives, such as if_not blocks instead of if, or !~ in matchers instead of ~.

The matching is case-insensitive on Windows and case-sensitive on other platforms.

Observability⁠#

Rule positions below are 1-based: the first rewrite directive in a block is rule 1. Give rules name values when you need readable identifiers instead of positions.

Metrics⁠#

MetricTypeAttributesDescription
ferron.rewrite.rewrites_appliedCounterferron.rewrite.rule_index, ferron.rewrite.rule_name (when named)Rewrite rule firings (one increment per matched rule, so chained rewrites count once per step)
ferron.rewrite.invalidCounterferron.rewrite.rule_index, ferron.rewrite.rule_name (when named)Rewrite rules that produced an invalid path (resulting in a 400 response)

Logs⁠#

When rewrite_log is on, Ferron logs each rewrite operation to the error log at DEBUG level.

Structured logs⁠#

Description (summary)LevelAttributes
URL rewrittenINFOferron.rewrite.from (string) shows the path + query string before the step. ferron.rewrite.to (string) shows the path + query string after the step. ferron.rewrite.rule_index (int) identifies the fired rule. ferron.rewrite.rule_name (string) is present when the rule is named. One line is emitted per fired rule, so chained rewrites produce one line per step

Access log fields⁠#

The rewrite module contributes the following fields to the HTTP access log line:

FieldTypeDescription
ferron.rewrite.appliedboolWhether Ferron applied a URL rewrite to the request.
ferron.rewrite.matched_rulesintNumber of rewrite rules that fired for the request.
ferron.rewrite.rule_indexint1-based position of the first fired rule.
ferron.rewrite.rule_namestringName of the first fired rule, when it sets name.

Trace spans⁠#

The rewrite stage sets the following attributes on its ferron.stage.rewrite span:

AttributeTypeDescription
ferron.rewrite.appliedboolWhether Ferron applied a rewrite rule to the request.
ferron.rewrite.pattern_countintNumber of rewrite rules evaluated (not the number that matched).
ferron.rewrite.matched_rule_countintNumber of rewrite rules that fired for the request.
ferron.rewrite.rule_indexint1-based position of the first fired rule.
ferron.rewrite.rule_namestringName of the first fired rule, when it sets name.