Re: [openresty-en] body_filter_by_lua gets empty body on second request when used in a caching reverse proxy setup

777 views
Skip to first unread message

Yichun Zhang (agentzh)

unread,
Aug 8, 2015, 5:49:22 AM8/8/15
to openresty-en
Hello!

On Thu, Aug 6, 2015 at 11:35 PM, Stefan Wille wrote:
> My setup works as long as I disable caching. If I enable caching, only the
> first request works. In the following requests, the response always has an
> empty body in body_filter_by_lua. It comes out fine at the browser though.
>

Short answer: your Lua code in body_filter_by_lua is buggy.

Long answer: the ngx.arg[1] (data chunk string) and ngx.arg[2] (the
eof flag) can both be set. And this is exactly the case with cache
hits in proxy_cache. Your Lua code did not take this case into account
and failed to set ngx.ctx.buffer with ngx.arg[1].

In addition, you should really really avoid concatenating a
potentially large number of Lua strings via ".." operator, which is
very expensive. Instead, you should use a Lua table to collect all the
string pieces and call table.concat() at last.

Finally, you should avoid calling ngx.ctx repeatedly because it
involves expensive metamethod invocation. Better cache the ngx.ctx
table in your own local Lua variables.

The following is a standalone example based on your example with the
aforementioned issues fixed:

http {
proxy_cache_path /tmp/cache levels=1:2 keys_zone=cache:60m max_size=1G;
server {
listen 8080;

location /replace-body {
proxy_pass http://127.0.0.1:$server_port/;
#echo a;
#echo b;
#echo c;
#echo d;

proxy_cache cache;
proxy_cache_valid 200 30s;

# Reset the response's content_length, so that Lua can generate a
# body with a different length.
header_filter_by_lua 'ngx.header.content_length = nil';

body_filter_by_lua '
local ctx = ngx.ctx
if ctx.buffers == nil then
ctx.buffers = {}
ctx.nbuffers = 0
end

local data = ngx.arg[1]
local eof = ngx.arg[2]
local next_idx = ctx.nbuffers + 1

if not eof then
if data then
ctx.buffers[next_idx] = data
ctx.nbuffers = next_idx
-- Send nothing to the client yet.
ngx.arg[1] = nil
end
return
elseif data then
ctx.buffers[next_idx] = data
ctx.nbuffers = next_idx
end

-- Yes, we have read the full body.
-- Make sure it is stored in our buffer.
assert(ctx.buffers)
assert(ctx.nbuffers ~= 0, "buffer must not be empty")

-- And send a new body
ngx.arg[1] = "Cool... " .. table.concat(ngx.ctx.buffers)
';
}
}
}

To try it out:

$ curl localhost:8080/replace-body
Cool... <html><head><title>It works!</title></head><body>It
works!</body></html>

$ curl localhost:8080/replace-body
Cool... <html><head><title>It works!</title></head><body>It
works!</body></html>

$ curl localhost:8080/replace-body
Cool... <html><head><title>It works!</title></head><body>It
works!</body></html>

It now works as expected.

It's worth noting that we use a custom ctx.nbuffers field to keep
track of the table length ourselves. This is a common trick of
optimization since the `#' operator on Lua tables is not an O(1)
operation and is potentially slow to be executed for many times.

To test the multi-chunk cases more reliably, we can use a series of
echo directives in the location in place of proxy_pass (see the
commented "echo" lines above).

I must say your example has another design issue. That is, if all you
need is to prepend something to the response body, then you really
really don't want to buffer all the response body data on the Lua
land. You only need to overwrite the first data chunk you see in the
body filter and leave subsequent data chunks (if any) intact. Or maybe
your example is just an artificial one that simply tests an idea?

I must say that if you always have to buffer *all* the response data
in memory in the body filter, then you shouldn't use the body filter
in the first place. Instead you should just use ngx.location.capture
and an internal location configured by proxy_pass and etc. All the
points in using a body filter is that you can avoid buffering all the
stream data and do streaming processing.

Oh, it's worth mentioning that the language name is Lua rather than
LUA. See http://www.lua.org/about.html#name

Best regards,
-agentzh

Stefan Wille

unread,
Aug 10, 2015, 3:29:35 PM8/10/15
to openresty-en
Cool, that was very helpful! Thanks a lot!

Best regards,
Stefan

Yuri M. Goltsev

unread,
Jul 26, 2016, 12:51:36 PM7/26/16
to openresty-en
Hi, 
I think I have the same issue, but I still can't solve it.

I'm using nginx as a frontend (proxy_pass) and all I need - just get a response body from backend and check with regexp if it is ok or not.

I'm trying to get a response body using ngx.arg[1]

function body()
    ngx.var.resp_body = ngx.var.resp_body .. ngx.arg[1]
end

I call body() function until ngx.arg[2] appears.

When I recieve body, I start to work with it:

if ngx.arg[2] then
    do_work()
end

But unfortunatly, I recieve body only with first reqeust, after that, I recieve nothing.

What could be wrong? Is body cached somewhere in another variable? 

Yuri M. Goltsev

unread,
Jul 27, 2016, 6:08:40 AM7/27/16
to openresty-en
In this case server returns 304 status code.
In this case I can get a content only by modifying original request headers (by excluding If-Modified-Since,Cache-Control, etc) or there is another more suitable approach?

вторник, 26 июля 2016 г., 18:51:36 UTC+2 пользователь Yuri M. Goltsev написал:

Yichun Zhang (agentzh)

unread,
Jul 29, 2016, 3:47:11 PM7/29/16
to openresty-en
Hello!

On Wed, Jul 27, 2016 at 3:08 AM, Yuri M. Goltsev wrote:
> In this case server returns 304 status code.
> In this case I can get a content only by modifying original request headers
> (by excluding If-Modified-Since,Cache-Control, etc) or there is another more
> suitable approach?
>

It seems to me that you are confused yourself. Maybe you should learn
more about 304 and etc first?

Regards,
-agentzh

Yuri M. Goltsev

unread,
Jul 29, 2016, 4:26:29 PM7/29/16
to openresty-en
That's true, was confused because of configuration changes and only after that determined that backend responds with 304 code. Thanks!
Reply all
Reply to author
Forward
0 new messages