OpenResty 使用body_filter_by_lua* 修改返回内容
背景
测试开发环境所有前端页面需要嵌入一段前端脚本,方便开发者在手机浏览器里调试,类似小程序调试的功能。
谈谈 body_filter_by_lua*
多次调用的问题
正如 OpenResty 文档中指出,body_filter_by_lua* 可能会在一次请求中多次调用。
Nginx output filters may be called multiple times for a single request because response body may be delivered in chunks.
Thus, the Lua code specified by in this directive may also run multiple times in the lifetime of a single HTTP request.
文档中举了个例子:
1 |
|
body_filter_by_lua*
首次调用时,ngx.arg[1]
的值只是 hello world
,不包括下面的 hiya globe
。
不过初看下来,很难将 echo hello world
; 和 delivered in chunks
联系起来。这个 chunk
的大小是怎么确定的?看例子,应该跟 echo/ngx.say
这一类输出方式有关。但是会不会跟输出的大小也有关?如果我一次性 ngx.say
了很多内容,是否会分成多个 chunks
发送?如果响应来自上游服务器,chunks
的数目又怎么定?
要回答这个问题,需要看看 Nginx
内响应内容的组织方式。Nginx
上游产生的内容,存储为 ngx_chain_t
类型的数据。这其实是一条 ngx_buf_t
链表。很容易可以想像到,这个链表就代表着数据流。上游产生的内容,像流水线上的包裹一样,不停地向下游传递。output filter
阶段像流水线上的机器,处理这些“包裹”。跟流水线上的机器不同的是,Nginx
中的 output filter
并非逐个处理这些“包裹”,而是一批一批地处理。上游成批成批地生产出这些包裹,每批包裹构成 ngx_chain_t
的子串,而 output fiter
则遍历这一子串,把其中的每个包裹打开处理。
想到 body_filter_by_lua*
其实属于 output filter
的一种,我们就回到了一开始讨论的问题。既然 body_filter_by_lua*
是一批一批处理上游的响应,那么它的调用次数就取决于上游的响应次数。上游的一次响应,如一次 ngx.say
,会产生一个 ngx_chain_t
的子串(就 ngx.say
而言,这个子串仅包含单个 ngx_buf_t
)。至于响应的大小,最多只会影响到子串的长短,具体情况则取决于具体实现。
以我们常用的 ngx.say 为例:
1 |
|
以上几个字符串会通过栈从 lua
域传递给 C 域。接着 OpenResty
计算它们的总长度,从 buffer chain
中找出一个空闲的大小合适的 ngx_buf_t,把它们拷贝进来。 之后就走 http_output_filter
把这个 ngx_buf_t
(准确来说,是它所在的链表)发送出去。
那么,上游什么时候会把数据发完了?Nginx
采用了一个 last_buf
的标志位,如果某个 ngx_buf_t
是链表中的最后一个,跟上游交互的模块会设置这一个标志位为1. 映射回 OpenResty
的 lua
域,则是 body_filter_by_lua*
中的 ngx.arg[2]
。你可能会注意到,last_buf
是一个设置在 ngx_buf_t
上的标志位,而传递给 output filter
的是 ngx_chain_t
。OpenResty
把这一差别隐藏在实现之下——它会遍历当前输入的子串,如果某个 ngx_buf_t
存在 last_buf
,那么就返回 true。
自行尝试
1 |
|
尽管这里只有两个 echo
,但是 body_filter_by_lua*
会被调用三次!第三次调用的时候,ngx.arg[1]
为空字符串,而 ngx.arg[2]
为 true
。这是因为 Nginx
的 upstream
相关模块,以及 OpenResty
的 content_by_lua
,会单独发送一个设置了 last_buf
的空 buffer
,来表示流的结束。所以我们需要在运行相关逻辑之前,检查 ngx.arg[1]
是否为空,但是需要注意的是 ngx.arg[2]=true
并不代表 ngx.arg[1]
一定为空。
也许你已经发现了,子请求也会走到 body_filter_by_lua*
的流程。严格意义上,如果只希望 body_filter_by_lua*
修改响应给客户端的内容,需要额外用 ngx.is_subrequest
判断下:
1 |
|
官方文档也有一段说明,源码链接地址:body_filter_by_lua
The input data chunk is passed via ngx.arg[1] (as a Lua string value) and the “eof” flag indicating the end of the response body data stream is passed via ngx.arg[2] (as a Lua boolean value).
流每次的内容输出在 ngx.arg[1]
中; eof
最后的标记在 ngx.arg[2]
中, 所以你要改输出内容那么就把 ngx.arg[1]
改掉,如果不想要以后的内容了那么 ngx.arg[2]=true
就行.
需要注意的地方
还有一个特别需要注意地方是,当代码运行到 body_filter_by_lua*
时,HTTP
报头(header)已经发送出去了。如果在之前设置了跟响应体相关的报头,而又在 body_filter_by_lua*
中修改了响应体,会导致响应报头和实际响应的不一致。举个简单的例子:假设上游的服务器返回了 Content-Length
报头,而 body_filter_by_lua*
又修改了响应体的实际大小。客户端收到这个报头后,按其中的 Content-Length
去处理,顺着一头栽进坑里。由于 Nginx
的流式响应,发出去的报头就像泼出去的水,要想修改只能提前进行。OpenResty
提供了跟 body_filter_by_lua*
相对应的 header_filter_by_lua*
。header_filter
会在 Nginx
发送报头之前调用,所以可以在这里置空 Content-Length
报头:
1 |
|
现在 Nginx
会代以 Transfer-Encoding: chunked
,再也不会误导客户端了。同样可能需要处理的还有 accept-range
和 etag
等跟响应体相关的报头。HTTP1.1 之后基于流式处理的方式,body_filter_by_lua
基本在一个请求中会调用多次。 简单直白的理解就是流式输出,每次拿到了如果你要处理那么就处理,不处理就输出!!
总结
body_filter_by_lua*
可能在一次请求中调用多次,跟响应数据量无关,取决于响应次数body_filter_by_lua*
的最后一次调用时,ngx.arg[1]
一般为空字符串body_filter_by_lua*
也会在subrequest
之中调用body_filter_by_lua*
有些时候离不开有header_filter_by_lua*
辅佐