How to route to content_by_lua nginx directive depending on both HTTP Action and URL prefix?

1.2k views Asked by At

I'd like to route all requests sent to my nginx server to my backend application by default, but selectively send API requests with GET HTTP verbs to a OpenResty Lua based REST API backed by a content_by_lua nginx directive.

I'm successfully able to route all API requests to the Lua API based on their URL prefix using the following configuration (note that this does not take into account the HTTP Verb):

http {
  upstream backend {
    server localhost:8080;
  }
  server {
    listen 80;

    location / {
      # Send all requests to the backend application
      proxy_pass http://backend;
      proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      proxy_set_header   Host             $http_host;
      proxy_set_header   X-Real-IP        $remote_addr;
      proxy_set_header   CLIENT_IP $remote_addr;
      proxy_set_header   HTTP_CLIENT_IP $remote_addr;
      proxy_redirect off;
    }

    location /api {
      # Send any URL with the /api prefix to the nginx Lua API application
      content_by_lua '
        require("lapis").serve("app")
      ';
    }
  }
}

But, as I stated above, I'd like to further restrict API requests such that any requests with HTTP Verbs other than GET (like POST, PUT, DELETE, etc) are still routed to the backend, while the GET requests alone are routed to the Lua API location.

Based on some other posts, blogs, and documentation (and hearing that the if directive is frowned upon), I tried using a limit_except directive, but then the nginx server crashed upon startup as it seems the content_by_lua directive was not designed for limit_except blocks. Here was my attempt:

http {
  upstream backend {
    server localhost:8080;
  }
  server {
    listen 80;

    location / {
      proxy_pass http://backend;
      proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      proxy_set_header   Host             $http_host;
      proxy_set_header   X-Real-IP        $remote_addr;
      proxy_set_header   CLIENT_IP $remote_addr;
      proxy_set_header   HTTP_CLIENT_IP $remote_addr;
      proxy_redirect off;
    }

    location /api {
      # Default the non-get API requests back to the backend server
      proxy_pass http://backend;
      proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      proxy_set_header   Host             $http_host;
      proxy_set_header   X-Real-IP        $remote_addr;
      proxy_set_header   CLIENT_IP $remote_addr;
      proxy_set_header   HTTP_CLIENT_IP $remote_addr;
      proxy_redirect off;

      # Select requests that *aren't* a PUT, POST, or DELETE, and pass those to the Lapis REST API
      limit_except PUT POST DELETE {
        content_by_lua '
          require("lapis").serve("app")
        ';
      }
    }
  }
}

which promptly crashed with

nginx: [emerg] "content_by_lua" directive is not allowed here in nginx.conf:46

What is the best way to selectively route in nginx based on both URL prefix and HTTP verb when delegating to the content_by_lua directive?

1

There are 1 answers

0
Eternal Rubyist On BEST ANSWER

I implemented conditional routing of particular URL GET actions by using the if directive, even though it's evil according to the nginx devs. It does seem like this might one of the few desirable use cases for the if directive, but if it's not or someone has a better approach, please let me know (which is why I haven't accepted my own answer).

Here is the nginx configuration file that currently implements the solution:

http {

  upstream backend {
    server localhost:3000;
  }

  server {
    listen 80;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_set_header   Host             $http_host;
    proxy_set_header   X-Real-IP        $remote_addr;
    proxy_set_header   CLIENT_IP $remote_addr;
    proxy_set_header   HTTP_CLIENT_IP $remote_addr;
    proxy_redirect off;

    # By default pass all the requests to the Rails app backend
    location / {
      proxy_pass http://backend;
    }

    # Delegate certain API calls to our special OpenResty Endpoints
    location /api {
      # Makes sure all POSTs, PUTs, and DELETE actions still get handed off to the backend
      if ($request_method ~ POST|PUT|DELETE) {
        proxy_pass http://backend;
      }
      # All that should remain are the GET and OPTIONS endpoints so send them to the OpenResty Lua backend!)
      content_by_lua 'require("lapis").serve("app")';
    }
  }

}