Using X-Accel-Redirect and intercept in 2.10.2

1. The problem I’m having:

I’m trying to convert an nginx config with an X-Accel-Redirect mechanism for thumbnails. The idea is that the back end only gets involved when requesting an image thumbnail for the first time, at which point it writes to a static image folder (future requests will hit that), and also returns the generated file via the X-Accel-Redirect mechanism. I’m running Caddy 2.10.2 (which I note is over 6 months old, though still the latest release).

I gather that handle_response can’t be used as it doesn’t replace the request properly like nginx does, and the caddy docs say that I should use intercept, but I can’t make the syntax work.

I’m basing it on the example from intercept (Caddyfile directive) — Caddy Documentation

I’m wondering if I have got the syntax correct, but perhaps intercept is not in the 2.10.2 build?

2. Error messages and/or full log output:

When I try to enable this config, it fails with this error:

Error: adapting config using caddyfile: parsing caddyfile tokens for 'handle': parsing caddyfile tokens for 'handle': parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective intercept

3. Caddy version:

v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=

4. How I installed and ran Caddy:

From stock Ubuntu packages, as per docs.

a. System environment:

Ubuntu 22.04 on aarch64.

b. Command:

c. Service/unit/compose file:

Default from stock package

d. My complete Caddy config:

api.chamsocial.com {
	# Compression
	encode gzip zstd

	# Limit upload/request body size
	request_body {
		max_size 4GB
	}

	# Access log (file)
	log {
		output file /var/log/caddy/api.chamsocial.access.log
	}

	# Security headers
	header {
		Alt-Svc "h3=\":443\"; ma=86400"
		Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
		X-Frame-Options "SAMEORIGIN"
		X-Content-Type-Options "nosniff"
		X-XSS-Protection "1; mode=block"
		Referrer-Policy "strict-origin-when-cross-origin"
		Permissions-Policy "midi=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), accelerometer=(), fullscreen=(self), payment=(), geolocation=(), usb=(), encrypted-media=()"
	}

	root * /var/www/www.chamsocial.com/public

	# /thumb: serve from chamsocial-images if file exists, otherwise proxy to backend
	handle /thumb/* {
		root * /var/www/chamsocial-images

		@thumbfile file
		handle @thumbfile {
			# Serve static thumb images
			file_server
		}

		# Fallback to backend with X-Accel-Redirect support
		handle {
			reverse_proxy http://localhost:7440 {
				header_up X-Real-IP {remote}
				header_up Host {host}
				header_up X-NginX-Proxy 1
				transport http {
					versions 1.1
				}

				@accel header X-Accel-Redirect *
				intercept @accel {
					rewrite {http.response.header.X-Accel-Redirect}
					root * /var/www/chamsocial-images
					file_server
				}
			}
		}
	}
}

I’m not sure what you mean by this.

If you scroll down a bit here:

you’ll find an example of your use case using handle_response:

example.com {
	reverse_proxy localhost:8080 {
		@accel header X-Accel-Redirect *
		handle_response @accel {
			root    * /path/to/private/files
			rewrite * {rp.header.X-Accel-Redirect}
			method  * GET
			file_server
		}
	}
}

However, if you insist on using the intercept directive, you’ll need to move it outside of the reverse_proxy.

Yes, that’s what I’ve been trying to use, but it’s not working. It never matches, the image is not served, and the X-Accel-Redirect header is visible to the client.

I’m not “insisting” on using intercept, I’m trying to do what the docs recommend. It’s also unclear whether intercept exists in the current release as the docs do not provide versioning/compatibility info.

/var/www/internal/static/test.js

test = "Test"

Caddyfile

{
	debug
}

## Main site
example.com {
	tls internal

	reverse_proxy 127.0.0.1:8080 {
		@accel header X-Accel-Redirect *
		handle_response @accel {
			root    * /var/www/internal
			rewrite * {rp.header.X-Accel-Redirect}
			method  * GET
			file_server
		}
	}
}

## Upstream placeholder
:8080 {
	header X-Accel-Redirect /static/test.js
	respond "Foo"
}

Test result:

$ curl https://example.com/
test = "Test"

$ curl https://example.com/ -I
HTTP/2 200
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000
content-type: text/javascript; charset=utf-8
etag: "dgd5wxboy9cfe"
last-modified: Thu, 12 Feb 2026 17:34:10 GMT
server: Caddy
vary: Accept-Encoding
content-length: 14
date: Thu, 12 Feb 2026 17:39:33 GMT

Log:

2026/02/12 17:39:33.886	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "127.0.0.1:8080", "total_upstreams": 1}
2026/02/12 17:39:33.887	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "127.0.0.1:8080", "duration": 0.0009215, "request": {"remote_ip": "127.0.0.1", "remote_port": "62476", "client_ip": "127.0.0.1", "proto": "HTTP/2.0", "method": "HEAD", "host": "example.com", "uri": "/", "headers": {"X-Forwarded-Host": ["example.com"], "Via": ["2.0 Caddy"], "User-Agent": ["curl/8.18.0"], "Accept": ["*/*"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["https"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "example.com"}}, "headers": {"Content-Length": ["3"], "Content-Type": ["text/plain; charset=utf-8"], "Server": ["Caddy"], "X-Accel-Redirect": ["/static/test.js"], "Date": ["Thu, 12 Feb 2026 17:39:33 GMT"]}, "status": 200}
2026/02/12 17:39:33.888	DEBUG	http.handlers.reverse_proxy	handling response	{"upstream": "127.0.0.1:8080", "duration": 0.0009215, "request": {"remote_ip": "127.0.0.1", "remote_port": "62476", "client_ip": "127.0.0.1", "proto": "HTTP/2.0", "method": "HEAD", "host": "example.com", "uri": "/", "headers": {"X-Forwarded-Host": ["example.com"], "Via": ["2.0 Caddy"], "User-Agent": ["curl/8.18.0"], "Accept": ["*/*"], "X-Forwarded-For": ["127.0.0.1"], "X-Forwarded-Proto": ["https"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "example.com"}}, "handler": 0}
2026/02/12 17:39:33.888	DEBUG	http.handlers.rewrite	rewrote request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "62476", "client_ip": "127.0.0.1", "proto": "HTTP/2.0", "method": "GET", "host": "example.com", "uri": "/", "headers": {"User-Agent": ["curl/8.18.0"], "Accept": ["*/*"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "example.com"}}, "method": "GET", "uri": "/"}
2026/02/12 17:39:33.888	DEBUG	http.handlers.rewrite	rewrote request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "62476", "client_ip": "127.0.0.1", "proto": "HTTP/2.0", "method": "GET", "host": "example.com", "uri": "/static/test.js", "headers": {"Accept": ["*/*"], "User-Agent": ["curl/8.18.0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "example.com"}}, "method": "GET", "uri": "/static/test.js"}
2026/02/12 17:39:33.888	DEBUG	http.handlers.file_server	sanitized path join	{"site_root": "/var/www/internal", "fs": "", "request_path": "/static/test.js", "result": "/var/www/internal/static/test.js"}
2026/02/12 17:39:33.888	DEBUG	http.handlers.file_server	opening file	{"filename": "/var/www/internal/static/test.js"}

Note the last two lines in the log output. When debug is enabled, they show where the file is eventually being pulled from.

It exists as a separate directive, not part of the reverse_proxy directive. It’s not meant to be nested within the reverse_proxy directive.

Thanks. I’ve managed to get it working now. It didn’t help that I’d missed that header name matching is case-sensitive, and the header was being set in lower case, but the matcher was mixed.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.