Merge branch 'wt/review-002-gw16-status-drift'
# Conflicts: # src/gateway/dispatch.rs
This commit is contained in:
+45
-27
@@ -30,7 +30,15 @@
|
||||
//! corrections remove only documentation of statuses/codes the runtime
|
||||
//! never emitted (a strict client matching them observed nothing to
|
||||
//! break), and every added slot documents behavior the runtime already
|
||||
//! had — the wire contract is unchanged.
|
||||
//! had — the wire contract is unchanged. `1.4.0` is the GW-16 status
|
||||
//! unification: the runtime's hand-rolled `/publish` framing faults
|
||||
//! and the `/batch` cap rejection moved from 400 to the documented
|
||||
//! `INVALID_INPUT → 422` mapping (one error class, one status), so the
|
||||
//! doc's 400/422 slots moved with the runtime. Clients matching the old
|
||||
//! 400 slots observed behavior the runtime no longer emits; the 422
|
||||
//! slots are where `INVALID_INPUT` was already documented, so the
|
||||
//! client-visible consequence is additive on the documented mapping
|
||||
//! side and the status drift is gone.
|
||||
//!
|
||||
//! # Error fidelity
|
||||
//!
|
||||
@@ -76,7 +84,7 @@ use alkcall::registry::spec::ErrorDefinition;
|
||||
use super::openapi_spec::OpenAPISpec;
|
||||
use crate::gateway::MAX_BATCH_OPERATIONS;
|
||||
|
||||
const GATEWAY_VERSION: &str = "1.3.0";
|
||||
const GATEWAY_VERSION: &str = "1.4.0";
|
||||
const GATEWAY_TITLE: &str = "alk gateway";
|
||||
const OPENAPI_VERSION: &str = "3.0.0";
|
||||
|
||||
@@ -349,10 +357,6 @@ fn publish_responses(operation_errors: &BTreeMap<u16, Value>) -> Value {
|
||||
let mut responses = json!({
|
||||
"200": json_response(ref_schema("CallOk"),
|
||||
"The operation's final ResponseEnvelope: request_id, result=ok, output."),
|
||||
"400": json_response(one_of_refs(&[
|
||||
"CallErrorInvalidInput".to_string(),
|
||||
]),
|
||||
"Malformed NDJSON stream framing: empty body, first line missing 'operation'/'chunk', a line exceeding the 2 MiB per-line cap, or a raw body-read failure (INVALID_INPUT). A later NDJSON line that is not valid JSON is a dispatch-path 422. The gateway's 2 MiB + 64 KiB body-limit layer answers an oversized whole-body upload with a plain-text 413 before the route runs. A non-Pub op without a resolved token is the 401 below (INVALID_OPERATION_TYPE), not this status (PRJ-18)."),
|
||||
"401": json_response(one_of_refs(&[
|
||||
"CallErrorForbidden".to_string(),
|
||||
"CallErrorInvalidOperationType".to_string(),
|
||||
@@ -366,14 +370,14 @@ fn publish_responses(operation_errors: &BTreeMap<u16, Value>) -> Value {
|
||||
"CallErrorInvalidInput".to_string(),
|
||||
"CallErrorInvalidOperationType".to_string(),
|
||||
]),
|
||||
"Dispatch-path client fault: a chunk failed the operation's publish_schema validation (INVALID_INPUT with details.chunk), a later NDJSON line was not valid JSON, or the operation's type is not Pub (INVALID_OPERATION_TYPE, token present)."),
|
||||
"Dispatch-path client fault: NDJSON stream framing faults — empty body, first line missing 'operation'/'chunk', a line exceeding the 2 MiB per-line cap, or a raw body-read failure (INVALID_INPUT, GW-16 unified with the mid-stream status) — plus a later NDJSON line that was not valid JSON, a chunk that failed the operation's publish_schema validation (INVALID_INPUT with details.chunk), or the operation's type is not Pub (INVALID_OPERATION_TYPE, token present). The gateway's 2 MiB + 64 KiB body-limit layer answers an oversized whole-body upload with a plain-text 413 before the route runs."),
|
||||
"500": json_response(one_of_refs(&[
|
||||
"CallErrorInternal".to_string(),
|
||||
"PublishFailure".to_string(),
|
||||
]),
|
||||
"Dispatcher failure, or an operation-level error code without HTTP_ prefix or http_status (the runtime mapper is purely code-driven and such codes surface as 500)."),
|
||||
"504": json_response(ref_response("Timeout"),
|
||||
"The Once-op final envelope dispatch exceeded the 30 s gateway deadline (retryable)."),
|
||||
"The sink dispatch exceeded the 30 s gateway deadline (retryable) — the same deadline bounds the Once-op invoke (GW-17)."),
|
||||
});
|
||||
merge_operation_errors(&mut responses, operation_errors);
|
||||
responses
|
||||
@@ -400,10 +404,10 @@ fn batch_path_item() -> Value {
|
||||
"responses": {
|
||||
"200": json_response(ref_schema("BatchResponse"),
|
||||
"results[] shares entries' order with the request; each entry is an envelope-shaped {request_id, result, output|error} object; entries for Internal ops carry a NOT_FOUND in-band error. Per-call dispatch failures surface only as these in-band entries — there is no HTTP error status for an individual call."),
|
||||
"400": json_response(one_of_refs(&[
|
||||
"422": json_response(one_of_refs(&[
|
||||
"BatchCapExceeded".to_string(),
|
||||
]),
|
||||
"Request-level failure: the batch exceeds 100 operations (INVALID_INPUT, JSON). Malformed JSON bodies are rejected earlier by the extractors with a plain-text 400 (not this JSON shape); a JSON array body whose items fail deserialization is a plain-text extractor 422. Oversized uploads are pre-empted by the gateway's body-limit layer with a plain-text 413."),
|
||||
"Request-level failure: the batch exceeds 100 operations (INVALID_INPUT, JSON; GW-16 unified with the INVALID_INPUT → 422 mapping). Malformed JSON bodies are rejected earlier by the extractors with a plain-text 400 (not this JSON shape); a JSON array body whose items fail deserialization is a plain-text extractor 422. Oversized uploads are pre-empted by the gateway's body-limit layer with a plain-text 413."),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1066,7 +1070,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_version_is_1_3_0_after_projection_truthfulness() {
|
||||
fn info_version_is_1_4_0_after_gw16_status_unification() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let version = spec
|
||||
@@ -1076,8 +1080,8 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(version, GATEWAY_VERSION);
|
||||
assert_eq!(
|
||||
version, "1.3.0",
|
||||
"minor bump: doc-contract corrections only — the wire contract is unchanged (ADR-045)"
|
||||
version, "1.4.0",
|
||||
"minor bump: status-mapping truthfulness corrections — the 422 slots are where INVALID_INPUT was already documented (ADR-045)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1328,12 +1332,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_documents_400_json_shape_and_422_free_call_entries() {
|
||||
fn batch_documents_422_json_shape_for_the_cap_reject() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_BATCH, "post");
|
||||
assert!(responses.contains_key("400"));
|
||||
let schema = response_schema(responses.get("400").unwrap());
|
||||
assert!(
|
||||
!responses.contains_key("400"),
|
||||
"GW-16: the batch cap reject moved to the INVALID_INPUT → 422 mapping"
|
||||
);
|
||||
let schema = response_schema(responses.get("422").unwrap());
|
||||
let refs = one_of_refs_of(schema);
|
||||
assert!(refs.contains(&"#/components/schemas/BatchCapExceeded".to_string()));
|
||||
}
|
||||
@@ -1729,12 +1736,16 @@ mod tests {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_PUBLISH, "post");
|
||||
for status in ["200", "400", "401", "403", "404", "422", "500", "504"] {
|
||||
for status in ["200", "401", "403", "404", "422", "500", "504"] {
|
||||
assert!(
|
||||
responses.contains_key(status),
|
||||
"/publish {status} documented"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!responses.contains_key("400"),
|
||||
"GW-16: /publish framing faults are INVALID_INPUT → 422; no 400 slot remains"
|
||||
);
|
||||
assert!(
|
||||
!responses.contains_key("429") && !responses.contains_key("503"),
|
||||
"no operation-declared statuses on an empty registry"
|
||||
@@ -1742,12 +1753,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publish_400_and_401_carry_the_identity_split_codes() {
|
||||
fn publish_401_and_422_carry_the_identity_split_codes() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_PUBLISH, "post");
|
||||
let refs_400 = one_of_refs_of(response_schema(&responses["400"]));
|
||||
assert!(refs_400.contains(&"#/components/schemas/CallErrorInvalidInput".to_string()));
|
||||
let refs_422 = one_of_refs_of(response_schema(&responses["422"]));
|
||||
assert!(refs_422.contains(&"#/components/schemas/CallErrorInvalidInput".to_string()));
|
||||
let refs_401 = one_of_refs_of(response_schema(&responses["401"]));
|
||||
assert!(
|
||||
refs_401.contains(&"#/components/schemas/CallErrorInvalidOperationType".to_string())
|
||||
@@ -1755,23 +1766,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publish_400_documents_only_the_framing_contract() {
|
||||
fn publish_422_documents_the_unified_framing_and_chunk_contract() {
|
||||
let registry = OperationRegistry::new();
|
||||
let spec = to_openapi(®istry).unwrap();
|
||||
let responses = responses(&spec, PATH_PUBLISH, "post");
|
||||
let refs_400 = one_of_refs_of(response_schema(&responses["400"]));
|
||||
let refs_422 = one_of_refs_of(response_schema(&responses["422"]));
|
||||
assert_eq!(
|
||||
refs_400,
|
||||
vec!["#/components/schemas/CallErrorInvalidInput".to_string()],
|
||||
"PRJ-18: /publish 400 oneOf is exactly the INVALID_INPUT framing code"
|
||||
refs_422,
|
||||
vec![
|
||||
"#/components/schemas/CallErrorInvalidInput".to_string(),
|
||||
"#/components/schemas/CallErrorInvalidOperationType".to_string(),
|
||||
],
|
||||
"GW-16: /publish 422 oneOf carries the framing INVALID_INPUT plus INVALID_OPERATION_TYPE"
|
||||
);
|
||||
let description = responses["400"]
|
||||
let description = responses["422"]
|
||||
.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap();
|
||||
assert!(
|
||||
description.contains("first line missing"),
|
||||
"GW-16: the 422 description names the framing faults now mapped 422: {description}"
|
||||
);
|
||||
assert!(
|
||||
!description.contains("reported INVALID_OPERATION_TYPE"),
|
||||
"PRJ-18: the 400 description must not claim the 401-reported condition as a 400 outcome: {description}"
|
||||
"the 422 description must not claim the 401-reported condition as a 422 outcome: {description}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user