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:

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; }
}
locationproxy_passRequestBackend 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
The last row is the common mistake /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.
A rule of thumb that matches the measurements If the 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.