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.
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.
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
- This directive specifies a regular expression pattern and replacement string for URL rewriting. You can reference regex capture groups in the replacement string (
Block options#
| Option | Arguments | Description | Default |
|---|---|---|---|
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 |
Configuration example:
example.com {
rewrite "^/old-path/(.*)" "/new-path/$1"
}Simple rewrite#
example.com {
rewrite "^/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.
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 "^/api/v1/(.*)" "/api/v2/$1" {
last
}
rewrite "^/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 "^/legacy/(.*)" "/modern/$1"
rewrite "^/modern/(.*)" "/current/$1"
}Ferron first rewrites a /legacy/foo request to /modern/foo. Then the second rule rewrites it to /current/foo.
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 "^/static/(.*)" "/assets/$1" {
file
directory false
}
}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
- This directive specifies whether Ferron logs each URL rewrite operation to the error log. Default:
Configuration example:
example.com {
rewrite_log true
}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#
Metrics#
| Metric | Type | Attributes | Description |
|---|---|---|---|
ferron.rewrite.rewrites_applied | Counter | None | URLs successfully rewritten |
ferron.rewrite.invalid | Counter | None | 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 INFO level.
Structured logs#
| Description (summary) | Level | Attributes |
|---|---|---|
| URL rewritten | INFO | ferron.rewrite.from (string) shows the original path + query string. ferron.rewrite.to (string) shows the rewritten path + query string |
Access log fields#
The rewrite module contributes the following field to the HTTP access log line:
| Field | Type | Description |
|---|---|---|
ferron.rewrite.applied | bool | Whether Ferron applied a URL rewrite to the request. |
Trace spans#
The rewrite stage sets the following attributes on its ferron.stage.rewrite span:
| Attribute | Type | Description |
|---|---|---|
ferron.rewrite.applied | bool | Whether Ferron applied a rewrite rule to the request. |
ferron.rewrite.pattern_count | int | Number of rewrite rules that matched. |