Lesson 2
proxy_pass and the trailing slash
proxy_pass decides what path the backend actually receives. This lesson measures four
spellings against the same request to see which path each one produces. The subtle part is that a single
trailing slash switches nginx between two different assembly rules.
The rule
nginx distinguishes two cases, according to whether proxy_pass carries a path component:
-
No path component (just
scheme://host:port): the request URI is passed through to the backend unchanged. -
A path component is present, even when it is only a single
/: the part of the request URI that matched thelocationprefix is replaced by that path.
The replacement in the second case is plain string substitution. nginx does not insert a separator and does not normalise the path afterwards.
Four spellings, measured
The rig runs two servers inside one nginx:1.27-alpine container. The second acts as the
backend and echoes the $request_uri it received, so nothing has to be inferred.
server {
listen 8080;
location / { default_type text/plain; return 200 "backend received: $request_uri\n"; }
}
server {
listen 80;
location /a/ { proxy_pass http://127.0.0.1:8080; }
location /b/ { proxy_pass http://127.0.0.1:8080/; }
location /c/ { proxy_pass http://127.0.0.1:8080/goc/; }
location /d/ { proxy_pass http://127.0.0.1:8080/goc; }
}
| location | proxy_pass | Request | Backend received |
|---|---|---|---|
/a/ | :8080 | /a/x/y | /a/x/y |
/b/ | :8080/ | /b/x/y | /x/y |
/c/ | :8080/goc/ | /c/x/y | /goc/x/y |
/d/ | :8080/goc | /d/x/y | /gocx/y |
/goc has no trailing slash while /d/ does. The remainder of the request is
x/y, and nginx joins /goc to x/y directly, producing
/gocx/y. The backend receives a path that does not exist and usually answers 404, while
nothing in the nginx log looks unusual enough to trace.
location ends in /, the path component of proxy_pass
should end in / as well. The first three rows follow that convention and all produce
readable paths; the fourth breaks it and produces the run-together segment.
The lab
Change any of the three fields. The algorithm running in your browser is a reimplementation of the rule above, checked against nginx 1.27.5 on exactly the four rows of the table — all four agree.
The path sent upstream
Change any of the three fields.
The four measured spellings:
When the location is a regex
Prefix replacement only makes sense for a prefix location. With
location ~ … nginx has no prefix to replace, so
proxy_pass inside a regex block may not carry a path component.
Writing proxy_pass http://backend/api/ in a regex block makes nginx refuse to start:
[emerg] "proxy_pass" cannot have URI part in location given by regular
expression, or inside named location, or inside "if" statement, or inside
"limit_except" block
Inside a regex block the alternative is to set $uri with rewrite first, then
proxy_pass to the bare origin with no path.
Check yourself
location /api/ with proxy_pass http://be:3000/. In what form does /api/users/7 reach the backend?
As /users/7. proxy_pass carries the path /, so the prefix /api/ is replaced by /, leaving users/7.
What changes if you drop the trailing slash, giving proxy_pass http://be:3000?
There is no path component any more, so the request URI passes through unchanged: the backend receives /api/users/7. That is what you want when the backend also serves under the /api/ prefix.
Why is proxy_pass http://be:3000/v1 with location /api/ a suspicious configuration?
/api/users arrives at the backend as /v1users. The two strings run together because /v1 does not end in a slash. Try it in the lab above.