fix(https): align canonical binding semantics

This commit is contained in:
2026-09-30 07:50:51 +09:00
parent beeffae9c5
commit 625f4c148d
9 changed files with 669 additions and 109 deletions
+72 -4
View File
@@ -3,9 +3,10 @@ use wip_https::wip_protocol::{
FetchRequest, ProtocolError, ProtocolErrorCode, ProtocolInteraction,
};
use wip_https::{
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, TransportFailureKind,
decode_fetch_response, encode_protocol_error_response, error_code_name, is_json_content_type,
status_for_error,
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, ResourceLimitKind,
TransportFailureKind, decode_fetch_response, encode_protocol_error_response,
encode_resource_limit_error_response, error_code_name, is_json_content_type, status_for_error,
status_for_resource_limit,
};
fn limits() -> Limits {
@@ -95,7 +96,7 @@ fn every_protocol_code_has_stable_name_and_status() {
(
ProtocolErrorCode::ResourceLimitExceeded,
"resource_limit_exceeded",
StatusCode::TOO_MANY_REQUESTS,
StatusCode::UNPROCESSABLE_ENTITY,
),
(
ProtocolErrorCode::Internal,
@@ -115,6 +116,73 @@ fn every_protocol_code_has_stable_name_and_status() {
}
}
#[test]
fn resource_limit_statuses_encode_and_decode_by_concrete_limit_class() {
let error = ProtocolError {
code: ProtocolErrorCode::ResourceLimitExceeded,
message: "bounded".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::Fetch,
&error,
kind,
limits(),
)
.unwrap();
assert_eq!(encoded.status(), status);
assert_eq!(
decode_fetch_response(&request(), &encoded, limits()).unwrap(),
DecodedResponse::ProtocolFailure {
error: error.clone(),
status_mismatch: None,
}
);
}
let default =
encode_protocol_error_response(ProtocolInteraction::Fetch, &error, limits()).unwrap();
assert_eq!(default.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert_ne!(default.status(), StatusCode::TOO_MANY_REQUESTS);
}
#[test]
fn rate_limit_status_without_protocol_envelope_stays_transport_failure() {
let response = Response::builder()
.status(StatusCode::TOO_MANY_REQUESTS)
.body(b"rate limited".to_vec())
.unwrap();
assert!(matches!(
decode_fetch_response(&request(), &response, limits()).unwrap_err(),
ClientResponseError::TransportBinding(failure)
if failure.status == StatusCode::TOO_MANY_REQUESTS
&& failure.kind == TransportFailureKind::HttpStatus
));
}
#[test]
fn valid_success_body_requires_exactly_http_200() {
let success_body = br#"{"name":"","interfaces":[]}"#;
for status in [StatusCode::CREATED, StatusCode::ACCEPTED] {
assert!(matches!(
decode_fetch_response(&request(), &response(status, success_body), limits())
.unwrap_err(),
ClientResponseError::InvalidResponse {
kind: InvalidResponseKind::NonCanonicalSuccessStatus,
..
}
));
}
}
#[test]
fn server_encoder_rejects_codes_disallowed_for_the_interaction() {
let error = ProtocolError {