chore(deps): consume alkcall 0.7.0 — CONNECTION_CLOSED maps 503
Dependency wave + one wire-surface behavior fix: - bump alkcall 0.6 -> 0.7 (gateway feature unchanged). No code change forced: this crate constructs no ServingConfig (the one 0.7.0 struct-literal break — from_wss keeps the pure-consumer from_connection; opt-in serving remains the review-006 Part C deferred decision), and the CF-006 per-call opener-identity overlay is behavior-only upstream (hub-forwarded WS-ferry opens now resolve the end client's identity, the intended effect) - gateway error table maps CONNECTION_CLOSED -> 503 Service Unavailable: alkcall 0.7.0's CF-007 amendment makes it the eighth protocol code (ADR-016's list is formal at eight); it previously fell to the catch-all 500, misreporting a retryable, provably-undelivered call as a permanent server failure. retryable: true, so the retry_after details path applies on 503. Tests pin the mapping and the Retry-After passthrough - ADR-023 amended: the protocol-code list is eight (ALREADY_EXISTS + CONNECTION_CLOSED rows added to the §3 table, normative rule and call.error code list updated); http-adapters.md six-code references de-staled Verification: cargo test (456 passed), cargo test --all-features (589 passed), clippy -D warnings (default + all-features), fmt --check, cargo doc --no-deps (0 warnings)
This commit is contained in:
27
CHANGELOG.md
27
CHANGELOG.md
@@ -6,6 +6,33 @@ this crate adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **`alkcall` dependency bumped to 0.7.0** (CF-005/006/007 — the
|
||||
connect-side caller-identity seam + the downstream-findings sweep).
|
||||
No code change in this crate: it constructs no `ServingConfig`
|
||||
(the one struct-literal break — `from_wss` keeps the pure-consumer
|
||||
`ChannelClient::from_connection`; opt-in serving remains the
|
||||
review-006 Part C deferred decision) and the per-call opener-identity
|
||||
overlay (CF-006) is behavior-only upstream — hub-forwarded opens on
|
||||
the WS ferry now resolve the end client's identity instead of the
|
||||
hub's, the intended effect. alkcall 0.7.0's ADR-016 amendment (the
|
||||
protocol-code list is eight codes) resolves the upstream half of
|
||||
review-006 Part C's doc-drift note.
|
||||
|
||||
### Changed (wire-surface behavior)
|
||||
|
||||
- **Gateway error table maps `CONNECTION_CLOSED` → 503 Service
|
||||
Unavailable** (the alkcall 0.7.0 CF-007 amendment makes it the eighth
|
||||
protocol code; ADR-023 amended). Previously it fell to the catch-all
|
||||
500, which misreported a retryable, provably-undelivered call as a
|
||||
permanent server failure. Reachable through the gateway when an
|
||||
imported op's forwarding handler calls into a dropped transport
|
||||
(`from_wss`'s pending-call fail-all is retryable
|
||||
`CONNECTION_CLOSED`). The code is `retryable: true`, so the existing
|
||||
`retry_after` details path applies on 503. Tests pin the mapping and
|
||||
the `Retry-After` passthrough.
|
||||
|
||||
## [0.3.0] — 2026-09-07
|
||||
|
||||
### Changed
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -27,9 +27,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alkcall"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44500900b82904d2a870e444051fc041a042e42ff7190803bbc5b3973f4feeb"
|
||||
checksum = "5162a10144df65b03b887e30f77eb30d956ca1ca460a3f81b409128cbcae3199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
||||
@@ -42,7 +42,7 @@ h2 = ["server", "dep:hyper", "hyper-util/http2", "hyper/http2"]
|
||||
http1 = ["server", "dep:hyper", "hyper-util/http1", "hyper/http1"]
|
||||
|
||||
[dependencies]
|
||||
alkcall = { version = "0.6", features = ["gateway"] }
|
||||
alkcall = { version = "0.7", features = ["gateway"] }
|
||||
arc-swap = { version = "1", optional = true }
|
||||
axum = { version = "0.8", optional = true, features = ["ws"] }
|
||||
bytes = "1"
|
||||
|
||||
@@ -4,21 +4,25 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended by alknet ADR-049 — protocol-level code list extended to six; alkcall ADR-021)
|
||||
Accepted (amended by alknet ADR-049 — protocol-level code list extended to six; alkcall ADR-021; alkcall 0.7.0 CF-007 — the list is eight: `ALREADY_EXISTS` (alkcall ADR-022, mapped 409 Conflict by review 006 UP-01) and `CONNECTION_CLOSED` (alkcall ADR-016 CF-001, mapped 503 Service Unavailable in this crate's gateway error table))
|
||||
|
||||
## Context
|
||||
|
||||
The `OperationSpec` in the call crate (now alkcall) has `input_schema` and
|
||||
`output_schema` but no `error_schemas`. The `call.error` payload (the alkcall
|
||||
crate's `call-protocol.md`) carries a `code` and `message`, where `code` is
|
||||
one of six infrastructure codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`,
|
||||
`INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`.
|
||||
one of eight infrastructure codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`,
|
||||
`INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`, `ALREADY_EXISTS`,
|
||||
`CONNECTION_CLOSED`.
|
||||
|
||||
These six codes cover **protocol-level failures** — the call protocol
|
||||
These eight codes cover **protocol-level failures** — the call protocol
|
||||
itself can always fail to find an operation, deny access, reject bad input,
|
||||
reject the wrong dispatch method for the operation type, time out, or hit
|
||||
an internal error. They are emitted by the dispatch machinery (the registry,
|
||||
the adapter), not by operation handlers. `INVALID_OPERATION_TYPE` was added
|
||||
reject the wrong dispatch method for the operation type, time out, hit an
|
||||
internal error, reject an `op/register` collision (`ALREADY_EXISTS`, alkcall
|
||||
ADR-022), or observe a provably-undelivered call (`CONNECTION_CLOSED`,
|
||||
alkcall ADR-016 CF-001 — the only retryable protocol code). They are emitted
|
||||
by the dispatch machinery (the registry, the adapter), not by operation
|
||||
handlers. `INVALID_OPERATION_TYPE` was added
|
||||
by alknet ADR-049 (streaming handler for subscriptions — now alkcall ADR-021;
|
||||
`invoke()` called on a `Sub`, or `invoke_streaming()` on a `Query`/`Mutation`).
|
||||
|
||||
@@ -173,8 +177,8 @@ optional-array convention.
|
||||
|
||||
- `code` — the error code. Either a protocol-level code (`NOT_FOUND`,
|
||||
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
|
||||
`TIMEOUT`) or an operation-level domain code from `error_schemas` (e.g.,
|
||||
`FILE_NOT_FOUND`, `RATE_LIMITED`).
|
||||
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`) or an operation-level
|
||||
domain code from `error_schemas` (e.g., `FILE_NOT_FOUND`, `RATE_LIMITED`).
|
||||
- `message` — human-readable error message. Unstructured — for logging and
|
||||
debugging, not for programmatic handling. Consumers should switch on
|
||||
`code`, not parse `message`.
|
||||
@@ -190,8 +194,8 @@ optional-array convention.
|
||||
|
||||
### 3. Protocol-level vs operation-level error codes
|
||||
|
||||
The six existing codes are **protocol-level** — emitted by the dispatch
|
||||
machinery, not by handlers:
|
||||
The eight protocol-level codes are **protocol-level** — emitted by the
|
||||
dispatch machinery, not by handlers:
|
||||
|
||||
| Code | Emitted by | Meaning |
|
||||
|------|-----------|---------|
|
||||
@@ -201,6 +205,8 @@ machinery, not by handlers:
|
||||
| `INVALID_OPERATION_TYPE` | Registry / `OperationEnv` | Wrong dispatch path for the operation's type (`invoke()` on a `Sub`, `invoke_streaming()` on a `Query`/`Mutation`, `invoke_sink()` on a `Query`/`Mutation`/`Sub`, or `OperationEnv::invoke()` on a `Sub` during composition — alkcall ADR-021; `Pub` ops dispatch via `invoke_sink()`, alkcall ADR-046) |
|
||||
| `INTERNAL` | Registry / Adapter | Handler panic, unhandled error, connection failure |
|
||||
| `TIMEOUT` | Adapter | Request timed out |
|
||||
| `ALREADY_EXISTS` | Registry / collision gate | `op/register` collision — a peer-announced op may not replace the serving side's own registrations (alkcall ADR-022 G-03) |
|
||||
| `CONNECTION_CLOSED` | Client write path | The `call.requested` frame could not be written — the call is provably undelivered (alkcall ADR-016 CF-001). `retryable: true` — the only protocol code a caller may auto-retry. |
|
||||
|
||||
Operation-level domain codes are emitted by **handlers** — the operation's
|
||||
own logic determines what went wrong. They are declared in `error_schemas`
|
||||
@@ -251,9 +257,9 @@ accordingly.
|
||||
```
|
||||
|
||||
**Normative rule (review #002 W20)**: `from_openapi` must not produce error
|
||||
codes that collide with the six protocol-level codes (`NOT_FOUND`,
|
||||
codes that collide with the protocol-level codes (`NOT_FOUND`,
|
||||
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
|
||||
`TIMEOUT`). The adapter prefixes
|
||||
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`). The adapter prefixes
|
||||
imported error codes with `HTTP_` and the status number (e.g., `HTTP_404`,
|
||||
`HTTP_429`) to avoid collision. This is a requirement for the adapter, not
|
||||
a naming convention — the `from_openapi` example above was previously shown
|
||||
|
||||
@@ -553,8 +553,9 @@ for the factoring recommendation (thin shared struct, not a trait).
|
||||
`from_openapi` maps OpenAPI non-2xx response status codes to
|
||||
`ErrorDefinition`s (alkcall ADR-016 §5). The normative rule (review
|
||||
#002 W20): `from_openapi` must not produce error codes that collide
|
||||
with the six protocol-level codes (`NOT_FOUND`, `FORBIDDEN`,
|
||||
`INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`). The
|
||||
with the protocol-level codes (`NOT_FOUND`, `FORBIDDEN`,
|
||||
`INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`,
|
||||
`ALREADY_EXISTS`, `CONNECTION_CLOSED`). The
|
||||
adapter prefixes imported error codes with `HTTP_` and the status
|
||||
number:
|
||||
|
||||
@@ -601,7 +602,7 @@ request-level cap failure (422; GW-16 unified it with the
|
||||
`INVALID_INPUT → 422` mapping — it was hand-rolled as 400 before
|
||||
review-002). `BatchResultEntry.error` references
|
||||
the `BatchError` component (review-002 PRJ-16b): the serialized
|
||||
`CallError` as a oneOf over the six protocol-code envelopes plus a
|
||||
`CallError` as a oneOf over the protocol-code envelopes plus a
|
||||
generic arm carrying the operation-declared codes. See alkcall ADR-016.
|
||||
|
||||
## Why
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
//! (`NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`,
|
||||
//! `TIMEOUT`, `ALREADY_EXISTS` — the seventh code, alkcall ADR-022's
|
||||
//! `op/register` collision policy, mapped 409 Conflict, review-006
|
||||
//! UP-01) map to fixed statuses; operation-level codes imported from
|
||||
//! external HTTP APIs are prefixed `HTTP_<status>` and map to their
|
||||
//! declared status.
|
||||
//! UP-01; `CONNECTION_CLOSED` — the eighth code, alkcall ADR-016's
|
||||
//! retryable undelivered-call code, mapped 503 Service Unavailable,
|
||||
//! alkcall 0.7.0's CF-007 amendment made the list formal) map to fixed
|
||||
//! statuses; operation-level codes imported from external HTTP APIs are
|
||||
//! prefixed `HTTP_<status>` and map to their declared status.
|
||||
//!
|
||||
//! The identity-aware variant maps the ambiguous protocol codes
|
||||
//! (`FORBIDDEN`, `INVALID_OPERATION_TYPE`) to `401` when no token
|
||||
@@ -30,6 +32,7 @@ const PROTOCOL_CODE_INVALID_INPUT: &str = "INVALID_INPUT";
|
||||
const PROTOCOL_CODE_INVALID_OPERATION_TYPE: &str = "INVALID_OPERATION_TYPE";
|
||||
const PROTOCOL_CODE_TIMEOUT: &str = "TIMEOUT";
|
||||
const PROTOCOL_CODE_ALREADY_EXISTS: &str = "ALREADY_EXISTS";
|
||||
const PROTOCOL_CODE_CONNECTION_CLOSED: &str = "CONNECTION_CLOSED";
|
||||
const PROTOCOL_CODE_INTERNAL: &str = "INTERNAL";
|
||||
|
||||
const HTTP_PREFIX: &str = "HTTP_";
|
||||
@@ -40,6 +43,7 @@ const STATUS_FORBIDDEN: u16 = 403;
|
||||
const STATUS_UNPROCESSABLE: u16 = 422;
|
||||
const STATUS_TIMEOUT: u16 = 504;
|
||||
const STATUS_CONFLICT: u16 = 409;
|
||||
const STATUS_SERVICE_UNAVAILABLE: u16 = 503;
|
||||
const STATUS_INTERNAL: u16 = 500;
|
||||
|
||||
const RETRY_AFTER_STATUSES: &[u16] = &[429, 503];
|
||||
@@ -77,6 +81,7 @@ pub fn call_error_to_http_status_with_identity(
|
||||
}
|
||||
PROTOCOL_CODE_TIMEOUT => STATUS_TIMEOUT,
|
||||
PROTOCOL_CODE_ALREADY_EXISTS => STATUS_CONFLICT,
|
||||
PROTOCOL_CODE_CONNECTION_CLOSED => STATUS_SERVICE_UNAVAILABLE,
|
||||
PROTOCOL_CODE_INTERNAL => STATUS_INTERNAL,
|
||||
code if code.starts_with(HTTP_PREFIX) => code[HTTP_PREFIX.len()..]
|
||||
.parse::<u16>()
|
||||
@@ -193,6 +198,25 @@ mod tests {
|
||||
assert_eq!(call_error_to_http_status(&error), 409);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_closed_maps_to_503() {
|
||||
let error = CallError::connection_closed("undelivered");
|
||||
assert_eq!(call_error_to_http_status(&error), 503);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_closed_carries_retry_after_when_details_present() {
|
||||
let error = CallError::connection_closed("undelivered")
|
||||
.with_details(serde_json::json!({ "retry_after": "5" }));
|
||||
let resp = call_error_to_http_response(&error);
|
||||
assert_eq!(resp.status(), 503);
|
||||
let retry_after = resp
|
||||
.headers()
|
||||
.get(header::RETRY_AFTER)
|
||||
.map(|v| v.to_str().unwrap().to_string());
|
||||
assert_eq!(retry_after.as_deref(), Some("5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_maps_to_500() {
|
||||
let error = CallError::internal("boom");
|
||||
|
||||
Reference in New Issue
Block a user