Introduction
This bug found me, not the other way around.
I was in the middle of migrating my team's infrastructure from NGINX Ingress Controller to NGINX Gateway Fabric when SSI (Server-Side Includes) stopped working. Subrequests were hitting the wrong backend paths, and pages that relied on SSI includes were silently broken. I could have worked around it, added a flag, patched a config, moved on. Instead, I decided to find where it was actually coming from and fix it at the source.
This is the story of that bug, what caused it, and the one-line condition that fixed it.
Background: What is NGINX Gateway Fabric?
NGINX Gateway Fabric is a Kubernetes Gateway API implementation backed by NGINX. It translates Kubernetes HTTPRoute resources into NGINX configuration, handling routing, load balancing, and traffic management. It's the next-generation replacement for NGINX Ingress Controller, built around the Kubernetes Gateway API spec.
When processing HTTP routes, it generates proxy_pass directives in NGINX location blocks to forward traffic to upstream backends.
The Migration That Surfaced the Bug
During the migration from NGINX Ingress Controller to NGINX Gateway Fabric, one of our services used SSI to compose pages from multiple backend responses a pattern like:
<h1>SSI Test Page</h1>
<!--# include virtual="/include.html" -->
On NGINX Ingress Controller this worked fine. After switching to NGINX Gateway Fabric, the SSI includes were broken. After some investigation, the generated proxy_pass directive was the culprit:
proxy_pass http://my-backend$request_uri;
$request_uri in NGINX always holds the original client request URI, it never changes, even during internal subrequests. So when SSI triggered a subrequest to /include.html, the proxy_pass directive would still forward $request_uri (e.g. /) to the backend completely ignoring the subrequest's intended path.
Understanding the Two Location Types
NGINX Gateway Fabric uses two types of location blocks:
External locations — match the original client request directly. Here, $uri is already the correctly processed URI. Using $request_uri is redundant and harmful.
Internal locations — used for NJS-driven HTTP matching. When a request comes in, NJS evaluates the matching rules and calls r.internalRedirect(match.redirectPath + args), which changes $uri to an internal path like /@rule0-route0. Without $request_uri, NGINX would forward that internal path to the backend — so $request_uri is needed here to restore the original client URI.
The bug was that the code made no distinction between these two cases.
The Fix
In internal/controller/nginx/config/servers.go, the createProxyPass function was changed from:
// Before: applies to ALL non-gRPC locations
if !grpc {
if filter == nil || filter.Path == nil {
requestURI = "$request_uri"
}
}
To:
// After: only applies to INTERNAL locations
if !grpc && locationType == http.InternalLocationType {
if filter == nil || filter.Path == nil {
requestURI = "$request_uri"
}
}
This means:
-
External locations generate:
proxy_pass http://my-backend; -
Internal locations still generate:
proxy_pass http://my-backend$request_uri;
Verifying the Fix
I deployed the fix to a local kind cluster with an SSI-enabled backend. Before the fix, the SSI include failed the subrequest hit the wrong path. After the fix, the response correctly returned:
<h1>SSI Test Page</h1>
<p>This content was included via SSI!</p>
The generated NGINX config confirmed the clean proxy_pass directive with no $request_uri for the external location.
Takeaways
- Real-world migrations are great bug finders. Moving from NGINX Ingress Controller to NGINX Gateway Fabric exposed a behavioral difference that tests hadn't caught.
- NGINX's
$request_uriis immutable across the entire request lifecycle, including subrequests. it always reflects the original client URI. - Blindly appending
$request_uritoproxy_passcan interfere with NGINX features that rely on internal subrequests (SSI, auth subrequests, etc.). - When you hit a bug in open source, you can just fix it. The maintainers were responsive and collaborative throughout the review process.
Top comments (0)