Nginx proxy_pass trailing slash: why /api/foo becomes /apifoo

Updated 10 November 2026 · By Brent Alexis

Short answer: when proxy_pass has a path after the address, even just /, Nginx cuts the part of the request that matched the location and puts that path in its place. If one of them ends in a slash and the other doesn't, the slash between them is lost or doubled: location / { proxy_pass http://127.0.0.1:3000/api; } sends /foo to your app as /apifoo. Make both end in a slash, or leave the path off proxy_pass completely. nginx -t can't warn you, because the config is valid.

Every case on this page was run on the official nginx Docker image (Nginx 1.30.6), Debian 12's package (Nginx 1.22.1) and the official Apache image (Apache 2.4.69), with a backend that answers with the exact path it received: 36 checks, all passed. How it's tested

The rule

Nginx's documentation puts it in one sentence: when proxy_pass is given with a URI, “the part of a normalized request URI matching the location is replaced by a URI specified in the directive.” The URI is everything after the address, so http://127.0.0.1:3000/ has one (/) and http://127.0.0.1:3000 doesn't.

Query strings came through unchanged in every case tested, with and without a path.

What each proxy_pass sends

Each row was checked on both Nginx versions with the same result. app:3000 stands for your backend's address.

locationproxy_passRequest → app receives
/api/http://app:3000/api/foo → /api/foo
/api/http://app:3000//api/foo → /foo
/api/http://app:3000/v1/api/foo → /v1foo (broken)
/api/http://app:3000/v1//api/foo → /v1/foo
/apihttp://app:3000//api/foo → //foo (broken)
/apihttp://app:3000//apifoo → /foo (caught too)
/http://app:3000/api/foo → /apifoo (broken)
/http://app:3000/api//foo → /api/foo

The pattern: if the location ends in a slash, the proxy_pass path must too, and the other way round.

One more difference: with a path, Nginx works from the cleaned-up (normalized) request. In the lab, /api//x reached the app as /x with proxy_pass http://app:3000/;. With no path in proxy_pass, it arrived as /api//x, exactly as sent. If your app needs the request exactly as the browser sent it, leave the path off proxy_pass.

The /apifoo bug and the fix

This is the common one: your whole site goes to an app that lives under /api.

location / {
    proxy_pass http://127.0.0.1:3000/api;    # /foo reaches the app as /apifoo
}

The location matched /, so Nginx swaps that / for /api and keeps the rest, foo: /api + foo = /apifoo. Your app answers 404, and nothing in the Nginx error log says why. The fix is one slash:

location / {
    proxy_pass http://127.0.0.1:3000/api/;   # /foo reaches the app as /api/foo
}

ProxyForge's Config Generator made this exact mistake until a reviewer spotted it. It now adds the slash for you when the backend address has a path, and a comment in the output shows where /foo ends up.

location /api vs location /api/

A location without the slash is a plain text match, so location /api also catches /apifoo, /api-docs and /apiary. In the lab, location /api { proxy_pass http://app:3000/; } sent /apifoo to the app as /foo, and /api/foo as //foo.

Fix: use location /api/ with proxy_pass http://app:3000/;. Both end in a slash, /api/foo arrives as /foo, and /apifoo is no longer caught.

Why /api redirects to /api/

With location /api/, a request for /api (no slash) gets a redirect instead of reaching your app. In the lab both Nginx versions answered like this (with the server's own name in place of example.com):

HTTP/1.1 301 Moved Permanently
Location: http://example.com/api/

That's by design. Nginx's documentation says that when a prefix location ending in a slash is handled by proxy_pass, a request for the same path without the slash gets a 301 to the path with the slash added. Usually that's what you want. If your app must answer /api itself, the documentation's fix is to add an exact-match location alongside, with no path after the address, such as location = /api { proxy_pass http://app:3000; }, which passes /api through unchanged.

Regex locations: "cannot have URI part"

A path in proxy_pass isn't allowed inside a regex location such as location ~ ^/api/. On both versions, nginx -t refused location ~ ^/api/ { proxy_pass http://app:3000/v1/; } with this message, followed by the file and line:

[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

Fix: build the path yourself with a capture, and add the query string back with $is_args$args:

upstream app {
    server 127.0.0.1:3000;
}
server {
    # ...
    location ~ ^/api/(.*)$ {
        proxy_pass http://app/v1/$1$is_args$args;    # /api/foo?x=1 reaches the app as /v1/foo?x=1
    }
}

Once proxy_pass contains a variable, Nginx sends exactly the path you built, and a hostname in it must be an upstream name like app above or be looked up through a resolver. If you don't need a regex, a prefix location like location /api/ is simpler.

Apache ProxyPass has the same trap

Apache swaps the path the same way, and its documentation warns: “If the first argument ends with a trailing /, the second argument should also end with a trailing /, and vice versa.” Tested in a virtual host on Apache 2.4.69:

ProxyPassRequest → app receives
/api/ http://app:3000//api/foo → /foo
/api http://app:3000//api/foo → //foo (broken)
/api/ http://app:3000/v1/api/foo → /v1foo (broken)
/api/ http://app:3000/v1//api/foo → /v1/foo

Two differences from Nginx turned up. ProxyPass /api http://app:3000/ did not catch /apifoo; Apache answered it with its own 404. And with ProxyPass /api/, a request for /api got a 404, not a redirect.

ProxyPass only works in the main server config or a virtual host, not in .htaccess. For .htaccess, the Config Generator uses a [P] rewrite rule with the slash written in, RewriteRule ^/?(.*) http://127.0.0.1:3000/api/$1 [P,L], so /foo arrives as /api/foo. See ProxyPass not allowed in .htaccess.

Check what your app receives

Request a test path and look for it in your app's access log:

curl -i https://example.com/foo

If your app has no access log, point proxy_pass for a minute at a tiny Nginx server that answers with the path it was sent. This is the backend the lab used:

server {
    listen 127.0.0.1:3001;
    location / {
        default_type text/plain;
        return 200 "$request_uri\n";
    }
}

With proxy_pass http://127.0.0.1:3001/api/;, curl https://example.com/foo prints /api/foo. Put your real backend back when you're done, then run sudo nginx -t && sudo systemctl reload nginx.

Generate a reverse proxy config with the slash already right →

How to use ProxyForge · FAQ: common errors · Guides · What's new · About