Difficulty with getting `path_regexp` & `redir` to work together

The problem I’m having:

I am trying to redirect pages if they dont exist to another url
e.g.

  • From https://mine.org/article/{lang}?title={title}
  • To https://{lang}.other.org/article/{title}
    • However instead it is returning https://.other.org/article/{title} (Notice language is missing)
reverse_proxy mine:8080 {
  @404 status 404
  handle_response @404 {
      @has_title query title=*
      @find_lang path_regexp ^/article/([a-z]{2})/
      redir @has_title https://{re.find_lang.1}.other.org/article/{http.request.uri.query.title} permanent
  }
}

I am sure of lang being 2 characters long (e.g. “en”)
Am i doing something wrong? any help is much appreciated

Caddy version:

I am using Caddy v2.7.2

1 Like

I have to say, your question took me on a wild ride! :grinning_face_with_smiling_eyes: I couldn’t get path_regex to work with redir at first - until I realized that capture groups only work with rewrite.

First off, you’re using version v2.7.2, so this won’t work:

@find_lang path_regexp ^/article/([a-z]{2})/

As of v2.8.0, if name is not provided, the name will be taken from the named matcher’s name. For example a named matcher @foo will cause this matcher to be named foo .

You’d need to use this instead:

@find_lang path_regexp find_lang ^/article/([a-z]{2})/

Also, check your regex - it expects something like /article/en/?title=etc, but your requests are actually /article/en?title=etc.

After a bunch of testing, this was the only way I could get path_regex capture groups to work for redirection:

My test Caddyfile:

{
	http_port 8080
}

:8080 {
	@lang_title {
		path_regexp lang_title ^/article/([a-z]{2})
		query title=*
	}
	route @lang_title {
		rewrite https://{re.lang_title.1}.other.org/article/{http.request.uri.query.title}
		uri query -title
		redir {http.request.uri} 301
	}
	respond "No redirect"
}

I had to rewrite it first (since capture groups only work with rewrite), strip title from the query, and only then could I use redir. Also, note the route - without it, this won’t work.

So, here’s my test:

$ curl http://localhost:8080/article/en?title=TEST -v
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* using HTTP/1.x
> GET /article/en?title=TEST HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.12.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 301 Moved Permanently
< Location: https://en.other.org/article/TEST
< Server: Caddy
< Date: Fri, 28 Mar 2025 03:10:02 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Once I got this working, I adapted it to your use case. In my Caddyfile, :8081 is there to emulate your mine:8080. If I request English, I get 404; otherwise, I get 200.

My Caddyfile:

{
	http_port 8080
}

:8080 {
	reverse_proxy localhost:8081 {
		@404 status 404
		handle_response @404 {
			@lang_title {
				path_regexp lang_title ^/article/([a-z]{2})
				query title=*
			}
			route @lang_title {
				rewrite https://{re.lang_title.1}.other.org/article/{http.request.uri.query.title}
				uri query -title
				redir {http.request.uri} 301
			}
		}
	}
}

:8081 {
	@english path /article/en*
	respond @english "English Site Missing" 404
	respond "Other Language Site"
}

Testing English (missing language):

$ curl http://localhost:8080/article/en?title=TEST -v
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* using HTTP/1.x
> GET /article/en?title=TEST HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.12.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 301 Moved Permanently
< Location: https://en.other.org/article/TEST
< Server: Caddy
< Date: Fri, 28 Mar 2025 03:14:03 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Testing French (non-missing language):

$ curl http://localhost:8080/article/fr?title=TEST -v
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* using HTTP/1.x
> GET /article/fr?title=TEST HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.12.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Length: 19
< Content-Type: text/plain; charset=utf-8
< Date: Fri, 28 Mar 2025 03:14:23 GMT
< Server: Caddy
< Server: Caddy
<
* Connection #0 to host localhost left intact
Other Language Site

Adjust as needed - hope this helps!

I really hope there’s an easier way to do this, and if there is, I’d love to see it - because this took me embarrassingly too long to figure out! :laughing:

5 Likes

I used regexr.com to get some info on this.

Using your regexp ^/article/([a-z]{2})/ says that there is an unescaped forward slash that may cause issues if copying/pasting to code.

I think you need to add backslashes before all forward slashes, and end it with a ?, as in ^\/article\/([a-z]{2})\/?

I wonder if that will fix the problem so it correctly parses the {lang}.

Edit: Jinx, @timelordx

2 Likes

@matt , I’d love to hear your thoughts on this - especially on how path_regex capture groups work with rewrite vs redir. Is there an easier way to do this? Thank you! :folded_hands:

3 Likes

I set a reminder to look at this more tomorrow (hopefully I will get the chance :crossed_fingers: )

4 Likes

@TheRettom regex online tools always ask for the \ to escape some characters, but they are not always needed :wink:

1 Like

Hello everyone :waving_hand:

The problem was with using path_regex & redir together, had to use handle

@404 status 404
handle_response @404 {
    @has_title query title=*
    handle @has_title {
        @find_lang path_regexp find_lang ^/article/([a-z]{2})
        handle @find_lang {
            redir https://{re.find_lang.1}.other.org/article/{http.request.uri.query.title} permanent
        }
    }
}

@timelordx :hugs: thank you for the insight

5 Likes

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