Nginx 作为反向代理:如何显示上游错误的自定义错误页面,除非上游拒绝?

Nginx as reverse proxy: How to display a custom error page for upstream errors, UNLESS the upstream says not to?

我有一个 Nginx 实例 运行 作为反向代理。当上游服务器没有响应时,我为 502 响应代码发送一个自定义错误页面。当上游服务器发送一个错误页面时,它会被转发给客户端,在这种情况下我也想显示一个自定义错误页面。

如果我想替换上游服务器的所有错误页面,我会设置 proxy_intercept_errors on 以在每个错误页面上显示一个自定义页面。但是,在某些情况下,我希望 return 上游服务器发送的实际响应:例如,对于 API 端点,或者如果错误页面具有特定的 user-readable 相关文本到这个问题。

在配置中,一个 server 正在代理多个应用程序,这些应用程序在它们自己的代理设置和它们自己的转发请求规则后面,所以我不能只为每个 location,它必须适用于任何匹配 server.

的 URL

因此,我想发送自定义错误页面,除非上游应用程序拒绝。最简单的方法是使用自定义 HTTP header。关于 doing this depending on the request headers 也有类似的问题。有没有办法根据 响应 header 来做到这一点?

(看来 somebody else already had this question 并且他们的结论是使用普通的 Nginx 是不可能的。如果这是真的,我会对其他一些关于如何解决这个问题的想法感兴趣,可能像那个人一样使用 OpenResty做了。)

到目前为止,我已尝试使用 OpenResty 来执行此操作,但它似乎与 proxy_pass 不兼容:Lua 代码生成的响应似乎覆盖了上游服务器的响应.

这是我尝试使用的 location 块:

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
    }
}

我认为您不能根据响应 header 发送自定义错误页面,因为据我所知,您可以使用 mapif 指令。由于这两个指令在请求发送到上游后都没有作用域,因此它们不可能读取响应 header.

但是,您可以使用 openresty 并编写自己的 lua 脚本来完成此操作。执行此类操作的 lua 脚本类似于:

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
  ';

  #
  .
  .
  .
}

您也可以使用 body_filter_by_lua_block(您可以将您的 lua 代码包含在花括号中,而不是写成 nginx 字符串)或 body_filter_by_lua_file(您可以编写您的 lua在单独的文件中编写代码并提供文件路径)。

您可以找到如何开始使用 openresty here

P.S.: 可以使用ngx.status从上游读取响应状态码。就读取 body 而言,变量 ngx.arg[1] 将包含我们在此处修改的来自上游的响应之后的响应 body。您可以将 ngx.arg[1] 保存在局部变量中,并尝试使用一些正则表达式从中读取错误消息并稍后附加到 html_text 变量中。希望对您有所帮助。

编辑 1: 将示例工作 lua 块粘贴到具有 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
    }
}

编辑 2: 您不能将 content_by_lua_blockproxy_pass 一起使用,否则您的代理将无法工作。您的位置块应如下所示(假设 X-Verbatim header 设置为“false”(字符串),如果您必须覆盖来自上游的错误响应 body。

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
}

}

这与要求的有点相反,但我认为它无论如何都适合。它显示原始响应除非上游说明要显示什么

有一组 X-Accel 自定义 header 是根据上游响应评估的。 X-Accel-Redirect 允许您告诉 NGINX 处理另一个位置。下面是一个如何使用它的例子。

这是一个给出 50/50 正常响应和错误的 Flask 应用程序。错误响应带有 X-Accel-Redirect header,指示 NGINX 使用 @error_page 位置的内容进行回复。

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)

这是一个 NGINX 配置:

server {
  listen 80;

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

将它们放在一起,您将看到来自应用程序的“正常响应”,或来自 @error_page 位置的“这是一个错误”(“随机错误”将被抑制)。使用此设置,您可以为各种错误创建多个不同的位置(@error_502@foo@etc)并让您的应用程序使用它们。