Repository: vercel/next.js
Pull request: fix(Turbopack): Give intercept routes correct regex in generated manifest files

## Fix Interception Route Handling in Edge Runtime

### What?
This PR enhances the route regex handling for interception routes by properly preserving interception markers in the generated regex patterns.

### Why?
The current implementation doesn't correctly handle interception markers in route segments, which can lead to incorrect route matching for intercepted routes.

### How?
- Modified `get_safe_key_from_segment` to accept an optional `intercept_prefix` parameter
- Updated the regex generation to include the interception marker in the pattern
- Added a test case to verify proper handling of interception routes
- Ensured interception markers are properly preserved in the generated regex

Fixes #PACK-5246

Changed files:
filename: Cargo.lock
patch: @@ -4347,6 +4347,7 @@ dependencies = [
  "qstring",
  "react_remove_properties",
  "regex",
+ "regress",
  "remove_console",
  "rustc-hash 2.1.1",
  "serde",
@@ -5832,9 +5833,9 @@ dependencies = [
 
 [[package]]
 name = "regress"
-version = "0.10.3"
+version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ef7fa9ed0256d64a688a3747d0fef7a88851c18a5e1d57f115f38ec2e09366"
+checksum = "145bb27393fe455dd64d6cbc8d059adfa392590a45eadf079c01b11857e7b010"
 dependencies = [
  "hashbrown 0.15.4",
  "memchr",

filename: Cargo.toml
patch: @@ -406,7 +406,7 @@ quote = "1.0.23"
 rand = "0.9.0"
 rayon = "1.10.0"
 regex = "1.10.6"
-regress = "0.10.3"
+regress = "0.10.4"
 reqwest = { version = "0.12.22", default-features = false }
 ringmap = "0.1.3"
 roaring = "0.10.10"

filename: crates/next-core/Cargo.toml
patch: @@ -28,6 +28,7 @@ allsorts = { workspace = true }
 futures = { workspace = true }
 thiserror = { workspace = true }
 tracing = { workspace = true }
+regress = { workspace = true }
 rustc-hash = { workspace = true }
 react_remove_properties = { workspace = true }
 remove_console = { workspace = true }

filename: crates/next-core/src/next_edge/route_regex.rs
patch: @@ -3,6 +3,7 @@
 
 use once_cell::sync::Lazy;
 use regex::Regex;
+use regress;
 use rustc_hash::FxHashMap;
 
 const INTERCEPTION_ROUTE_MARKERS: [&str; 4] = ["(..)(..)", "(.)", "(..)", "(...)"];
@@ -64,7 +65,7 @@ fn parse_parameter(param: &str) -> ParsedParameter {
 }
 
 fn escape_string_regexp(segment: &str) -> String {
-    regex::escape(segment)
+    regress::escape(segment)
 }
 
 /// Removes the trailing slash for a given route or page path. Preserves the
@@ -164,6 +165,7 @@ fn get_safe_key_from_segment(
     segment: &str,
     route_keys: &mut FxHashMap<String, String>,
     key_prefix: Option<&'static str>,
+    intercept_prefix: Option<&str>,
 ) -> String {
     let ParsedParameter {
         key,
@@ -195,11 +197,13 @@ fn get_safe_key_from_segment(
     } else {
         route_keys.insert(cleaned_key.clone(), key);
     }
+
+    let intercept_prefix = intercept_prefix.map_or_else(String::new, escape_string_regexp);
     match (repeat, optional) {
-        (true, true) => format!(r"(?:/(?P<{cleaned_key}>.+?))?"),
-        (true, false) => format!(r"/(?P<{cleaned_key}>.+?)"),
-        (false, true) => format!(r"(?:/(?P<{cleaned_key}>[^/]+?))?"),
-        (false, false) => format!(r"/(?P<{cleaned_key}>[^/]+?)"),
+        (true, true) => format!(r"(?:/{intercept_prefix}(?P<{cleaned_key}>.+?))?"),
+        (true, false) => format!(r"/{intercept_prefix}(?P<{cleaned_key}>.+?)"),
+        (false, true) => format!(r"(?:/{intercept_prefix}(?P<{cleaned_key}>[^/]+?))?"),
+        (false, false) => format!(r"/{intercept_prefix}(?P<{cleaned_key}>[^/]+?)"),
     }
 }
 
@@ -213,11 +217,12 @@ fn get_named_parametrized_route(
     let parameterized_route = segments
         .iter()
         .map(|segment| {
+            let interception_marker = INTERCEPTION_ROUTE_MARKERS
+                .iter()
+                .find(|&m| segment.starts_with(m))
+                .copied();
             let key_prefix = if prefix_route_keys {
-                let has_interception_marker = INTERCEPTION_ROUTE_MARKERS
-                    .iter()
-                    .any(|&m| segment.starts_with(m));
-                if has_interception_marker {
+                if interception_marker.is_some() {
                     Some(NEXT_INTERCEPTION_MARKER_PREFIX)
                 } else {
                     Some(NEXT_QUERY_PARAM_PREFIX)
@@ -233,6 +238,7 @@ fn get_named_parametrized_route(
                     &matches[1],
                     &mut route_keys,
                     key_prefix,
+                    interception_marker,
                 );
             }
             format!("/{}", escape_string_regexp(segment))
@@ -265,3 +271,39 @@ pub fn get_named_middleware_regex(normalized_route: &str) -> String {
     let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true);
     format!("^{parameterized_route}(?:/)?$")
 }
+
+#[cfg(test)]
+mod test {
+    use super::get_named_middleware_regex;
+
+    #[test]
+    fn should_properly_handle_intercept_routes() {
+        let tests = [
+            (
+                "/[locale]/example/(...)[locale]/intercepted",
+                "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\.\\)(?P<nxtIlocale>[^/]+?)/\
+                 intercepted(?:/)?$",
+            ),
+            (
+                "/[locale]/example/(..)[locale]/intercepted",
+                "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\)(?P<nxtIlocale>[^/]+?)/intercepted(?\
+                 :/)?$",
+            ),
+            (
+                "/[locale]/example/(.)[locale]/intercepted",
+                "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\)(?P<nxtIlocale>[^/]+?)/intercepted(?:/\
+                 )?$",
+            ),
+            (
+                "/[locale]/example/(..)(..)[locale]/intercepted",
+                "^/(?P<nxtPlocale>[^/]+?)/example/\\(\\.\\.\\)\\(\\.\\.\\)(?P<nxtIlocale>[^/]+?)/\
+                 intercepted(?:/)?$",
+            ),
+        ];
+
+        for test in tests {
+            let intercept_route = get_named_middleware_regex(test.0);
+            assert_eq!(intercept_route, test.1);
+        }
+    }
+}