fix(http): separate transport and protocol limits

This commit is contained in:
2026-10-02 02:50:56 +09:00
parent 65c51006ad
commit df006ee706
12 changed files with 643 additions and 336 deletions
+10 -32
View File
@@ -11,7 +11,8 @@ use wip_protocol::{
use crate::response::{self, DecodedResponse};
use crate::{
BodyKind, ClientResponseError, CodecError, EncodeHttpError, Endpoint, InvalidResponseKind,
JSON_CONTENT_TYPE, Limits, ResourceLimitKind, Route, is_json_content_type, json,
JSON_CONTENT_TYPE, Limits, Route, codec::has_supported_content_coding, is_json_content_type,
json,
};
/// A retrieval method accepted by the standard v1 batch endpoint.
@@ -335,20 +336,16 @@ pub fn encode_batch_response(
/// Encodes a top-level batch-envelope failure.
///
/// Item-local failures belong in [`BatchResponseItem`]. Only failures that can
/// reject the batch envelope before item execution are accepted here. A
/// resource-limit failure defaults to the logical-limit status; use
/// [`encode_batch_resource_limit_error_response`] when the concrete class is
/// known.
/// Item-local failures belong in [`BatchResponseItem`]. Only `invalid_request`
/// and `internal` may reject the complete batch as protocol failures; packaging
/// and representation limits remain transport-local.
pub fn encode_batch_error_response(
error: &ProtocolError,
limits: Limits,
) -> Result<Response<Vec<u8>>, EncodeHttpError> {
if !matches!(
error.code,
ProtocolErrorCode::InvalidRequest
| ProtocolErrorCode::ResourceLimitExceeded
| ProtocolErrorCode::Internal
ProtocolErrorCode::InvalidRequest | ProtocolErrorCode::Internal
) {
return Err(CodecError::InvalidField {
field: "error.code".into(),
@@ -363,26 +360,6 @@ pub fn encode_batch_error_response(
.body(body)?)
}
/// Encodes a class-specific top-level batch `resource_limit_exceeded` failure.
pub fn encode_batch_resource_limit_error_response(
error: &ProtocolError,
kind: ResourceLimitKind,
limits: Limits,
) -> Result<Response<Vec<u8>>, EncodeHttpError> {
if error.code != ProtocolErrorCode::ResourceLimitExceeded {
return Err(CodecError::InvalidField {
field: "error.code".into(),
reason: "resource-limit encoder requires resource_limit_exceeded".into(),
}
.into());
}
let body = json::serialize(&response::error_to_json(error), BodyKind::Response, limits)?;
Ok(Response::builder()
.status(response::status_for_resource_limit(kind))
.header(CONTENT_TYPE, JSON_CONTENT_TYPE)
.body(body)?)
}
fn response_from_json(
value: &Json,
request: &BatchRequest,
@@ -466,6 +443,9 @@ pub fn decode_batch_response<B: AsRef<[u8]>>(
value: &Response<B>,
limits: Limits,
) -> Result<DecodedResponse<BatchResponse>, ClientResponseError> {
if !has_supported_content_coding(value.headers()) {
return Err(response::unsupported_content_coding(value.status()));
}
let valid_content_type = value
.headers()
.get(CONTENT_TYPE)
@@ -496,9 +476,7 @@ pub fn decode_batch_response<B: AsRef<[u8]>>(
if let DecodedResponse::ProtocolFailure { error, .. } = &decoded
&& !matches!(
error.code,
ProtocolErrorCode::InvalidRequest
| ProtocolErrorCode::ResourceLimitExceeded
| ProtocolErrorCode::Internal
ProtocolErrorCode::InvalidRequest | ProtocolErrorCode::Internal
)
{
return Err(ClientResponseError::invalid(
+42 -55
View File
@@ -1,7 +1,7 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use http::header::{ACCEPT, CONTENT_TYPE};
use http::header::{ACCEPT, CONTENT_ENCODING, CONTENT_TYPE};
use http::{HeaderMap, Method, Request, Response, StatusCode};
use wip_protocol::{
CallOperationRequest, CallOperationResponse, FetchInterfaceRequest, FetchInterfaceResponse,
@@ -12,7 +12,7 @@ use wip_protocol::{
use crate::response::{self, DecodedResponse};
use crate::{
BodyKind, ClientResponseError, CodecError, Endpoint, InvalidResponseKind, JSON_CONTENT_TYPE,
Limits, ResourceLimitKind, Route, json,
Limits, Route, json,
};
fn request(
@@ -36,6 +36,9 @@ fn response(status: StatusCode, body: Vec<u8>) -> Result<Response<Vec<u8>>, http
}
fn require_response_content_type<B>(response: &Response<B>) -> Result<(), ClientResponseError> {
if !has_supported_content_coding(response.headers()) {
return Err(response::unsupported_content_coding(response.status()));
}
let valid = response
.headers()
.get(CONTENT_TYPE)
@@ -181,9 +184,11 @@ fn accepts_json(headers: &HeaderMap) -> bool {
/// A failure while validating inbound HTTP request metadata.
#[derive(Debug)]
pub enum RequestMetadataError {
/// The method or route does not match the selected WIP interaction.
InvalidRequest(CodecError),
/// The request body does not use UTF-8 `application/json`.
/// The request URI does not exactly identify the selected standard route.
NotFound,
/// A known standard route was requested with a method other than `POST`.
MethodNotAllowed,
/// The request body does not use UTF-8 `application/json` or identity coding.
UnsupportedMediaType,
/// The request's `Accept` metadata excludes `application/json`.
NotAcceptable,
@@ -194,32 +199,47 @@ impl RequestMetadataError {
#[must_use]
pub const fn status(&self) -> StatusCode {
match self {
Self::InvalidRequest(_) => StatusCode::BAD_REQUEST,
Self::NotFound => StatusCode::NOT_FOUND,
Self::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE,
}
}
/// Returns the value for an `Allow` response header, when required.
#[must_use]
pub const fn allow(&self) -> Option<&'static str> {
match self {
Self::MethodNotAllowed => Some("POST"),
Self::NotFound | Self::UnsupportedMediaType | Self::NotAcceptable => None,
}
}
}
impl Display for RequestMetadataError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRequest(error) => error.fmt(formatter),
Self::NotFound => formatter.write_str("request URI is not a standard WIP route"),
Self::MethodNotAllowed => formatter.write_str("standard WIP routes require POST"),
Self::UnsupportedMediaType => {
formatter.write_str("expected UTF-8 application/json Content-Type")
formatter.write_str("expected identity-coded UTF-8 application/json Content-Type")
}
Self::NotAcceptable => formatter.write_str("request does not accept application/json"),
}
}
}
impl Error for RequestMetadataError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidRequest(error) => Some(error),
Self::UnsupportedMediaType | Self::NotAcceptable => None,
}
}
impl Error for RequestMetadataError {}
pub(crate) fn has_supported_content_coding(headers: &HeaderMap) -> bool {
headers.get_all(CONTENT_ENCODING).iter().all(|value| {
value.to_str().is_ok_and(|value| {
let mut codings = value.split(',').map(str::trim);
let first = codings.next();
first.is_some_and(|coding| coding.eq_ignore_ascii_case("identity"))
&& codings.all(|coding| coding.eq_ignore_ascii_case("identity"))
})
})
}
/// Validates mandatory HTTP metadata for an inbound WIP request.
@@ -233,29 +253,19 @@ pub fn validate_http_request<B>(
route: Route,
value: &Request<B>,
) -> Result<(), RequestMetadataError> {
if value.method() != Method::POST {
return Err(RequestMetadataError::InvalidRequest(
CodecError::InvalidField {
field: "method".into(),
reason: "standard WIP routes require POST".into(),
},
));
}
let expected = endpoint.route(route);
if value.uri().path() != expected.path() || value.uri().query().is_some() {
return Err(RequestMetadataError::InvalidRequest(
CodecError::InvalidField {
field: "route".into(),
reason: "request URI does not match the selected standard route".into(),
},
));
return Err(RequestMetadataError::NotFound);
}
if value.method() != Method::POST {
return Err(RequestMetadataError::MethodNotAllowed);
}
let valid_content_type = value
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(is_json_content_type);
if !valid_content_type {
if !valid_content_type || !has_supported_content_coding(value.headers()) {
return Err(RequestMetadataError::UnsupportedMediaType);
}
if !accepts_json(value.headers()) {
@@ -526,9 +536,8 @@ fn encode_protocol_error_with_status(
/// The code must be allowed for `interaction`. This API accepts only
/// [`ProtocolError`], so client-local `InvalidResponse` and
/// `UnsupportedDescriptorFormat` classifications cannot be emitted as Host
/// error envelopes. Resource-limit errors use the logical-limit status; callers
/// that know the concrete limit class should use
/// [`encode_resource_limit_error_response`].
/// error envelopes. Logical `resource_limit_exceeded` failures use HTTP 422;
/// representation and deployment limits are not protocol errors.
pub fn encode_protocol_error_response(
interaction: ProtocolInteraction,
error: &ProtocolError,
@@ -542,28 +551,6 @@ pub fn encode_protocol_error_response(
)
}
/// Encodes a `resource_limit_exceeded` envelope with its class-specific status.
pub fn encode_resource_limit_error_response(
interaction: ProtocolInteraction,
error: &ProtocolError,
kind: ResourceLimitKind,
limits: Limits,
) -> Result<Response<Vec<u8>>, EncodeHttpError> {
if error.code != wip_protocol::ProtocolErrorCode::ResourceLimitExceeded {
return Err(CodecError::InvalidField {
field: "error.code".into(),
reason: "resource-limit encoder requires resource_limit_exceeded".into(),
}
.into());
}
encode_protocol_error_with_status(
interaction,
error,
response::status_for_resource_limit(kind),
limits,
)
}
/// Failure while building an outbound framework-neutral HTTP message.
#[derive(Debug)]
pub enum EncodeHttpError {
-23
View File
@@ -102,15 +102,6 @@ pub enum BodyKind {
Response,
}
/// The class of a resource limit exceeded by a wire representation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceLimitKind {
/// A request or response representation exceeded its byte-size limit.
RepresentationBytes,
/// A logical limit such as nesting depth, batch count, or collection size was exceeded.
Logical,
}
/// A malformed or locally disallowed wire representation.
#[derive(Debug)]
pub enum CodecError {
@@ -186,20 +177,6 @@ pub enum CodecError {
},
}
impl CodecError {
/// Classifies this failure when it represents a canonical resource limit.
#[must_use]
pub const fn resource_limit_kind(&self) -> Option<ResourceLimitKind> {
match self {
Self::BodyTooLarge { .. } => Some(ResourceLimitKind::RepresentationBytes),
Self::NestingTooDeep { .. } | Self::TooManyBatchItems { .. } => {
Some(ResourceLimitKind::Logical)
}
_ => None,
}
}
}
impl Display for CodecError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
+3 -6
View File
@@ -40,8 +40,7 @@ mod response;
pub use batch::{
BatchMethod, BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse,
BatchResponseItem, BatchSuccess, decode_batch_request, decode_batch_response,
encode_batch_error_response, encode_batch_request, encode_batch_resource_limit_error_response,
encode_batch_response,
encode_batch_error_response, encode_batch_request, encode_batch_response,
};
pub use codec::{
CallOperationMetadata, EncodeHttpError, RequestMetadataError, decode_call_operation_metadata,
@@ -49,15 +48,13 @@ pub use codec::{
decode_fetch_interface_response, decode_observe_request, decode_observe_response,
encode_call_operation_request, encode_call_operation_response, encode_fetch_interface_request,
encode_fetch_interface_response, encode_observe_request, encode_observe_response,
encode_protocol_error_response, encode_resource_limit_error_response, is_json_content_type,
validate_http_request,
encode_protocol_error_response, is_json_content_type, validate_http_request,
};
pub use endpoint::{Endpoint, EndpointError, JSON_CONTENT_TYPE, Route};
pub use error::{BodyKind, CodecError, LimitConfigurationError, Limits, ResourceLimitKind};
pub use error::{BodyKind, CodecError, LimitConfigurationError, Limits};
pub use response::{
ClientResponseError, DecodedResponse, InvalidResponseKind, StatusMismatch,
TransportBindingFailure, TransportFailureKind, error_code_name, status_for_error,
status_for_resource_limit,
};
/// Framework-neutral HTTP types used by this binding's public API.
+11 -20
View File
@@ -5,7 +5,7 @@ use http::StatusCode;
use serde_json::{Map, Value as Json};
use wip_protocol::{ProtocolError, ProtocolErrorCode, ProtocolInteraction};
use crate::{BodyKind, CodecError, Limits, ResourceLimitKind, json};
use crate::{BodyKind, CodecError, Limits, json};
/// A successfully classified client response.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -37,6 +37,8 @@ pub enum TransportFailureKind {
AuthenticationRequired,
/// HTTP 403, commonly emitted by endpoint policy or infrastructure.
EndpointForbidden,
/// The response uses a content coding the client does not support.
UnsupportedContentCoding,
/// Another non-success HTTP response without a WIP error envelope.
HttpStatus,
}
@@ -119,10 +121,6 @@ impl Display for ClientResponseError {
impl Error for ClientResponseError {}
/// Returns the canonical HTTP status for a wire-level protocol error.
///
/// [`ProtocolErrorCode::ResourceLimitExceeded`] defaults to the logical-limit
/// status. Use [`status_for_resource_limit`] when the concrete limit class is
/// known.
#[must_use]
pub const fn status_for_error(code: ProtocolErrorCode) -> StatusCode {
match code {
@@ -145,22 +143,8 @@ pub const fn status_for_error(code: ProtocolErrorCode) -> StatusCode {
}
}
/// Returns the canonical HTTP status for a concrete resource-limit class.
#[must_use]
pub const fn status_for_resource_limit(kind: ResourceLimitKind) -> StatusCode {
match kind {
ResourceLimitKind::RepresentationBytes => StatusCode::PAYLOAD_TOO_LARGE,
ResourceLimitKind::Logical => StatusCode::UNPROCESSABLE_ENTITY,
}
}
fn status_matches_error(status: StatusCode, code: ProtocolErrorCode) -> bool {
match code {
ProtocolErrorCode::ResourceLimitExceeded => {
status == StatusCode::PAYLOAD_TOO_LARGE || status == StatusCode::UNPROCESSABLE_ENTITY
}
_ => status == status_for_error(code),
}
status == status_for_error(code)
}
/// Returns the stable snake-case wire spelling of a protocol code.
@@ -355,6 +339,13 @@ pub(crate) fn decode<T>(
}
}
pub(crate) fn unsupported_content_coding(status: StatusCode) -> ClientResponseError {
ClientResponseError::TransportBinding(TransportBindingFailure {
status,
kind: TransportFailureKind::UnsupportedContentCoding,
})
}
pub(crate) fn transport_failure(status: StatusCode) -> ClientResponseError {
let kind = match status {
StatusCode::UNAUTHORIZED => TransportFailureKind::AuthenticationRequired,
+86 -23
View File
@@ -6,9 +6,8 @@ use wip_http::wip_protocol::{
use wip_http::{
BatchMethod, BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse,
BatchResponseItem, BatchSuccess, ClientResponseError, DecodedResponse, Endpoint,
InvalidResponseKind, Limits, ResourceLimitKind, decode_batch_request, decode_batch_response,
encode_batch_error_response, encode_batch_request, encode_batch_resource_limit_error_response,
encode_batch_response,
InvalidResponseKind, Limits, TransportFailureKind, decode_batch_request, decode_batch_response,
encode_batch_error_response, encode_batch_request, encode_batch_response,
};
fn limits() -> Limits {
@@ -200,29 +199,93 @@ fn invalid_batch_envelopes_are_rejected_at_top_level() {
}
#[test]
fn batch_resource_limits_use_413_or_422_and_never_429() {
fn resource_limit_is_item_local_and_forbidden_as_a_top_level_batch_error() {
let error = ProtocolError {
code: ProtocolErrorCode::ResourceLimitExceeded,
message: "bounded".into(),
message: "logical observation limit exceeded".into(),
};
for (kind, status) in [
(
ResourceLimitKind::RepresentationBytes,
StatusCode::PAYLOAD_TOO_LARGE,
),
(ResourceLimitKind::Logical, StatusCode::UNPROCESSABLE_ENTITY),
] {
let encoded = encode_batch_resource_limit_error_response(&error, kind, limits()).unwrap();
assert_eq!(encoded.status(), status);
assert_ne!(encoded.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(
decode_batch_response(&request(), &encoded, limits()).unwrap(),
DecodedResponse::ProtocolFailure {
error: error.clone(),
status_mismatch: None,
}
);
}
assert!(encode_batch_error_response(&error, limits()).is_err());
let request = request();
let response = BatchResponse {
responses: vec![
BatchResponseItem {
id: "object".into(),
outcome: Err(error.clone()),
},
BatchResponseItem {
id: "interface".into(),
outcome: Err(ProtocolError {
code: ProtocolErrorCode::InterfaceNotFound,
message: "gone".into(),
}),
},
],
};
let encoded = encode_batch_response(&request, &response, limits()).unwrap();
assert_eq!(encoded.status(), StatusCode::OK);
assert_eq!(
decode_batch_response(&request, &encoded, limits()).unwrap(),
DecodedResponse::Success(response)
);
let top_level = wip_http::http::Response::builder()
.status(StatusCode::UNPROCESSABLE_ENTITY)
.header("content-type", "application/json; charset=utf-8")
.body(br#"{"error":{"code":"resource_limit_exceeded","message":"not allowed"}}"#.to_vec())
.unwrap();
assert!(matches!(
decode_batch_response(&request, &top_level, limits()).unwrap_err(),
ClientResponseError::InvalidResponse {
kind: InvalidResponseKind::InvalidBatch,
..
}
));
}
#[test]
fn batch_transport_failures_do_not_become_protocol_errors() {
let request = request();
let bare_413 = wip_http::http::Response::builder()
.status(StatusCode::PAYLOAD_TOO_LARGE)
.body(Vec::<u8>::new())
.unwrap();
assert!(matches!(
decode_batch_response(&request, &bare_413, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::PAYLOAD_TOO_LARGE
&& failure.kind == TransportFailureKind::HttpStatus
));
let mut encoded = encode_batch_response(
&request,
&BatchResponse {
responses: vec![
BatchResponseItem {
id: "object".into(),
outcome: Ok(BatchSuccess::Observe(observed_item(None))),
},
BatchResponseItem {
id: "interface".into(),
outcome: Err(ProtocolError {
code: ProtocolErrorCode::InterfaceNotFound,
message: "gone".into(),
}),
},
],
},
limits(),
)
.unwrap();
encoded
.headers_mut()
.insert("content-encoding", "gzip".parse().unwrap());
assert!(matches!(
decode_batch_response(&request, &encoded, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::OK
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
));
}
#[test]
+6 -14
View File
@@ -10,11 +10,11 @@ use wip_http::wip_protocol::{
};
use wip_http::{
ClientResponseError, CodecError, DecodedResponse, Endpoint, InvalidResponseKind, Limits,
ResourceLimitKind, decode_call_operation_metadata, decode_call_operation_request,
decode_call_operation_response, decode_fetch_interface_request,
decode_fetch_interface_response, decode_observe_request, decode_observe_response,
encode_call_operation_request, encode_call_operation_response, encode_fetch_interface_request,
encode_fetch_interface_response, encode_observe_request, encode_observe_response,
decode_call_operation_metadata, decode_call_operation_request, decode_call_operation_response,
decode_fetch_interface_request, decode_fetch_interface_response, decode_observe_request,
decode_observe_response, encode_call_operation_request, encode_call_operation_response,
encode_fetch_interface_request, encode_fetch_interface_response, encode_observe_request,
encode_observe_response,
};
fn limits() -> Limits {
@@ -404,23 +404,15 @@ fn malformed_unknown_null_and_noncanonical_base64_are_rejected() {
}
#[test]
fn caller_limits_bound_bytes_and_nesting() {
fn caller_limits_remain_local_codec_failures() {
let tiny = Limits::new(8, 8, 2, 1).unwrap();
let too_large = decode_observe_request(br#"{"path":"/long","depth":0}"#, tiny).unwrap_err();
assert!(matches!(&too_large, CodecError::BodyTooLarge { .. }));
assert_eq!(
too_large.resource_limit_kind(),
Some(ResourceLimitKind::RepresentationBytes)
);
let nested = br#"{"path":[[["/"]]],"depth":0}"#;
let nesting_limited = Limits::new(1024, 1024, 2, 1).unwrap();
let too_deep = decode_observe_request(nested, nesting_limited).unwrap_err();
assert!(matches!(&too_deep, CodecError::NestingTooDeep { .. }));
assert_eq!(
too_deep.resource_limit_kind(),
Some(ResourceLimitKind::Logical)
);
let outbound_limited = Limits::new(1_000_000, 1_000_000, 1, 10).unwrap();
assert!(
+81 -35
View File
@@ -3,10 +3,9 @@ use wip_http::wip_protocol::{
ObserveRequest, ProtocolError, ProtocolErrorCode, ProtocolInteraction,
};
use wip_http::{
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, ResourceLimitKind,
TransportFailureKind, decode_observe_response, encode_protocol_error_response,
encode_resource_limit_error_response, error_code_name, is_json_content_type, status_for_error,
status_for_resource_limit,
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, TransportFailureKind,
decode_observe_response, encode_protocol_error_response, error_code_name, is_json_content_type,
status_for_error,
};
fn limits() -> Limits {
@@ -120,41 +119,45 @@ fn every_protocol_code_has_stable_name_and_status() {
}
#[test]
fn resource_limit_statuses_encode_and_decode_by_concrete_limit_class() {
fn logical_resource_limit_uses_only_422_and_bare_413_is_transport_failure() {
let error = ProtocolError {
code: ProtocolErrorCode::ResourceLimitExceeded,
message: "bounded".into(),
message: "logical observation limit exceeded".into(),
};
let cases = [
(
ResourceLimitKind::RepresentationBytes,
StatusCode::PAYLOAD_TOO_LARGE,
),
(ResourceLimitKind::Logical, StatusCode::UNPROCESSABLE_ENTITY),
];
for (kind, status) in cases {
assert_eq!(status_for_resource_limit(kind), status);
let encoded = encode_resource_limit_error_response(
ProtocolInteraction::Observe,
&error,
kind,
limits(),
)
.unwrap();
assert_eq!(encoded.status(), status);
assert_eq!(
decode_observe_response(&request(), &encoded, limits()).unwrap(),
DecodedResponse::ProtocolFailure {
error: error.clone(),
status_mismatch: None,
}
);
}
let default =
let encoded =
encode_protocol_error_response(ProtocolInteraction::Observe, &error, limits()).unwrap();
assert_eq!(default.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert_ne!(default.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(encoded.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(
decode_observe_response(&request(), &encoded, limits()).unwrap(),
DecodedResponse::ProtocolFailure {
error: error.clone(),
status_mismatch: None,
}
);
let mut noncanonical = encoded;
*noncanonical.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
assert_eq!(
decode_observe_response(&request(), &noncanonical, limits()).unwrap(),
DecodedResponse::ProtocolFailure {
error,
status_mismatch: Some(wip_http::StatusMismatch {
actual: StatusCode::PAYLOAD_TOO_LARGE,
expected: StatusCode::UNPROCESSABLE_ENTITY,
}),
}
);
let bare_413 = Response::builder()
.status(StatusCode::PAYLOAD_TOO_LARGE)
.body(Vec::<u8>::new())
.unwrap();
assert!(matches!(
decode_observe_response(&request(), &bare_413, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::PAYLOAD_TOO_LARGE
&& failure.kind == TransportFailureKind::HttpStatus
));
}
#[test]
@@ -171,6 +174,49 @@ fn rate_limit_status_without_protocol_envelope_stays_transport_failure() {
));
}
#[test]
fn unsupported_response_content_coding_is_transport_failure() {
let mut encoded = encode_protocol_error_response(
ProtocolInteraction::Observe,
&ProtocolError {
code: ProtocolErrorCode::NotFound,
message: "missing".into(),
},
limits(),
)
.unwrap();
encoded
.headers_mut()
.insert("content-encoding", "gzip".parse().unwrap());
assert!(matches!(
decode_observe_response(&request(), &encoded, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::NOT_FOUND
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
));
let success = response(StatusCode::OK, br#"{"object":{"name":"","interfaces":[]}}"#);
let mut identity = success.clone();
identity
.headers_mut()
.insert("content-encoding", "identity".parse().unwrap());
assert!(matches!(
decode_observe_response(&request(), &identity, limits()).unwrap(),
DecodedResponse::Success(_)
));
let mut unsupported = success;
unsupported
.headers_mut()
.insert("content-encoding", "br".parse().unwrap());
assert!(matches!(
decode_observe_response(&request(), &unsupported, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::OK
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
));
}
#[test]
fn valid_success_body_requires_exactly_http_200() {
let success_body = br#"{"object":{"name":"","interfaces":[]}}"#;
+42 -2
View File
@@ -61,7 +61,10 @@ fn inbound_http_metadata_enforces_post_route_and_json() {
.header("content-type", "application/json")
.body(Vec::<u8>::new())
.unwrap();
assert!(validate_http_request(&endpoint, Route::Observe, &wrong_method).is_err());
let error = validate_http_request(&endpoint, Route::Observe, &wrong_method).unwrap_err();
assert!(matches!(&error, RequestMetadataError::MethodNotAllowed));
assert_eq!(error.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(error.allow(), Some("POST"));
let wrong_route = Request::builder()
.method(Method::POST)
@@ -69,7 +72,21 @@ fn inbound_http_metadata_enforces_post_route_and_json() {
.header("content-type", "application/json")
.body(Vec::<u8>::new())
.unwrap();
assert!(validate_http_request(&endpoint, Route::Observe, &wrong_route).is_err());
let error = validate_http_request(&endpoint, Route::Observe, &wrong_route).unwrap_err();
assert!(matches!(&error, RequestMetadataError::NotFound));
assert_eq!(error.status(), StatusCode::NOT_FOUND);
assert_eq!(error.allow(), None);
let query_and_wrong_method = Request::builder()
.method(Method::GET)
.uri("/wip/v1/observe?unexpected=true")
.header("content-type", "application/json")
.body(Vec::<u8>::new())
.unwrap();
assert!(matches!(
validate_http_request(&endpoint, Route::Observe, &query_and_wrong_method).unwrap_err(),
RequestMetadataError::NotFound
));
for accept in [
"text/plain, */*;q=1, application/json;q=0",
@@ -114,6 +131,29 @@ fn inbound_http_metadata_enforces_post_route_and_json() {
validate_http_request(&endpoint, Route::Observe, &unsupported_media_type).unwrap_err();
assert!(matches!(&error, RequestMetadataError::UnsupportedMediaType));
assert_eq!(error.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
for coding in ["gzip", "identity, gzip"] {
let unsupported_coding = Request::builder()
.method(Method::POST)
.uri("/wip/v1/observe")
.header("content-type", "application/json")
.header("content-encoding", coding)
.body(Vec::<u8>::new())
.unwrap();
let error =
validate_http_request(&endpoint, Route::Observe, &unsupported_coding).unwrap_err();
assert!(matches!(&error, RequestMetadataError::UnsupportedMediaType));
assert_eq!(error.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
let identity_coding = Request::builder()
.method(Method::POST)
.uri("/wip/v1/observe")
.header("content-type", "application/json")
.header("content-encoding", "identity")
.body(Vec::<u8>::new())
.unwrap();
assert!(validate_http_request(&endpoint, Route::Observe, &identity_coding).is_ok());
}
#[test]