1

With the following nginx location directive

  location ~* (.*)(\/graphql)$ {
    proxy_pass http://my-backend:80/$2;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
  }

I expect that URLs like https://example.com/anything/graphql is redirected to http://my-backend:80/$2 but that's not the case as nginx is giving me a 404 error whenever I try to visit an URL ending with /graphql. The error will be

[error] 31#31: *1 no resolver defined to resolve my-backend, client: 172.18.0.1, server: localhost, request: "GET /anything/graphql HTTP/2.0", host: "localhost"
  • Your regex doesn't make sense to me. What exactly do you want to pass as an URI to your backend when you got an `/anything/graphql` request? Can it be any suffix after the `/graphql` part? – Ivan Shatsky May 16 '22 at 15:59
  • Shouldn't `$2` be exactly `/graphql`? – Antonio Santoro May 16 '22 at 16:05
  • Yes, it would be exactly the `/graphql`. So all you need is to pass an URI as a `/graphql`? – Ivan Shatsky May 16 '22 at 16:09
  • Even if `$2` will always be `/graphql` the mapping won't work – Antonio Santoro May 16 '22 at 16:10
  • Your error isn't related to mapping. Once again, all you need is to pass an URI as a `/graphql` for every request like `/anything/graphql` or `/even/more/graphql`? Your upstream does not need to know the `/anything` request part at all? – Ivan Shatsky May 16 '22 at 16:14
  • Assuming the answer is "yes", here is an explanation and solution that won't require any `resolver` specified. – Ivan Shatsky May 16 '22 at 17:17

1 Answers1

0

It is a known nginx limitation that you can't specify an URI for proxy_pass inside the regex location. If you'd try to do something like

proxy_pass http://my-backend/graphql;

you've got the following nginx error:

nginx: [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 in ...

You have two options instead. The first one is to use variables to pass whatever you want to your upstream as an URI, either the way you have chosen or, if it is a constant string, specifying your endpoint explicitly like

set $endpoint graphql;
proxy_pass http://my_backend/$endpoint;

However this approach have a drawback you already faced - it is requires a resolver being defined if your upstream is specified via the hostname rather than IP address. That is, the

proxy_pass http://localhost/$endpoint;

configuration line would require a resolver while

proxy_pass http://127.0.0.1/$endpoint;

won't require it. It doesn't matter the $endpoint variable is used only for the URI part. If you wonder what is the need of that resolver, let me quote the most relevant part from the blog post already mentioned:

Linux, POSIX and the like offer only one way to get an IP from a name: gethostbyname. If you take time to read the man page (always a safe thing to do... ;)) you'll realise there is a lot to do to resolve a name: open files, ask NIS or YP what they think about it, ask a DNS server (may be in a few different ways). And all this is synchronous. Now that you are used to the nginx way, you know how bad this is and you don't want to go down the ugly synchronous way. So Igor, faithful to himself, reimplemented a DNS lookup (with an in-memory cache, mind you) just to avoid calling this ugly blocking gethostbyname... And that's why we have this extra resolver directive. Yes, coding the fastest web server comes at a price...

When there are no other way but to use variables with the proxy_pass, using the public DNS like 8.8.8.8 is an option, however setting up your own local one should be much more performant (or you'll have continuous traffic exchange with that 8.8.8.8 host). However there is another option. You can rewrite an URI inside your location block to required one and use the proxy_pass without any additional variables:

location ~ /graphql$ {
    rewrite ^ /graphql break;
    proxy_pass http://my-backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

For the most of cases this is a more convenient way since using such a configuration you won't need any specified resolver at all.

As you can see, I don't use any capture groups here since there are no any real sense to use them solving your particular case. The given regex does exactly the same as yours (while being slightly more performant). However there are cases when you really need to use them, and if someone who really need to use a capture group(s) will read this, he must be warned that the straight way of doing something like

location ~ (/graphql)$ {
    rewrite ^ $1 break;
   ...

won't work. The reason is that numbered captures are being reevaluated whenever regex pattern matching operation takes its place, and the rewrite directive is exactly one of those that did such an operation (making $1 to be an empty string). The solution would be to use named capture groups instead:

location ~ (?<url>/graphql)$ {
    rewrite ^ $url break;
   ...

or one can reuse regex pattern (which I think will be somewhat less performant):

location ~ /graphql$ {
    rewrite (/graphql)$ $1 break;
    ...
Ivan Shatsky
  • 13,267
  • 2
  • 21
  • 37