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