After setting up the reverse proxy I’d like to log the response.
My issue:
I’m doing some reverse engineering of radicale CalDAV . It operates within a docker-container and reverse proxy nginx is passing requests there. After getting symlinks right I get a nice access-log.
But now I’d like to see (read) radicale’s replies. Is there a way to make nginx log what the radicale-container replies?
liam
March 28, 2026, 5:26pm
2
You can do this with the JavaScript module (njs) which has a convenient js_body_filter directive that can be used to create a variable with the response as it is streamed back from the upstream server.
You could either dump the response directly into the error log with njs or create a variable that can be escaped and included in the access log.
liam
March 28, 2026, 5:49pm
3
Forgot I had written an entire blog about this. It’s overkill for what you want, but includes the response body logging.
We show how to use the NGINX JavaScript module to capture data about requests that cause errors, with enough details to be useful for debugging and troubleshooting, while not cluttering the log with this information about requests that didn't...
Thx, I had a go on your suggestion but ended up with some js-foo.
root@pi5:/home/pi# nginx -t
2026/03/29 19:45:18 [emerg] 1568759#1568759: unknown directive "js_import" in /etc/nginx/sites-enabled/default.conf:1
nginx: configuration file /etc/nginx/nginx.conf test failed
root@pi5:/home/pi# nginx -v
nginx version: nginx/1.22.1
An old-fashioned directive js_include was just the same.
liam
March 29, 2026, 6:13pm
5
That means you don’t have the njs module loaded. If your nginx is in a container based on the Docker Official Image then you need to add to your nginx.conf (top-level, main context)
load_module modules/ngx_http_js_module.so;
If not, then see if you already have ngx_http_js_module.so on your system.
If not, you should be able to get it from your package manager.
1 Like
I should have mentioned my architecture binary-armhf that stops me with … Skipping acquire of configured file 'nginx/binary-armhf/Packages' as repository 'http://nginx.org/packages/debian bookworm InRelease' doesn’t support architecture ‘armhf’
liam
March 30, 2026, 8:08am
7
How do you feel about building the njs module from source then?
There’s a script for that
build_module.sh -v 1.22.1 https://github.com/nginx/njs.git
1 Like
Thank you for your support. Script looked well until …
build_module.sh: INPUT: Enter module nickname [njs]:
build_module.sh: INFO: Creating /tmp/build_module.sh.1663126 build area
build_module.sh: INFO: Cloning module source
Cloning into ‘/tmp/build_module.sh.1663126/njs’…
remote: Enumerating objects: 19449, done.
remote: Counting objects: 100% (997/997), done.
remote: Compressing objects: 100% (481/481), done.
remote: Total 19449 (delta 728), reused 516 (delta 516), pack-reused 18452 (from 3)
Receiving objects: 100% (19449/19449), 22.20 MiB | 7.35 MiB/s, done.
Resolving deltas: 100% (15317/15317), done.
build_module.sh: ERROR: Cannot locate module config file - quitting
liam
March 30, 2026, 10:24am
9
OK! Let’s do it the hard way
Install the build tools
# apt-get install wget git gcc make libpcre2-dev zlib1g-dev libssl-dev libxslt1-dev
Clone the njs repo
$ git clone https://github.com/nginx/njs
Get the nginx sources that match the version you have installed. This way we don’t modify your existing nginx install in any way.
$ wget -O - http://nginx.org/download/`nginx -v 2>&1 | tr / - | cut -f3 -d' '`.tar.gz | tar xfz -
Build the njs module
$ cd nginx-*
$ ./configure --with-compat --add-dynamic-module=../njs/nginx
$ make modules
You’ll find two modules (.so files for http_js and stream_js) in the objs directory. Copy them to /etc/nginx/modules
Don’t forget to add the load_module directive to point at the http_js_module file.
2 Likes
Uh, always coming back with pointless errors such as
root@pi5:~/njs-repo# wget -O - http://nginx.org/download/`nginx -v 2>&1 | tr / - | cut -f3 -d' '`.tar.gz | tar xfz
tar: Old option 'f' requires an argument.
Try 'tar --help' or 'tar --usage' for more information.
--2026-03-30 13:28:29-- http://nginx.org/download/nginx-1.28.3.tar.gz
Resolving nginx.org (nginx.org)... 3.125.197.172, 2a05:d014:5c0:2600::6, 2a05:d014:5c0:2601::6
Connecting to nginx.org (nginx.org)|3.125.197.172|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1284562 (1.2M) [application/octet-stream]
Saving to: ‘STDOUT’
- 0%[ ] 0 --.-KB/s in 0s
Cannot write to ‘-’ (Broken pipe).
liam
March 30, 2026, 11:47am
11
Oops - copy/paste error on my end. Needs a trailing - on the tar command to read from stdin. I’ve edited the post with the fix.
1 Like
Wow, thanks. It worked but just wrote ngx_http_js_module.so into objs.
But my brave little nginx is fine.
root@pi5:/etc/nginx# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@pi5:/etc/nginx# service nginx restart
root@pi5:/etc/nginx# service nginx status
● nginx.service - nginx - high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-03-30 14:11:05 CEST; 4s ago
Docs: https://nginx.org/en/docs/
Process: 1685167 ExecStart=/usr/sbin/nginx -c ${CONFFILE} (code=exited, status=0/SUCCESS)
Main PID: 1685168 (nginx)
Tasks: 5 (limit: 19359)
CPU: 39ms
CGroup: /system.slice/nginx.service
├─1685168 "nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf"
├─1685169 "nginx: worker process"
├─1685170 "nginx: worker process"
├─1685171 "nginx: worker process"
└─1685172 "nginx: worker process"
Mar 30 14:11:05 pi5 systemd[1]: Starting nginx.service - nginx - high performance web server...
Mar 30 14:11:05 pi5 systemd[1]: Started nginx.service - nginx - high performance web server.
Thank you!
1 Like
liam
March 30, 2026, 12:53pm
13
Yay! That’s awesome - I guessed it was a Raspberry Pi
Good luck with the body read…
Hey Lian,
you’re right, I’m trying to pimp my Raspberry. Though accessing replies’ content is almost done there is just the next JavaScript-related issue. I modified your example to
function kvAccess(r) {
var log = `${r.variables.time_iso8601} client=${r.remoteAddress} method=${r.method} uri=${r.uri} status=${r.status}`;
// r.rawHeadersIn.forEach(h => log += ` in.${h[0]}=${h[1]}`);
// r.rawHeadersOut.forEach(h => log += ` out.${h[0]}=${h[1]}`);
// log += r.responseText;
log += ` Response body=${r.responseBody}`;
log += ` Response buffer=${r.responseBuffer}`;
log += ` Response text=${r.responseText}`;
return log;
}
export default { kvAccess }
but get this log
2026-03-30T15:52:03+02:00 client=192.168.178.40 method=GET uri=/robots.txt status=404 Response body=undefined Response buffer=undefined Response text=undefined
2026-03-30T15:52:06+02:00 client=192.168.178.40 method=GET uri=/ status=302 Response body=undefined Response buffer=undefined Response text=undefined
2026-03-30T15:52:06+02:00 client=192.168.178.40 method=GET uri=/.web status=302 Response body=undefined Response buffer=undefined Response text=undefined
2026-03-30T15:52:06+02:00 client=192.168.178.40 method=GET uri=/.web/ status=200 Response body=undefined Response buffer=undefined Response text=undefined
2026-03-30T15:52:11+02:00 client=192.168.178.40 method=GET uri=/.web/fn.js status=200 Response body=undefined Response buffer=undefined Response text=undefined
It looks like njs does not find responseText . Do I have to apply further configuration while loading ngx_http_js_module.so?
liam
March 31, 2026, 9:11am
15
responseText is only populated when there has been a r.subrequest() call.
I apologise that the blog post wasn’t as complete as I remembered. We’ll need to extend the code/config a little.
In the location with the proxy_pass directive, add the following to intercept the response body and pass it to a JS function.
js_body_filter main.getResponseBody; # Change the module name from 'main' if different
Extend the .js file to include a new function that appends a new variable res as the response is streamed back to the client.
var res = '';
function getResponseBody(r, data, flags) {
res += data;
r.sendBuffer(data, flags);
}
function kvAccess(r) {
var log = `${r.variables.time_iso8601} client=${r.remoteAddress} method=${r.method} uri=${r.uri} status=${r.status}`;
// r.rawHeadersIn.forEach(h => log += ` in.${h[0]}=${h[1]}`);
// r.rawHeadersOut.forEach(h => log += ` out.${h[0]}=${h[1]}`);
log += ` response=${res}`; // Now we can log the response
return log;
}
export default { getResponseBody, kvAccess }
Not tested -but should get you pretty close.
2 Likes
Too bad I’m lacking another solution-button. It works perfectly fine now. I get responses such as 2026-03-31T13:16:29+02:00 client=192.168.178.40 method=PROPFIND uri=/user/data/ status=207 response=\x1F\xEF\xBF\xBD\x08\x00\x00\x00\x00\x00\x00\x03\xEF\xBF\xBDT[o\xEF\xBF which needs a bit of decoding.
Thank you!
liam
March 31, 2026, 4:44pm
17
Glad you got something! The PROPFIND method might also help you, as an extension of WebDAV. Good luck!