Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 59 additions & 12 deletions ghost/src/redirect_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,23 @@ pub fn build_location(config: &RedirectConfig) -> Result<String, VclError> {
.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
Expand Down Expand Up @@ -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(
Expand Down
Loading