Skip to content

Commit e7335f6

Browse files
committed
Refactored encoding type support based on feedback
1 parent 86fa858 commit e7335f6

12 files changed

Lines changed: 208 additions & 171 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ ethereum_ssz_derive = "0.8"
4343
eyre = "0.6.12"
4444
futures = "0.3.30"
4545
headers = "0.4.0"
46+
headers-accept = "0.2.1"
4647
indexmap = "2.2.6"
4748
jsonwebtoken = { version = "9.3.1", default-features = false }
4849
lazy_static = "1.5.0"

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ethereum_ssz.workspace = true
2222
ethereum_ssz_derive.workspace = true
2323
eyre.workspace = true
2424
futures.workspace = true
25+
headers-accept.workspace = true
2526
jsonwebtoken.workspace = true
2627
lh_eth2_keystore.workspace = true
2728
lh_types.workspace = true

crates/common/src/utils.rs

Lines changed: 93 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[cfg(test)]
22
use std::cell::Cell;
33
use std::{
4-
fmt,
4+
fmt::Display,
55
net::Ipv4Addr,
66
str::FromStr,
77
time::{SystemTime, UNIX_EPOCH},
@@ -15,12 +15,12 @@ use axum::{
1515
};
1616
use bytes::Bytes;
1717
use futures::StreamExt;
18+
use headers_accept::Accept;
1819
pub use lh_types::ForkName;
1920
use lh_types::test_utils::{SeedableRng, TestRandom, XorShiftRng};
20-
use mediatype::{MediaType, MediaTypeList, names};
2121
use rand::{Rng, distr::Alphanumeric};
2222
use reqwest::{
23-
Response, StatusCode,
23+
Response,
2424
header::{ACCEPT, CONTENT_TYPE, HeaderMap},
2525
};
2626
use serde::{Serialize, de::DeserializeOwned};
@@ -42,6 +42,10 @@ use crate::{
4242
types::{BlsPublicKey, Chain, Jwt, JwtClaims, ModuleId},
4343
};
4444

45+
const APPLICATION_JSON: &str = "application/json";
46+
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
47+
const WILDCARD: &str = "*/*";
48+
4549
const MILLIS_PER_SECOND: u64 = 1_000;
4650
pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version";
4751

@@ -421,23 +425,52 @@ pub fn get_user_agent_with_version(req_headers: &HeaderMap) -> eyre::Result<Head
421425
Ok(HeaderValue::from_str(&format!("commit-boost/{HEADER_VERSION_VALUE} {ua}"))?)
422426
}
423427

424-
/// Parse ACCEPT header, default to JSON if missing or mal-formatted
425-
pub fn get_accept_header(req_headers: &HeaderMap) -> Accept {
426-
Accept::from_str(
427-
req_headers.get(ACCEPT).and_then(|value| value.to_str().ok()).unwrap_or("application/json"),
428+
/// Parse the ACCEPT header to get the type of response to encode the body with,
429+
/// defaulting to JSON if missing. Returns an error if malformed or unsupported
430+
/// types are requested. Supports requests with multiple ACCEPT headers or
431+
/// headers with multiple media types.
432+
pub fn get_accept_type(req_headers: &HeaderMap) -> eyre::Result<EncodingType> {
433+
let accept = Accept::from_str(
434+
req_headers.get(ACCEPT).and_then(|value| value.to_str().ok()).unwrap_or(APPLICATION_JSON),
428435
)
429-
.unwrap_or(Accept::Json)
436+
.map_err(|e| eyre::eyre!("invalid accept header: {e}"))?;
437+
438+
if accept.media_types().count() == 0 {
439+
// No valid media types found, default to JSON
440+
return Ok(EncodingType::Json);
441+
}
442+
443+
// Get the SSZ and JSON media types if present
444+
let mut ssz_type = false;
445+
let mut json_type = false;
446+
let mut unsupported_type = false;
447+
accept.media_types().for_each(|mt| match mt.essence().to_string().as_str() {
448+
APPLICATION_OCTET_STREAM => ssz_type = true,
449+
APPLICATION_JSON | WILDCARD => json_type = true,
450+
_ => unsupported_type = true,
451+
});
452+
453+
// If SSZ is present, prioritize it
454+
if ssz_type {
455+
return Ok(EncodingType::Ssz);
456+
}
457+
// If there aren't any unsupported types, use JSON
458+
if !unsupported_type {
459+
return Ok(EncodingType::Json);
460+
}
461+
Err(eyre::eyre!("unsupported accept type"))
430462
}
431463

432-
/// Parse CONTENT TYPE header, default to JSON if missing or mal-formatted
433-
pub fn get_content_type_header(req_headers: &HeaderMap) -> ContentType {
434-
ContentType::from_str(
464+
/// Parse CONTENT TYPE header to get the encoding type of the body, defaulting
465+
/// to JSON if missing or malformed.
466+
pub fn get_content_type(req_headers: &HeaderMap) -> EncodingType {
467+
EncodingType::from_str(
435468
req_headers
436469
.get(CONTENT_TYPE)
437470
.and_then(|value| value.to_str().ok())
438-
.unwrap_or("application/json"),
471+
.unwrap_or(APPLICATION_JSON),
439472
)
440-
.unwrap_or(ContentType::Json)
473+
.unwrap_or(EncodingType::Json)
441474
}
442475

443476
/// Parse CONSENSUS_VERSION header
@@ -451,133 +484,91 @@ pub fn get_consensus_version_header(req_headers: &HeaderMap) -> Option<ForkName>
451484
.ok()
452485
}
453486

487+
/// Enum for types that can be used to encode incoming request bodies or
488+
/// outgoing response bodies
454489
#[derive(Debug, Clone, Copy, PartialEq)]
455-
pub enum ContentType {
490+
pub enum EncodingType {
491+
/// Body is UTF-8 encoded as JSON
456492
Json,
493+
494+
/// Body is raw bytes representing an SSZ object
457495
Ssz,
458496
}
459497

460-
impl std::fmt::Display for ContentType {
498+
impl std::fmt::Display for EncodingType {
461499
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462500
match self {
463-
ContentType::Json => write!(f, "application/json"),
464-
ContentType::Ssz => write!(f, "application/octet-stream"),
501+
EncodingType::Json => write!(f, "application/json"),
502+
EncodingType::Ssz => write!(f, "application/octet-stream"),
465503
}
466504
}
467505
}
468506

469-
impl FromStr for ContentType {
507+
impl FromStr for EncodingType {
470508
type Err = String;
471509
fn from_str(value: &str) -> Result<Self, Self::Err> {
472510
match value {
473-
"application/json" => Ok(ContentType::Json),
474-
"application/octet-stream" => Ok(ContentType::Ssz),
475-
_ => Ok(ContentType::Json),
511+
"application/json" | "" => Ok(EncodingType::Json),
512+
"application/octet-stream" => Ok(EncodingType::Ssz),
513+
_ => Err(format!("unsupported encoding type: {value}")),
476514
}
477515
}
478516
}
479517

480-
#[derive(Debug, Clone, Copy, PartialEq)]
481-
pub enum Accept {
482-
Json,
483-
Ssz,
484-
Any,
518+
pub enum BodyDeserializeError {
519+
SerdeJsonError(serde_json::Error),
520+
SszDecodeError(ssz::DecodeError),
521+
UnsupportedMediaType,
485522
}
486523

487-
impl fmt::Display for Accept {
488-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524+
impl Display for BodyDeserializeError {
525+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489526
match self {
490-
Accept::Ssz => write!(f, "application/octet-stream"),
491-
Accept::Json => write!(f, "application/json"),
492-
Accept::Any => write!(f, "*/*"),
527+
BodyDeserializeError::SerdeJsonError(e) => write!(f, "JSON deserialization error: {e}"),
528+
BodyDeserializeError::SszDecodeError(e) => {
529+
write!(f, "SSZ deserialization error: {:?}", e)
530+
}
531+
BodyDeserializeError::UnsupportedMediaType => write!(f, "unsupported media type"),
493532
}
494533
}
495534
}
496535

497-
impl FromStr for Accept {
498-
type Err = String;
499-
500-
fn from_str(s: &str) -> Result<Self, Self::Err> {
501-
let media_type_list = MediaTypeList::new(s);
502-
503-
// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
504-
// find the highest q-factor supported accept type
505-
let mut highest_q = 0_u16;
506-
let mut accept_type = None;
507-
508-
const APPLICATION: &str = names::APPLICATION.as_str();
509-
const OCTET_STREAM: &str = names::OCTET_STREAM.as_str();
510-
const JSON: &str = names::JSON.as_str();
511-
const STAR: &str = names::_STAR.as_str();
512-
const Q: &str = names::Q.as_str();
513-
514-
media_type_list.into_iter().for_each(|item| {
515-
if let Ok(MediaType { ty, subty, suffix: _, params }) = item {
516-
let q_accept = match (ty.as_str(), subty.as_str()) {
517-
(APPLICATION, OCTET_STREAM) => Some(Accept::Ssz),
518-
(APPLICATION, JSON) => Some(Accept::Json),
519-
(STAR, STAR) => Some(Accept::Any),
520-
_ => None,
521-
}
522-
.map(|item_accept_type| {
523-
let q_val = params
524-
.iter()
525-
.find_map(|(n, v)| match n.as_str() {
526-
Q => {
527-
Some((v.as_str().parse::<f32>().unwrap_or(0_f32) * 1000_f32) as u16)
528-
}
529-
_ => None,
530-
})
531-
.or(Some(1000_u16));
532-
533-
(q_val.unwrap(), item_accept_type)
534-
});
535-
536-
match q_accept {
537-
Some((q, accept)) if q > highest_q => {
538-
highest_q = q;
539-
accept_type = Some(accept);
540-
}
541-
_ => (),
542-
}
536+
pub async fn deserialize_body<T>(
537+
headers: &HeaderMap,
538+
body: Bytes,
539+
) -> Result<T, BodyDeserializeError>
540+
where
541+
T: serde::de::DeserializeOwned + ssz::Decode + 'static,
542+
{
543+
if headers.contains_key(CONTENT_TYPE) {
544+
return match get_content_type(headers) {
545+
EncodingType::Json => {
546+
serde_json::from_slice::<T>(&body).map_err(BodyDeserializeError::SerdeJsonError)
543547
}
544-
});
545-
accept_type.ok_or_else(|| "accept header is not supported".to_string())
548+
EncodingType::Ssz => {
549+
T::from_ssz_bytes(&body).map_err(BodyDeserializeError::SszDecodeError)
550+
}
551+
};
546552
}
553+
554+
Err(BodyDeserializeError::UnsupportedMediaType)
547555
}
548556

549557
#[must_use]
550-
#[derive(Debug, Clone, Copy, Default)]
551-
pub struct JsonOrSsz<T>(pub T);
558+
#[derive(Debug, Clone, Default)]
559+
pub struct RawRequest {
560+
pub body_bytes: Bytes,
561+
}
552562

553-
impl<T, S> FromRequest<S> for JsonOrSsz<T>
563+
impl<S> FromRequest<S> for RawRequest
554564
where
555-
T: serde::de::DeserializeOwned + ssz::Decode + 'static,
556565
S: Send + Sync,
557566
{
558567
type Rejection = AxumResponse;
559568

560569
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
561-
let headers = req.headers().clone();
562-
let content_type = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok());
563-
564570
let bytes = Bytes::from_request(req, _state).await.map_err(IntoResponse::into_response)?;
565-
566-
if let Some(content_type) = content_type {
567-
if content_type.starts_with(&ContentType::Json.to_string()) {
568-
let payload: T = serde_json::from_slice(&bytes)
569-
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
570-
return Ok(Self(payload));
571-
}
572-
573-
if content_type.starts_with(&ContentType::Ssz.to_string()) {
574-
let payload = T::from_ssz_bytes(&bytes)
575-
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
576-
return Ok(Self(payload));
577-
}
578-
}
579-
580-
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
571+
Ok(Self { body_bytes: bytes })
581572
}
582573
}
583574

crates/pbs/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub enum PbsClientError {
66
NoResponse,
77
NoPayload,
88
Internal,
9+
DecodeError(String),
910
}
1011

1112
impl PbsClientError {
@@ -14,6 +15,7 @@ impl PbsClientError {
1415
PbsClientError::NoResponse => StatusCode::BAD_GATEWAY,
1516
PbsClientError::NoPayload => StatusCode::BAD_GATEWAY,
1617
PbsClientError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
18+
PbsClientError::DecodeError(_) => StatusCode::BAD_REQUEST,
1719
}
1820
}
1921
}
@@ -24,6 +26,7 @@ impl IntoResponse for PbsClientError {
2426
PbsClientError::NoResponse => "no response from relays".to_string(),
2527
PbsClientError::NoPayload => "no payload from relays".to_string(),
2628
PbsClientError::Internal => "internal server error".to_string(),
29+
PbsClientError::DecodeError(e) => format!("error decoding request: {e}"),
2730
};
2831

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

0 commit comments

Comments
 (0)