Fix checkpoint sync 401 with trailing-slash path-embedded API keys (#9548)

Closes #9545


  `BeaconNodeHttpClient::eth_path` clones the configured server URL and pushes path segments onto it. When the base URL ends with a slash (such as a QuickNode-style endpoint that embeds the API key in the path, `https://<host>/<api-key>/`), the URL already carries an empty trailing segment, so pushing `eth` yields a double slash:

```
https://<host>/<api-key>//eth/v2/debug/beacon/states/finalized
```

The provider responds to the `//` form with a `307` redirect to a normalized path that drops the key segment, so the redirected request arrives unauthenticated and checkpoint sync fails with `Error loading checkpoint state from remote: StatusCode(401)`.

This PR calls `pop_if_empty()` before pushing segments, the standard `url`-crate idiom for trailing-slash-tolerant joining, so a trailing slash no longer changes the resulting path. The fix is applied in `eth_path` (the shared prefix builder that every `/eth/vX` endpoint derives from) and in `post_lighthouse_liveness`, the only other builder that extends the base URL directly.


Co-Authored-By: rahulbarman <itsrahulbarman1@gmail.com>
This commit is contained in:
Rahul Barman
2026-06-29 14:46:37 +05:30
committed by GitHub
parent 0ac8c257ef
commit d814c029bb

View File

@@ -208,6 +208,9 @@ impl BeaconNodeHttpClient {
path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
// Drop a trailing empty segment so a base URL with a trailing slash
// (e.g. `https://host/<api-key>/`) does not produce a double slash.
.pop_if_empty()
.push("eth")
.push(&version.to_string());
@@ -3232,6 +3235,7 @@ impl BeaconNodeHttpClient {
path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.pop_if_empty()
.push("lighthouse")
.push("liveness");
@@ -3508,3 +3512,37 @@ impl BeaconNodeHttpClient {
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn eth_v2_prefix(base: &str) -> String {
let server = SensitiveUrl::parse(base).expect("valid base url");
let client = BeaconNodeHttpClient::new(server, Timeouts::set_all(Duration::from_secs(1)));
client.eth_path(V2).unwrap().to_string()
}
// A trailing slash on the base URL must not change the resulting endpoint
// path. With a path-embedded API key this previously produced a `//eth`
// double slash that providers redirect to a key-less, unauthenticated path.
// See https://github.com/sigp/lighthouse/issues/9545.
#[test]
fn eth_path_is_trailing_slash_tolerant() {
// The reported case: a path-embedded API key with a trailing slash.
assert_eq!(
eth_v2_prefix("https://example.com/secret-key/"),
"https://example.com/secret-key/eth/v2"
);
assert_eq!(
eth_v2_prefix("https://example.com/secret-key/"),
eth_v2_prefix("https://example.com/secret-key")
);
// Regression guard: a bare host with a trailing slash.
assert_eq!(
eth_v2_prefix("http://localhost:5052/"),
eth_v2_prefix("http://localhost:5052")
);
}
}