skip to Main Content

I have an Nginx instance running as a reverse proxy. When the upstream server does not respond, I send a custom error page for the 502 response code. When the upstream server sends an error page, that gets forwarded to the client, and I’d like to show a custom error page in that case as well.

If I wanted to replace all of the error pages from the upstream server, I would set proxy_intercept_errors on to show a custom page on each of them. However, there are cases where I’d like to return the actual response that the upstream server sent: for example, for API endpoints, or if the error page has specific user-readable text relating to the issue.

In the config, a single server is proxying multiple applications that are behind their own proxy setups and their own rules for forwarding requests around, so I can’t just specify this per each location, and it has to work for any URL that matches a server.

Because of this, I would like to send the custom error page, unless the upstream application says not to. The easiest way to do this would be with a custom HTTP header. There is a similar question about doing this depending on the request headers. Is there a way to do this depending on the response headers?

(It appears that somebody else already had this question and their conclusion was that it was impossible with plain Nginx. If that’s true, I would be interested in some other ideas on how to solve this, possibly using OpenResty like that person did.)

So far I have tried using OpenResty to do this, but it doesn’t seem compatible with proxy_pass: the response that the Lua code generates seems to overwrite the response from the upstream server.

Here’s the location block I tried to use:

location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://localhost:65000;

        content_by_lua_block{
          ngx.say("This seems to overwrite the content from the proxy?!")
        }        

        body_filter_by_lua_block {
          ngx.arg[1]="Truncated by code!"
          ngx.arg[2]=false
          if ngx.status >= 400 then
            if not ngx.resp.get_headers()["X-Verbatim"] then
              local file = io.open('/usr/share/nginx/error.html', 'w')
              local html_text = file:read("*a")
              ngx.arg[1] = html_text
              ngx.arg[2] = true
              return
            end
          end
    }
}

2

Answers


  1. I don’t think that you can send custom error pages based on the response header since the only way, as per my knowledge, you could achieve that was using either map or if directive. Since both of these directives don’t have scope after the request is sent to the upstream, they can’t possibly read the response header.

    However, you could do this using openresty and writing your own lua script. The lua script to do such a thing would look something like:

    location / {
      body_filter_by_lua '
         if ngx.resp.get_headers()["Cust-Resp-Header"] then
             local file = io.open('/path/to/file.html', 'r')
             local html_text = f:read()
             ngx.arg[1] = html_text
             ngx.arg[2] = true
             return
         end
      ';
    
      #
      .
      .
      .
    }
    

    You could also use body_filter_by_lua_block (you could enclose your lua code inside curly brances instead writing as nginx string) or body_filter_by_lua_file (you could write your lua code in a separate file and provide the file path).

    You can find how to get started with openresty here.

    P.S.: You can read the response status code from the upstream using ngx.status. As far as reading the body is concerned, the variable ngx.arg[1] would contain the response body after the response from the upstream which we’re modifying here. You can save the ngx.arg[1] in a local variable and try to read the error message from that using some regexp and appending later in the html_text variable. Hope that helps.

    Edit 1: Pasting here a sample working lua block inside a location block with proxy_pass:

    location /hello {
        proxy_pass          http://localhost:3102/;
        body_filter_by_lua_block {
            if ngx.resp.get_headers()["erratic"] == "true" then                          
                    ngx.arg[1] = "<html><body>Hi</body></html>"
            end
        }
    }
    

    Edit 2: You can’t use content_by_lua_block with proxy_pass or else your proxy wouldn’t work. Your location block should look like this (assuming X-Verbatim header is set to "false" (a string) if you’ve to override the error response body from the upstream).

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://localhost:65000;        
    
        body_filter_by_lua_block {
          if ngx.status >= 400 then
            if ngx.resp.get_headers()["X-Verbatim"] == "false" then
              local file = io.open('/usr/share/nginx/error.html', 'w')
              local html_text = file:read("*a")
              ngx.arg[1] = html_text
              ngx.arg[2] = true
            end
          end
    }
    

    }

    Login or Signup to reply.
  2. This is somewhat opposite of the requested but I think it can fit anyway. It shows the original response unless upstream says what to show.

    There is a set of X-Accel custom headers that are evaluated from upstream responses. X-Accel-Redirect allows you to tell NGINX to process another location instead. Below is an example how it can be used.

    This is a Flask application that gives 50/50 normal responses and errors. The error responses come with X-Accel-Redirect header, instructing NGINX to reply with contents from the @error_page location.

    import flask
    import random
    
    application = flask.Flask(__name__)
    
    
    @application.route("/")
    def main():
        if random.randint(0, 1):
            resp = flask.Response("Random error")  # upstream body contents
            resp.headers['X-Accel-Redirect'] = '@error_page'  # the header
            return resp
        else:
            return "Normal response"
    
    
    if __name__ == '__main__':
        application.run("0.0.0.0", port=4000)
    

    And here is a NGINX config for that:

    server {
      listen 80;
    
      location / {
        proxy_pass http://localhost:4000/;
      }
      location @error_page {
        return 200 "That was an error";
      }
    }
    

    Putting these together you will see either "Normal response" from the app, or "That was an error" from the @error_page location ("Random error" will be suppressed). With this setup you can create a number of various locations (@error_502, @foo, @etc) for various errors and make your application to use them.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search