How to percent-encode request URL when redirecting?

1. The problem I’m having:

I’d like to have Caddy reverse proxy connections to a backend service, while leveraging the forward_auth directive along with oauth2-proxy to handle authentication.

The oauth2-proxy docs provide an example Caddyfile to demontrate how to integrate with Caddy. In the linked example, the client is redirected to /oauth2/sign_in with the original URL supplied via the rd (redirect) query parameter like this:

redir * /oauth2/sign_in?rd={scheme}://{host}{uri}

However, this is flawed because if the original URI has a query string, whatever endpoint receives the redirected request will see an rd parameter containing the original URL with all query parameters missing, except for the first. This is because the uri placeholder is not percent-encoded. Please see the concrete example in sections 4b, 4d and 2 to better see what I mean.

In short, how can I perform a redirect like

redir * /oauth2/sign_in?rd={encoded_url}

where {encoded_url} is the request URL that has been percent-encoded (or whatever encoding is appropriate when part of a query string)?

2. Error messages and/or full log output:

FRONTEND_PORT=6348

curl -L -- "http://localhost:${FRONTEND_PORT}/path?k1=v1&k2=v2"

Output:

rd: http://localhost:6348/path?k1=v1

What’s the recommended way to perform the redirection in the Caddyfile in section 4d so that the following is output instead?

rd: http://localhost:6348/path?k1=v1&k2=v2

3. Caddy version:

Version

v2.10.1-0.20250508175407-44d078b6705c h1:l8ZfoDLbL98cLNBAbjdj/T3fkH3BCO0H42H6lczzL9k=

4. How I installed and ran Caddy:

a. System environment:

  • OS: macOS 15.4.1
  • CPU architecture: arm64

b. Command:

xcaddy build 44d078b6705c7abcabb2a60f501568ff7f5a57a1

FRONTEND_PORT=6348
OAUTH2_PROXY_PORT=6349

env -- \
  FRONTEND_PORT="${FRONTEND_PORT}" \
  OAUTH2_PROXY_PORT="${OAUTH2_PROXY_PORT}" \
  ./caddy run --config Caddyfile

c. Service/unit/compose file:

Not applicable.

d. My complete Caddy config:

{
	admin off
}

:{$FRONTEND_PORT} {
	handle /oauth2/* {
		reverse_proxy http://localhost:{$OAUTH2_PROXY_PORT}
	}

	handle {
		forward_auth http://localhost:{$OAUTH2_PROXY_PORT} {
			uri /oauth2/auth

			@unauthorized status 401
			handle_response @unauthorized {
				redir * /oauth2/sign_in?rd={scheme}://{hostport}{uri}
			}
		}
	}
}

:{$OAUTH2_PROXY_PORT} {
	handle /oauth2/auth {
		respond 401
	}

	handle /oauth2/sign_in {
		respond "rd: {query.rd}"
	}
}

5. Links to relevant resources:

1 Like

Just recently, I had a similar requirement myself, so I added the placeholder {extra.http.request.url.query_escaped} to my plugin caddy-extra-placeholders:

Placeholder Description
{extra.http.request.url.query_escaped} The full URL of the HTTP request in query-escaped form, safe for use in query strings.

Hope this helps.

2 Likes

Thank you! I got this working, although it took me a while to realize that extra_placeholders must be included in the site block.

Caddyfile:

{
        admin off
}

:{$FRONTEND_PORT} {
        extra_placeholders {
                disable_loadavg_placeholders
        }

        handle /oauth2/* {
                reverse_proxy http://localhost:{$OAUTH2_PROXY_PORT}
        }

        handle {
                forward_auth http://localhost:{$OAUTH2_PROXY_PORT} {
                        uri /oauth2/auth

                        @unauthorized status 401
                        handle_response @unauthorized {
                                redir * /oauth2/sign_in?rd={extra.http.request.url.query_escaped}
                        }
                }
        }
}

:{$OAUTH2_PROXY_PORT} {
        handle /oauth2/auth {
                respond 401
        }

        handle /oauth2/sign_in {
                respond "rd: {query.rd}"
        }
}

Build and run Caddy:

xcaddy build 44d078b6705c7abcabb2a60f501568ff7f5a57a1 \
  --with=github.com/steffenbusch/caddy-extra-placeholders@0ded7e965fe19b5bbb731392aa94620bf30bea55

FRONTEND_PORT=6348
OAUTH2_PROXY_PORT=6349

env -- \
  FRONTEND_PORT="${FRONTEND_PORT}" \
  OAUTH2_PROXY_PORT="${OAUTH2_PROXY_PORT}" \
  ./caddy run --config Caddyfile

curl:

FRONTEND_PORT=6348

curl -L -- "http://localhost:${FRONTEND_PORT}/path?k1=v1&k2=v2"

curl output:

rd: http://localhost:6348/path?k1=v1&k2=v2

Awesome. Glad it works for you.
You might want to move the directive into the handle_response @unauthorized { if the extra placeholders are only used there.

1 Like

Thanks, will do. Out of curiosity, is there any performance benefit in doing so?

Most likely no measurable performance benefit – but if the plugin is not part of the middleware chain, it won’t be invoked, which saves a few CPU cycles.

1 Like