Reverse proxy - how to log http-content of response?

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?

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.

Forgot I had written an entire blog about this. It’s overkill for what you want, but includes the response body logging.

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.

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’

How do you feel about building the njs module from source then? :slight_smile:

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

OK! Let’s do it the hard way :slight_smile:

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).

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

Yay! That’s awesome - I guessed it was a Raspberry Pi :slight_smile:

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?

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!

Glad you got something! The PROPFIND method might also help you, as an extension of WebDAV. Good luck!