Reverse proxy with path from handle_response

1. The problem I’m having:

Hello Caddy community, I’m attempting to offload some proxy work to Caddy from a backend service. The backend replies with some headers that include the hostname and path that are then passed to a reverse_proxy wrapped in a handle_response block.

Ideally I would like to handle an initial request, i.e some hostname (doesn’t matter), which is then reverse proxied to the backend in order to perform the lookup (the backend needs the host header to perform the lookup). A response handler should then dissect the backend headers and proxy them to the correct backend with the appropriate path.

The main goal is to offload the proxy traffic that the backend app would normally handle, and instead have Caddy perform the actual proxying based on the values it receives.

Request (some hostname) → Caddy → Reverse proxy to backend → Handle response based on returned headers → Reverse proxy to destination service.

The backend will respond with 2 headers (backend code below) Location and Path, which are then supposed to be routed to another proxy in the handle_response block:

    'Location' : 'cloudflare-ipfs.com',
    'Path': '/ipfs/QmQhCuJqSk9fF58wU58oiaJ1qbZwQ1eQ8mVzNWe7tgLNiD/'

However, in my testing it was revealed that Caddy is indeed proxying to the correct backend as specified by {rp.header.Location} but it is not honoring/including the {rp.header.Path} URI to pass to the reverse_proxy.

2. Error messages and/or full log output:

{"level":"error","ts":1681336024.8235428,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"41402","proto":"HTTP/2.0","method":"GET","host":"localhost:8443","uri":"/","headers":{"User-Agent":["curl/7.85.0"],"Accept":["*/*"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"user_id":"","duration":0.600057451,"size":151,"status":403,"resp_headers":{"Cf-Ray":["7b6eaa6e4ec5acf2-ATL"],"Date":["Wed, 12 Apr 2023 21:47:05 GMT"],"Content-Type":["text/html"],"Server":["Caddy","cloudflare"],"Alt-Svc":["h3=\":8443\"; ma=2592000"],"Content-Length":["151"]}}

Based on the log output, Caddy is not proxying to the path specified in {rp.header.Path}. and instead is defaulting to /.

3. Caddy version:

v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=

4. How I installed and ran Caddy:

Downloaded from Caddy site.

a. System environment:

Standalone binary on Fedora 37 for local testing.

b. Command:

caddy run

c. Service/unit/compose file:

n/a

d. My complete Caddy config:

{
	auto_https disable_redirects
}

https://:8443 {
	bind 0.0.0.0

	tls internal {
		on_demand
	}

	log {
		level DEBUG
		format json
	}

	route * {
		reverse_proxy http://localhost:3000 {
			@proxy_redirect status 301
			handle_response @proxy_redirect {
				rewrite * /{rp.header.Path}
				reverse_proxy {rp.header.Location}:443 {
					transport http {
						tls
						dial_timeout 2s
					}
				}
			}
		}
	}
}

I have also tried the following handle_response block:

			handle_response @proxy_redirect {
				@target_path path /{rp.header.Path}
				reverse_proxy @target_path {rp.header.Location}:443 {
					transport http {
						tls
						dial_timeout 2s
					}
				}
			}

5. Links to relevant resources:

Backend service responsible for returning the Path and Location headers to Caddy:

const http = require('http');

const server = http.createServer((req, res, next) => {
  res.writeHead(301, {
    'Location' : 'cloudflare-ipfs.com',
    'Path': '/ipfs/QmQhCuJqSk9fF58wU58oiaJ1qbZwQ1eQ8mVzNWe7tgLNiD/'
    }
  );
  res.end();
});
server.listen(3000);

That’s essentially what the forward_auth (Caddyfile directive) — Caddy Documentation directive lets you do – it’s basically a reverse_proxy with some defaults configured to make it not terminate the request and instead pass it off to the next handler.

The typical usecase is to check the request for authentication (e.g. via Cookie or Authorization headers), but you can use it to populate a request header which would contain the upstream address instead.

You would need to make sure that the upstream address and request path+query are separate, because you’d need to feed the upstream address to reverse_proxy, and the path+query to rewrite.

An alternate approach to this is to implement your own dynamic upstreams module which could do a lookup against your database (or whatever) from within Caddy itself to find out where to pass the request.

Make sure the enable the debug global option when playing with this, it’ll show lots more detail in your logs about the proxy traffic and the rewrites happening.

1 Like

Thank you, I will give this a try and report back!

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