diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c383742..b789f491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **RequestRedirect: redirect port when the scheme is unchanged.** A + `requestRedirect` filter that set `scheme` without `port` only substituted the + scheme's well-known port when the redirect scheme differed from the request + scheme. The Gateway API spec assigns the well-known port (http → 80, + https → 443) for any non-empty redirect scheme. On a listener bound to a + non-standard port, `scheme: http` with no `port` emitted + `http://example.org:8080/` instead of `http://example.org/`. A redirect + scheme with no well-known port now falls back to the listener port, as the + spec recommends. The conformance suite does not exercise this case. + ## [v0.23.0 - 2026-07-24] ### Added diff --git a/ghost/src/redirect_backend.rs b/ghost/src/redirect_backend.rs index d807aaa0..dd219f65 100644 --- a/ghost/src/redirect_backend.rs +++ b/ghost/src/redirect_backend.rs @@ -108,21 +108,23 @@ pub fn build_location(config: &RedirectConfig) -> Result { .as_deref() .unwrap_or(&config.original_hostname); - // Port handling: if scheme changes without explicit port, use default port for new scheme + // Port handling per the Gateway API spec for HTTPRequestRedirectFilter.port: + // - explicit port wins + // - otherwise, if the redirect scheme is non-empty, use that scheme's + // well-known port (http -> 80, https -> 443), regardless of whether it + // matches the original request scheme + // - a redirect scheme with no well-known port, or no redirect scheme at + // all, falls back to the Gateway listener port let port = if let Some(explicit_port) = config.filter.port { explicit_port - } else if config.filter.scheme.is_some() - && config.filter.scheme.as_deref() != Some(&config.original_scheme) - { - // Scheme changed without explicit port - use default for new scheme - if scheme == "https" { - 443 - } else { - 80 - } } else { - // No scheme change or scheme not specified - keep original port - config.original_port + match config.filter.scheme.as_deref() { + Some("http") => 80, + Some("https") => 443, + // Scheme with no well-known port, or no scheme specified - keep + // the listener port the request arrived on. + _ => config.original_port, + } }; // Rewrite path if specified @@ -313,6 +315,51 @@ mod tests { assert_eq!(location, "https://example.com:8443/path"); } + #[test] + fn test_build_location_same_scheme_uses_well_known_port() { + // Spec: a non-empty redirect scheme gets that scheme's well-known port + // even when it equals the original scheme. The Gateway API conformance + // suite has no scheme-http-and-port-nil case on its 8080 listener, so + // this case is only covered here. + let config = make_config( + make_filter(Some("http"), None, None, None, None, 302), + "http", + "example.org", + 8080, + "/", + "", + ); + + let location = build_location(&config).unwrap(); + assert_eq!(location, "http://example.org/"); + + // Same for https on a non-well-known TLS listener port. + let config = make_config( + make_filter(Some("https"), None, None, None, None, 302), + "https", + "example.org", + 8443, + "/", + "", + ); + + let location = build_location(&config).unwrap(); + assert_eq!(location, "https://example.org/"); + + // No redirect scheme - the listener port is preserved. + let config = make_config( + make_filter(None, None, None, None, None, 302), + "http", + "example.org", + 8080, + "/", + "", + ); + + let location = build_location(&config).unwrap(); + assert_eq!(location, "http://example.org:8080/"); + } + #[test] fn test_build_location_path_rewrite_full() { let config = make_config(