Fixing URL Prefix Stripping in Traefik: Inside Pull Request #12863
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
- Part 9: Fixing URL Prefix Stripping in Traefik: Inside Pull Request #12863
If you run Traefik as your reverse proxy (and millions of developers do), you’ve probably used the StripPrefix or StripPrefixRegex middleware. These handy tools let you clean up URLs before they reach your backend services — for example, stripping /api/ so your service only sees /v1/users.
But there was a sneaky bug. When your prefix contained percent-encoded characters (like %2F for a slash or %20 for a space), the stripping process sometimes corrupted the URL. Backends would receive broken paths, leading to mysterious 404s or malformed requests.
Pull Request #12863 fixes exactly that. Merged into Traefik v2.11, it makes prefix stripping reliable even with encoded characters. Let’s walk through what was broken, why it happened, how the fix works, and why this small change matters.
The Problem: Encoded Characters Break the Slice 🔗
Traefik (written in Go) uses Go’s built-in net/url package. Every incoming request becomes a *http.Request with two important URL fields:
URL.Path: The decoded version (human-readable, e.g.,/api/v1).URL.RawPath: The original encoded version as it arrived over the wire (e.g.,/ap%69/v1where%69is the letter “i”).
The middleware needs to:
- Match and remove the prefix using the decoded
Path(easier for regex and string matching). - Also update
RawPathso the backend receives the correctly encoded remaining path.
Before the fix, the code simply did something like:
// Old (buggy) logic – simplified
prefixLen := len(prefix) // based on decoded Path
req.URL.RawPath = req.URL.RawPath[prefixLen:] // slice by decoded length!
Why this fails with encoded characters:
Imagine this request path:
- RawPath (what arrived):
/ap%69/api/v1 - Decoded Path:
/api/api/v1(because%69= “i”)
If your StripPrefix config is prefixes: ["/api/"]:
- Decoded prefix length = 5 characters (
/api/). - But in RawPath, the matching prefix
/ap%69/takes 6 bytes (/ a p % 6 9 /).
Slicing RawPath at position 5 cuts inside the %69 sequence → corrupted output like /i/api/v1 or worse.
Result? Your backend sees garbage, and routing breaks.
Why This Bug Existed (Technical Deep Dive) 🔗
Go’s net/url package is clever: Path is always unescaped for easy matching, while RawPath preserves exact original encoding (important for proxies that forward the request). Many middlewares and even Go’s own http.StripPrefix have historically struggled with this distinction.
In Traefik’s case:
StripPrefixRegexran a regex on the decodedPathto find the prefix.- Then it blindly used
len(prefix)(decoded) to slice the encodedRawPath. StripPrefix(the non-regex version) had the same flaw.
This only surfaced with prefixes containing %XX sequences — a real-world scenario in APIs using encoded segments, international characters, or special routing.
Community reports (and internal testing) showed the bug causing intermittent failures when paths included encoded slashes, spaces, or other reserved characters.
The Fix: Calculate the Encoded Prefix Length 🔗
The PR (authored by Gina A. ) introduces a simple but brilliant helper function:
func encodedPrefixLen(rawPath, decodedPrefix string) int {
decoded := 0
i := 0
for i < len(rawPath) && decoded < len(decodedPrefix) {
if rawPath[i] == '%' && i+2 < len(rawPath) {
i += 3 // Skip entire %XX sequence (3 bytes)
} else {
i++ // Normal character (1 byte)
}
decoded++
}
return i
}
How it works step-by-step (for our example /ap%69/api/v1 + prefix /api/):
- Start at byte 0 of RawPath.
- Count decoded characters one by one.
- When you hit a
%, jump forward 3 bytes instead of 1. - Stop when you’ve counted enough decoded characters to match the prefix.
- Return the exact byte offset in RawPath → 6 in this case.
Then the new stripping code becomes:
// New, correct logic
if req.URL.RawPath != "" {
offset := encodedPrefixLen(req.URL.RawPath, prefix)
req.URL.RawPath = ensureLeadingSlash(req.URL.RawPath[offset:])
}
A tiny helper ensureLeadingSlash makes sure the result always starts with / (as expected by most routers).
The same logic was applied to StripPrefixRegex for consistency.
Tests Prove It Works 🔗
The PR added comprehensive test cases in both strip_prefix_test.go and strip_prefix_regex_test.go. Example:
{
desc: "encoded char in prefix segment of raw path",
config: dynamic.StripPrefix{Prefixes: []string{"/api/"}},
path: "/ap%69/a%2Fb", // decoded: /api/a/b
expectedPath: "/a/b",
expectedRawPath: "/a%2Fb", // correctly preserved encoding!
expectedHeader: "/api/",
},
These tests cover regex patterns, spaces (%20), and mixed encoded segments — ensuring the fix is solid.
Goals of the Pull Request — Clearly Achieved 🔗
✅ What: Fixed incorrect slicing in StripPrefix and StripPrefixRegex when prefixes contain percent-encoded characters.
✅ How: Added encodedPrefixLen to compute the true byte offset in RawPath by walking and skipping %XX sequences.
✅ Why: Prevent corrupted RawPath values that broke backend routing and caused subtle production bugs.
By the end of the PR:
- Both middlewares now handle encoded prefixes perfectly.
- Backward compatibility is 100% preserved.
- New tests guarantee the behavior won’t regress.
- The change is small, efficient, and merged cleanly into Traefik v2.11.
Why This Matters for You 🔗
Whether you’re routing microservices, handling API gateways, or just stripping /admin/ paths — you can now trust Traefik’s prefix middleware even with complex, encoded URLs. No more mysterious 404s or corrupted paths.
Upgrade tip: If you’re on v2.10 or earlier, update to v2.11+ and enjoy rock-solid URL handling.
Thanks to contributor Gina A. for the elegant fix, and the Traefik maintainers for the quick review and merge!
Traefik — because modern networking should just work. 🚀
I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
- Part 9: Fixing URL Prefix Stripping in Traefik: Inside Pull Request #12863