Repository navigation
[api] #[api] cannot be applied to WebSocket or SSE handlers — the return type has no escape hatch #86
Description
Problem
gotcha re-exports WebSocketUpgrade, Sse and Event (gotcha-0.4.1/src/lib.rs:14,23) but #[api] cannot be applied to a handler that uses them. A streaming route is therefore either annotated (and does not compile) or unannotated (and vanishes from the document entirely — extract_operable only finds handlers that submitted an Operable, gotcha-0.4.1/src/router.rs:104,323-330). There is no third option.
Verified against 0.4.1. A plain websocket handler:
#[gotcha::api]
async fn ws(ws: WebSocketUpgrade) -> Response { ws.on_upgrade(|_| async {}) }gives two E0277s — the trait bound 'WebSocketUpgrade: ParameterProvider' is not satisfied, and the trait bound 'axum::http::Response<Body>: Responsible' is not satisfied / the trait 'Schematic' is not implemented for 'axum::http::Response<Body>'.
The parameter half is already solvable; the return half is not. The macro does support a per-parameter #[api(skip)] (gotcha_macro-0.4.1/src/route.rs:75-88), and adding it removes the first error completely. The second survives verbatim: the return type is spliced into <#ty as ::gotcha::Responsible>::response() unconditionally (route.rs:105-109), with no skip, no override, and no way to say "this response is not JSON-shaped". So #[api] on a websocket handler is impossible, and the blocker is precisely the return type.
The obvious dodge fails too — -> impl IntoResponse gives error[E0562]: 'impl Trait' is not allowed in paths, because the macro splices the written return type into a path position.
SSE is the same story. The idiomatic -> Sse<impl Stream<Item = Result<Event, Infallible>>> hits the same E0562; with a concrete stream type to isolate the bound it becomes error[E0277]: the trait bound 'Sse<...>: Responsible' is not satisfied / the trait 'Schematic' is not implemented for 'Sse<...>'.
Separately: #[api(skip)] is effectively undiscoverable. It appears nowhere in the README or any doc comment — only in the macro implementation and one trybuild fixture (gotcha-0.4.1/tests/pass/handler/can_skip_fields.rs:7) that the published Cargo.toml declares no test target for. Its detection is also a substring match on the attribute's token string (route.rs:79-80), so any #[api(..)] parameter attribute containing the letters skip skips the parameter.
Where
gotcha_macro-0.4.1/src/route.rs:105-109— return type spliced into<#ty as Responsible>with no escape hatchgotcha_macro-0.4.1/src/route.rs:75-88— the undocumented per-parameter#[api(skip)]gotcha_core-0.4.1/src/parameter.rs:43-254— the fullParameterProviderlist; noWebSocketUpgradegotcha-0.4.1/src/router.rs:104,323-330— unannotated handlers are silently left out of the spec
How remote-hand works around it
It gives up on documenting them. Four routes are deliberately left unannotated with a comment explaining why: api/mod.rs (session feed), feed/global.rs (GET /api/events), terminals/routes.rs (PTY attach) and chats/mod.rs (POST /api/api-chats/{id}/message, SSE). That happens to match the TypeScript server this was ported from, which also left them out — but it was not a choice, and a WebSocketUpgrade route that should appear in the document has nowhere to go.
Suggested direction
- A return-side escape hatch:
#[api(skip_response)], or#[api(responses(101 = ..))]/#[api(response(content_type = "text/event-stream"))]so a streaming route can be described rather than omitted. impl ParameterProvider for WebSocketUpgrade {}(empty, likeState<T>andRequestalready have atparameter.rs:187,230) so the parameter needs no skip at all.- A
Responsibleimpl forSse<S>describingtext/event-stream, and one foraxum::response::Responsedescribing an opaque body. - Document
#[api(skip)], and parse it properly instead of by substring.
Review against current main
Reviewed after PR #118 merged at e5335d55ebcc8e2917a7fdc4e2beb378334371f8.
The Responsible implementation for axum::response::Response is now present. Explicit responses(...) declarations and drop_default also exist. However, declarations still start with inferred return metadata, so impl IntoResponse and Sse<impl Stream<...>> are still inserted into a trait path. WebSocketUpgrade still has no ParameterProvider, Sse<S> has no Responsible, and parameter skipping still uses a substring match.
Initial proposal (not approved; scope reconsidered below)
- Add
skip_responseas an explicit response-inference opt-out, requiring at least one entry inresponses(...). Use the existing response declaration format; preserve inference and per-status overrides for existing annotations. In this mode, the Rust return type is not used to generate response metadata. - Add the missing WebSocket upgrade parameter adapter and an SSE response adapter describing
200 text/event-stream. WebSocket declarations can explicitly describe the101upgrade response; opaque SSE return types can use the opt-out. - Parse the existing parameter
#[api(skip)]structurally and document both forms of opting out of inference. - Add focused compilation/diagnostic checks and document assertions for streaming handlers and invalid opt-out annotations. Keep runtime WebSocket/SSE behavior unchanged.
This proposal has not been implemented.
Standards review: WebSocket and SSE have different documentation needs
The maintainer raised AsyncAPI for WebSocket and asked which standard applies to SSE. Official specifications establish the following:
- AsyncAPI has WebSocket bindings; its channel/message/operation model can describe communication after the connection is established. Describing an HTTP
101upgrade in OpenAPI alone does not describe those messages. - SSE's wire format is defined by WHATWG HTML, using
text/event-stream. - OpenAPI 3.2 natively describes SSE streams.
itemSchemaapplies to each parsed event;event,id, anddataare strings, andretryis an integer. JSON encoded indatacan be described withcontentMediaTypeandcontentSchema. See also the official streaming guide.
Gotcha currently emits OpenAPI 3.0.0 in gotcha/src/openapi/mod.rs:205. The earlier proposal using a string body and text/event-stream only supplies basic response documentation; it does not define individual event contracts. Adding skip_response resolves a macro inference limitation but is not complete streaming API documentation.
Revised architectural recommendation, pending maintainer agreement: consider WebSocket message contracts through AsyncAPI separately, and SSE through OpenAPI. Native typed SSE support requires evaluating an OpenAPI 3.2 migration and its schema/tooling compatibility before finalizing the Rust annotation design. The earlier combined implementation proposal is on hold; no code has been changed.
Prerequisite: update the oas data model
Gotcha and gotcha_core currently depend on oas = "0.1", resolving locally to 0.1.1. Kilerd/oas main is 0.2.1, whose published documentation still targets OpenAPI 3.0. Its MediaType has no itemSchema; Schema still exposes a single-string type and the 3.0 nullable field. A dependency version bump alone cannot supply native OpenAPI 3.2 SSE support.
Recommended order: first add OpenAPI 3.2 model/serialization support in oas; then migrate gotcha and gotcha_core together, including macro-generated schemas and 3.0-to-3.1/3.2 nullability semantics; then implement typed SSE documentation. The official schema migration guide explains the JSON Schema changes. WebSocket message contracts remain a separate AsyncAPI design. Implementation scope is still awaiting review.
Dependency update
oas 0.3.0 is now published and Kilerd/oas#1 is closed. The release was verified through crates.io and the downloaded package. It supplies the schema/streaming representation, but does not automatically migrate 3.0 schema semantics or claim complete 3.2 coverage. Gotcha's dependency and schema-generation migration is tracked separately in #119, pending approval; typed SSE work follows that migration.
Next review after the OpenAPI 3.2 migration (pending approval)
PR #120 is merged at 2863e69a700d12f0c291ccd20fae51e625844e86; #119 is closed. Gotcha now uses oas 0.3 and emits OpenAPI 3.2, including reference handling for itemSchema and contentSchema. The dependency migration prerequisite above is complete.
The remaining macro boundary is concrete: responses(...) still starts by inferring the written return type through Responsible (or ResultResponse for errors(...)). Opaque return types such as Sse<impl Stream<...>> therefore remain unusable even with explicit declarations. ResponseMeta exposes only a whole-body body type, not a stream-item type.
Proposed next PR: feat(api): support explicit streaming response contracts.
- Add
skip_responseto bypass return inference only when a nonemptyresponses(...)contract is supplied. It must not emit a trait path using the opaque return type. Reject combining it witherrors(...), whose existing meaning depends on inferring the success branch; declare all statuses underresponses(...)in this mode. - Extend the existing
response(...)declaration withitem = "EventType"forMediaType.item_schema. Require an explicitcontent_typefor item schemas. Retainbodyas the independent whole-body schema; neither field should be silently reinterpreted as the other. - For SSE,
EventTypedescribes the complete parsed event (event,id, anddatastrings, optionalretryinteger), not just JSON contained indata. A customSchematicimplementation can use oascontentMediaType/contentSchemafor JSON encoded indata. The generated item schema participates in the existing component registry. An SSE-specific convenience API can be reviewed separately. - Validate declarations and compile opaque-return handlers; assert generated item schemas and resolved references. Runtime SSE behavior is unaffected by these documentation declarations.
Example proposed syntax:
#[api(
skip_response,
responses(response(
status = 200,
content_type = "text/event-stream",
item = "ChatSseEvent"
))
)]
async fn events() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
// Existing Axum stream implementation.
}This is a proposal only; no implementation has started. WebSocket/AsyncAPI message contracts and the separate parameter-skipping cleanup are outside this PR. Scalar 1.68.0 can display received SSE data in its request client, but documentation rendering for itemSchema awaits scalar/scalar#10178, which is still open as of this review.
Activity