Why does a variable not work in NGINX `proxy_pass`? - nginx

Why does a variable not work in proxy_pass?
This works perfectly:
location /foo/ {
proxy_pass http://127.0.0.1/;
}
This doesn't work at all:
location /foo/ {
set $FOO http://127.0.0.1/;
proxy_pass $FOO;
add_header x-debug $FOO;
}
I see the x-header: http://127.0.0.1/ but the result is 404 so I don't know where it's proxying to but it's not identical to the first example.
Source where it is explained that using a variable in proxy_pass will prevent NGINX startup errors when the upstream is not available.
UPDATE: The issue is the upstream path rewriting. I expect it to rewrite /foo/blah to the upstream at /blah removing the /foo prefix. It works fine with static host/uri entries but not with a variable.

The final answer, much aided by #MSalters was more complicated than I could imagine. The reason is that NGINX works differently with variables than with statically entered hostnames - it does not even use the same DNS mechanism.
The main issue is that path handling and prefix stripping does not work the same with variables. You have to strip path prefixes yourself. In my original example:
location /foo/ {
set $FOO 127.0.0.1;
rewrite /foo/(.*) /$1 break;
proxy_pass http://$FOO/$1$is_args$args;
}
In my example I use an IP address so no resolver is required. However, if you use a host name a resolver is required so add your DNS IP there. Shrugs.
For full disclosure, we are using NGINX inside Kubernetes so it gets even more complicated. The special points of interest are:
Add a resolver directive with the IP of the cluster's DNS service (in my case 10.43.0.10). This is the ClusterIP of the kube-dns service in the kube-system namespace.
Use a FQDN even if your NGINX is in the same namespace since the DNS can only resolve FQDN apparently.
location /foo/ {
set $MYSERVICE myservice.mynamespace.svc.cluster.local;
rewrite /foo/(.*) /$1 break;
proxy_pass http://$MYSERVICE/$1$is_args$args;
resolver 10.43.0.10 valid=10s;
}
NOTE: Due to a BUG (which is unfortunately not acknowledged by NGINX maintainers) in NGINX, using $1 in URLs will break if the path contains a space. So /foo%20bar/ will be passed upstream as /foo bar/ and just break.

The idea of variables in nginx.conf is to delay evaluation. An nginx.conf configuration is parsed for two different reasons: on startup, and later when a request is made. In the second parse, request-dependent variables are filled in.
This parser is not very smart, and this behavior is not officially specified. The trick in your link is a proper hack.
The work-around I use in production is proxy-pass http://$FOO. The concatenation of the literal string http and the variable $FOO does happen when a request is made, showing that variables do work in proxy-pass. Why it doesn't work with a plain substitution, I don't know. And since it's an undocumented hack, this might change from version to version. It would be nice if nginx itself was smarter.
[EDIT]
In some cases, the part of a request URI to be replaced cannot be determined:
...
When variables are used in proxy_pass:
location /name/ { proxy_pass http://127.0.0.1$request_uri;}
In this case, if URI is specified in the directive, it is passed to the server as is, replacing the original request URI.
This different behavior if a variable is present is spelled out in the manual. You could try a rewrite directive, which modifies the URI to be sent.

Related

Removing location without removing the rest of the path

According to many old posts here and in other places on the internet, the following nginx configuration should proxy http://nginx-service/foo/bar to http://web.default.svc.cluster.local:8080/bar.
In other words, it should strip the /foo part in the path (the location matched) while appending the rest (/bar) when proxying it on.
That is not what I observe in practice; the full path is removed and / is proxied on.
How could I proxy this to the upstream service while keeping /bar?
You didn't add any config in your question but any of the following should work:
location /foo/ {
proxy_pass http://web.default.svc.cluster.local:8080/;
}
or
location /foo/bar {
proxy_pass http://web.default.svc.cluster.local:8080/bar;
}

How nginx rewrite and proxy? http://sa.com/rabbitmq/api/#/queues/%2F/somequeue

I try to use a single domain to proxy several programs like this:
http://sa.com/rabbitmq/ ---> http://localhost:15672/
http://sa.com/zabbix/ ---> http://localhost:10000/
and my conf is blow:
location /rabbitmq {
rewrite /rabbitmq(.*) $1 break;
proxy_pass http://localhost:15672;
It works well until I click a queue name to watch the detail,
which url is as the title said:
http://sa.com/rabbitmq/api/#/queues/%2F/somequeue
an 404 error occured, I saw an request in dev-tools of chrome:
http://rabbitmq.testing.gotokeep.com:15672/api/queues/%2F/dailyNewLike?lengths_age=60&lengths_incr=5&msg_rates_age=60&msg_rates_incr=5
this request returned 404.
I guess that when rewrite processed, the uri was decoded (.../%2F/... -> ...///...) and the extra slashes will be removed...
Is my guess right? Is there a solution?
Your guess is good, but no, the real problem is that nginx converts %2F into %252F (% -> %25).
%2F is vhost name (/). I don't found the real solution for this problem, and my workaround was to use other vhost name which do not contains / symbol (e.g. pool1).
You can use $request_uri to prevent nginx decode the uri.
use conf like below
location /rabbitmq {
if ($request_uri ~* "/rabbitmq/(.*)") {
proxy_pass http://localhost:15672/$1;
}
}

nginx 'proxy_pass' cannot have URI part in location?

I have a location block as
location #test{
proxy_pass http://localhost:5000/1;
}
but nginx complains that "proxy_pass cannot have URI part in location given by regular expression..." Does anyone know what might be wrong?
I'm trying to query localhost:5000/1 when an upload is complete:
location /upload_attachment {
upload_pass #test;
upload_store /tmp;
...
}
Technically just adding the URI should work, because it's documented here and it says that it should work, so
location #test{
proxy_pass http://localhost:5000/1/; # with a trailing slash
}
Should have worked fine, but since you said it didn't I suggested the other way around, the trick is that instead of passing /my/uri to localhost:5000/1, we pass /1/my/uri to localhost:5000,
That's what my rewrite did
rewrite ^ /1$1
Meaning rewrite the whole URL, prepend it with /1 then add the remaining, the whole block becomes
location #test{
rewrite ^ /1$1;
proxy_pass http://localhost:5000;
}
Note: #Fleshgrinder provided an answer explaining why the first method didn't work.
What's actually happening here?
nginx cannot process your desired URI part in the proxy_pass directive because you're within a named location (hence the error message). This is because nginx is built in a modular fashion and each configuration block is read in various stages by the various modules. So just remember that you cannot have a URI within your proxy_pass directive in the following cases:
Regular Expression Locations
Named Locations
if Blocks
How could we solve this problem?
Mohammad AbuShady explained how to do a rewrite and pass the requested URI to the proxy server. I just wanted to clarify the reason.
Try omitting the "/" (URI part) and check.
location #test{
proxy_pass http://localhost:5000;
}

Avoid nginx decoding query parameters on proxy_pass (equivalent to AllowEncodedSlashes NoDecode)

I use nginx as a load balencer in front of several tomcats. In my incoming requests, I have encoded query parameters. But when the request arrives to tomcat, parameters are decoded :
incoming request to nginx:
curl -i "http://server/1.1/json/T;cID=1234;pID=1200;rF=http%3A%2F%2Fwww.google.com%2F"
incoming request to tomcat:
curl -i "http://server/1.1/json/T;cID=1234;pID=1200;rF=http:/www.google.com/"
I don't want my request parameters to be transformed, because in that case my tomcat throws a 405 error.
My nginx configuration is the following :
upstream tracking {
server front-01.server.com:8080;
server front-02.server.com:8080;
server front-03.server.com:8080;
server front-04.server.com:8080;
}
server {
listen 80;
server_name tracking.server.com;
access_log /var/log/nginx/tracking-access.log;
error_log /var/log/nginx/tracking-error.log;
location / {
proxy_pass http://tracking/webapp;
}
}
In my current apache load balancer configuration, I have the AllowEncodedSlashes directive that preserves my encoded parameters:
AllowEncodedSlashes NoDecode
I need to move from apache to nginx.
My question is quite the opposite from this question : Avoid nginx escaping query parameters on proxy_pass
I finally found the solution: I need to pass $request_uri parameter :
location / {
proxy_pass http://tracking/webapp$request_uri;
}
That way, characters that were encoded in the original request will not be decoded, i.e. will be passed as-is to the proxied server.
Jean's answer is good, but it does not work with sublocations. In that case, the more generic answer is:
location /path/ {
if ($request_uri ~* "/path/(.*)") {
proxy_pass http://tracking/webapp/$1;
}
}
Note that URL decoding, commonly known as $uri "normalisation" within the documentation of nginx, happens before the backend IFF:
either any URI is specified within proxy_pass itself, even if just the trailing slash all by itself,
or, URI is changed during the processing, e.g., through rewrite.
Both conditions are explicitly documented at http://nginx.org/r/proxy_pass (emphasis mine):
If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive
If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client when the original request is processed, or the full normalized request URI is passed when processing the changed URI
The solution depends on whether or not you need to change the URL between the front-end and the backend.
If no URI change is required:
# map `/foo` to `/foo`:
location /foo {
proxy_pass http://localhost:8080; # no URI -- not even just a slash
}
Otherwise, if you do need to swap or map /api of the front-end with /app on the backend, then you can get the original URI from the $request_uri variable, and the use the rewrite directives over the $uri variable similar to a DFA (BTW, if you want more rewrite DFA action, take a look at mdoc.su). Note that the return 400 part is needed in case someone tries to get around your second rewrite rule, as it wouldn't match something like //api/.
# map `/api` to `/app`:
location /foo {
rewrite ^ $request_uri; # get original URI
rewrite ^/api(/.*) /app$1 break; # drop /api, put /app
return 400; # if the second rewrite won't match
proxy_pass http://localhost:8080$uri;
}
If you simply want to add a prefix for the backend, then you can just use the $request_uri variable right away:
# add `/webapp` to the backend:
location / {
proxy_pass http://localhost:8080/webapp$request_uri;
}
You might also want to take a look at a related answer, which shows some test-runs of the code similar to the above.
There is one documented option for Nginx proxy_pass directive
If it is necessary to transmit URI in the unprocessed form then directive proxy_pass should be used without URI part:
location /some/path/ {
proxy_pass http://127.0.0.1;
}
so in your case it could be like this. Do not worry about request URI it will be passed over to upstream servers
location / {
proxy_pass http://tracking;
}
Hope it helps.
In some cases, the problem is not on the nginx side - you must set the uri encoding on Tomcat connector to UTF-8.

Correct proxy path in nginx.conf

we have two servers, A and B. Server A is accessed worldwide. He has nginx installed. That's what I have in conf:
location /test {
proxy_pass http://localserver.com;
}
What it should do, is translate the addreess http://globalserver.com/test (that is server A) to internal server address http://localserver.com. However, it does append the location path, that is, itries to look for http://localserver.com/test, which is not available at all. How can I make the proxy pass to the correct address, throwing out the last part in the location?
That should work. Nginx should strip the '/test' path on the upstream local server. So what I can say is that is not the cause. To make it a bit better, try this:
location /test/ {
proxy_pass http://localserver.com/;
}
The 2 slashes I added at the first 2 lines will avoid mistakenly match '/testABC' and send the wrong request to the upstream local server, for example.
Do you have a
proxy_redirect
line in the same location block? If your upstream local server has redirects, then a mistake on that line will cause an issue like you described.
[UPDATE] Found the root cause why the original config didn't work and mine works: nginx does NOT replace URI path part if the proxy_pass directive does not have a URI path itself. So my fix of adding a slash (slash is treated as a URI path) at the end triggers the URI path replacement.
Reference: http://wiki.nginx.org/HttpProxyModule#proxy_pass
If it is necessary to transmit URI in the unprocessed form then directive proxy_pass should be used without URI part
location /some/path/ {
proxy_pass http://127.0.0.1;
}
try to add as specified here http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass:
proxy_pass http://localserver.com/;
try rewrite
location /test {
rewrite ^ $scheme://$host/;
proxy_pass http://localserver.com;
}
some helpful links...
http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite
http://wiki.nginx.org/Pitfalls

Resources