Skip to content

Commit cbf451d

Browse files
committed
Rewrite get_header relay communication to support SSZ and JSON
content negotiation. Includes: - SSZ and JSON encoding/decoding for relay get_header responses - Content-Type and Eth-Consensus-Version header parsing - Accept header forwarding to relays with q-value ordering - MIME parameter tolerance on relay response Content-Type - Fork-aware SSZ bid value extraction for all supported forks - Mock relay and validator SSZ support in test infrastructure - get_header integration tests for both encodings - Dynamic port allocation in tests via get_free_listener
1 parent 3e62c4c commit cbf451d

15 files changed

Lines changed: 1268 additions & 302 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/common/src/pbs/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub enum PbsError {
1515
#[error("json decode error: {err:?}, raw: {raw}")]
1616
JsonDecode { err: serde_json::Error, raw: String },
1717

18+
#[error("ssz decode error: {err:?}, fork: {fork}")]
19+
SSZDecode { err: String, fork: ForkName },
20+
1821
#[error("{0}")]
1922
ReadResponse(#[from] ResponseReadError),
2023

crates/common/src/wire.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use lh_types::{BeaconBlock, ForkName};
1010
use mediatype::{MediaType, ReadParams};
1111
use reqwest::{
1212
Response,
13-
header::{ACCEPT, CONTENT_TYPE, HeaderMap},
13+
header::{ACCEPT, CONTENT_TYPE, HeaderMap, ToStrError},
1414
};
1515
use thiserror::Error;
1616

@@ -37,6 +37,18 @@ pub enum ResponseReadError {
3737
NonSuccess { status_code: u16, error_msg: String, request_url: String },
3838
}
3939

40+
#[derive(Debug, Error)]
41+
pub enum AcceptedEncodingsError {
42+
#[error("invalid header string: {0}")]
43+
InvalidString(#[from] ToStrError),
44+
45+
#[error("invalid accept header: {error_msg}")]
46+
InvalidEncoding { error_msg: String },
47+
48+
#[error("unsupported accept type")]
49+
UnsupportedAcceptType,
50+
}
51+
4052
#[cfg(feature = "testing-flags")]
4153
thread_local! {
4254
static IGNORE_CONTENT_LENGTH: Cell<bool> = const { Cell::new(false) };
@@ -108,7 +120,7 @@ pub async fn read_chunked_body_with_max(
108120
/// Reads an HTTP response body with a size limit, erroring on non-success
109121
/// status or read failure.
110122
pub async fn safe_read_http_response(
111-
response: reqwest::Response,
123+
response: Response,
112124
max_size: usize,
113125
) -> Result<Vec<u8>, ResponseReadError> {
114126
let status_code = response.status();
@@ -198,16 +210,22 @@ impl IntoIterator for AcceptedEncodings {
198210
/// The returned order honors the RFC 9110 §12.5.1 precedence rules already
199211
/// applied by `headers_accept::Accept::media_types()` (specificity, then
200212
/// q-value, then original order).
201-
pub fn get_accept_types(req_headers: &HeaderMap) -> eyre::Result<AcceptedEncodings> {
213+
pub fn get_accept_types(
214+
req_headers: &HeaderMap,
215+
) -> Result<AcceptedEncodings, AcceptedEncodingsError> {
202216
// Only two supported media types, so the ordered set is at most two
203217
// entries: primary + optional fallback.
204218
let mut primary: Option<EncodingType> = None;
205219
let mut fallback: Option<EncodingType> = None;
206220
let mut saw_any = false;
207221
let mut had_supported = false;
208222
for header in req_headers.get_all(ACCEPT).iter() {
209-
let accept = Accept::from_str(header.to_str()?)
210-
.map_err(|e| eyre::eyre!("invalid accept header: {e}"))?;
223+
let accept_str = header.to_str().map_err(AcceptedEncodingsError::InvalidString)?;
224+
let accept =
225+
Accept::from_str(accept_str).map_err(|e| AcceptedEncodingsError::InvalidEncoding {
226+
error_msg: (format!("invalid accept header: {e}")).to_string(),
227+
})?;
228+
211229
for mt in accept.media_types() {
212230
saw_any = true;
213231

@@ -243,7 +261,7 @@ pub fn get_accept_types(req_headers: &HeaderMap) -> eyre::Result<AcceptedEncodin
243261
}
244262

245263
if saw_any && !had_supported {
246-
eyre::bail!("unsupported accept type");
264+
return Err(AcceptedEncodingsError::UnsupportedAcceptType)
247265
}
248266

249267
// No accept header (or only q=0 rejections): fall back to the request

crates/pbs/src/error.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use axum::{http::StatusCode, response::IntoResponse};
2-
use cb_common::wire::BodyDeserializeError;
1+
use axum::{
2+
http::StatusCode,
3+
response::{IntoResponse, Response},
4+
};
5+
use cb_common::wire::{AcceptedEncodingsError, BodyDeserializeError};
36
use thiserror::Error;
47

58
#[derive(Debug, Error)]
@@ -13,6 +16,8 @@ pub enum PbsClientError {
1316
Internal,
1417
#[error("failed to deserialize body: {0}")]
1518
DecodeError(#[from] BodyDeserializeError),
19+
#[error("invalid accept types: {0}")]
20+
HeaderError(#[from] AcceptedEncodingsError),
1621
}
1722

1823
impl PbsClientError {
@@ -22,17 +27,19 @@ impl PbsClientError {
2227
PbsClientError::NoPayload => StatusCode::BAD_GATEWAY,
2328
PbsClientError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
2429
PbsClientError::DecodeError(_) => StatusCode::BAD_REQUEST,
30+
PbsClientError::HeaderError(_) => StatusCode::BAD_REQUEST,
2531
}
2632
}
2733
}
2834

2935
impl IntoResponse for PbsClientError {
30-
fn into_response(self) -> axum::response::Response {
36+
fn into_response(self) -> Response {
3137
let msg = match &self {
3238
PbsClientError::NoResponse => "no response from relays".to_string(),
3339
PbsClientError::NoPayload => "no payload from relays".to_string(),
3440
PbsClientError::Internal => "internal server error".to_string(),
3541
PbsClientError::DecodeError(e) => format!("error decoding request: {e}"),
42+
PbsClientError::HeaderError(e) => format!("header error: {e}"),
3643
};
3744

3845
(self.status_code(), msg).into_response()

0 commit comments

Comments
 (0)