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:
2026-09-07 18:01:36 +00:00
parent 126fdec243
commit dbaaa4ec8c
6 changed files with 80 additions and 22 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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");