diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 4213fd4ab8..cb4ce34682 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -10,7 +10,9 @@ use eth2::{ types::{ BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, }, - BeaconNodeHttpClient, Error, StatusCode, Timeouts, + BeaconNodeHttpClient, Error, + Error::ServerMessage, + StatusCode, Timeouts, }; use execution_layer::test_utils::{ MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, @@ -807,6 +809,39 @@ impl ApiTester { self } + pub async fn post_beacon_states_validator_balances_unsupported_media_failure(self) -> Self { + for state_id in self.interesting_state_ids() { + for validator_indices in self.interesting_validator_indices() { + let validator_index_ids = validator_indices + .iter() + .cloned() + .map(|i| ValidatorId::Index(i)) + .collect::>(); + + let unsupported_media_response = self + .client + .post_beacon_states_validator_balances_with_ssz_header( + state_id.0, + validator_index_ids, + ) + .await; + + if let Err(unsupported_media_response) = unsupported_media_response { + match unsupported_media_response { + ServerMessage(error) => { + assert_eq!(error.code, 415) + } + _ => panic!("Should error with unsupported media response"), + } + } else { + panic!("Should error with unsupported media response"); + } + } + } + + self + } + pub async fn test_beacon_states_validator_balances(self) -> Self { for state_id in self.interesting_state_ids() { for validator_indices in self.interesting_validator_indices() { @@ -5660,6 +5695,14 @@ async fn get_events_from_genesis() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_unsupported_media_response() { + ApiTester::new() + .await + .post_beacon_states_validator_balances_unsupported_media_failure() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn beacon_get() { ApiTester::new() diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index d8b2c8ef2d..5a51aaec5a 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -415,6 +415,23 @@ impl BeaconNodeHttpClient { ok_or_error(response).await } + /// Generic POST function that includes octet-stream content type header. + async fn post_generic_with_ssz_header( + &self, + url: U, + body: &T, + ) -> Result { + let builder = self.client.post(url); + let mut headers = HeaderMap::new(); + + headers.insert( + "Content-Type", + HeaderValue::from_static("application/octet-stream"), + ); + let response = builder.headers(headers).json(body).send().await?; + ok_or_error(response).await + } + /// Generic POST function supporting arbitrary responses and timeouts. async fn post_generic_with_consensus_version_and_ssz_body, U: IntoUrl>( &self, @@ -543,6 +560,26 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// TESTING ONLY: This request should fail with a 415 response code. + pub async fn post_beacon_states_validator_balances_with_ssz_header( + &self, + state_id: StateId, + ids: Vec, + ) -> Result { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("validator_balances"); + + let request = ValidatorBalancesRequestBody { ids }; + + self.post_generic_with_ssz_header(path, &request).await + } + /// `POST beacon/states/{state_id}/validator_balances` /// /// Returns `Ok(None)` on a 404 error. diff --git a/common/warp_utils/src/json.rs b/common/warp_utils/src/json.rs index 203a6495a4..6ee5e77261 100644 --- a/common/warp_utils/src/json.rs +++ b/common/warp_utils/src/json.rs @@ -1,4 +1,5 @@ use bytes::Bytes; +use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use serde::de::DeserializeOwned; use std::error::Error as StdError; use warp::{Filter, Rejection}; @@ -16,7 +17,17 @@ impl Json { } pub fn json() -> impl Filter + Copy { - warp::body::bytes().and_then(|bytes: Bytes| async move { - Json::decode(bytes).map_err(|err| reject::custom_deserialize_error(format!("{:?}", err))) - }) + warp::header::optional::(CONTENT_TYPE_HEADER) + .and(warp::body::bytes()) + .and_then(|header: Option, bytes: Bytes| async move { + if let Some(header) = header { + if header == SSZ_CONTENT_TYPE_HEADER { + return Err(reject::unsupported_media_type( + "The request's content-type is not supported".to_string(), + )); + } + } + Json::decode(bytes) + .map_err(|err| reject::custom_deserialize_error(format!("{:?}", err))) + }) } diff --git a/common/warp_utils/src/reject.rs b/common/warp_utils/src/reject.rs index b6bb5ace3d..d33f32251b 100644 --- a/common/warp_utils/src/reject.rs +++ b/common/warp_utils/src/reject.rs @@ -136,6 +136,15 @@ pub fn invalid_auth(msg: String) -> warp::reject::Rejection { warp::reject::custom(InvalidAuthorization(msg)) } +#[derive(Debug)] +pub struct UnsupportedMediaType(pub String); + +impl Reject for UnsupportedMediaType {} + +pub fn unsupported_media_type(msg: String) -> warp::reject::Rejection { + warp::reject::custom(UnsupportedMediaType(msg)) +} + #[derive(Debug)] pub struct IndexedBadRequestErrors { pub message: String, @@ -170,6 +179,9 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result().is_some() { + code = StatusCode::UNSUPPORTED_MEDIA_TYPE; + message = "UNSUPPORTED_MEDIA_TYPE".to_string(); } else if let Some(e) = err.find::() { message = format!("BAD_REQUEST: body deserialize error: {}", e.0); code = StatusCode::BAD_REQUEST;