Dart HTTP transport
(Since 0.16.0, cratestack#1204; the*DioAdapter classes this replaces are the shipped surface through 0.15.4. Upgrading from them is a one-line change per adapter, see Migrating the Dart client to the HTTP transport.)
Every package cratestack generate-dart writes sends through one small interface, CratestackHttpTransport. It sits under the two adapter interfaces the generated client already depended on, CratestackClientAdapter (REST) and CratestackRpcAdapter (RPC). Dio is one implementation of it. fespalier’s HttpTransport is another (see The fespalier preset), and an app can write its own.
The seam is pure Dart and carries no dependency, so the generated package no longer forces a Dio on an app that sends some other way: its own client, request signing over the final bytes, a captive-portal check, HTTP spans.
The adapter owns the codec work, the headers, the dispatch on the answer’s Content-Type and the error mapping. The transport owns the connection, and with it retries, deadlines and interception.
The seam
Declared in the generatedlib/src/runtime.dart of every package:
The contract a transport honours:
- Resolve with the response for every HTTP status. A 4xx or 5xx answer is not an error at this layer; the adapter maps it.
- Fail with a
CratestackTransportExceptionwhen there is no answer, never with a stack-specific type. - Send
request.bodyexactly as given, so a signer layered above sees the bytes that go out, and add no header the request does not carry beyond what the platform adds itself.
One adapter per protocol
CratestackHttpAdapter (REST) and CratestackRpcHttpAdapter (RPC) are written once over the seam. Both take a transport, a baseUri and a format:
baseUriis the scheme, authority and optional path prefix every request path is joined onto:https://api.example.comorhttps://example.com/v1. A prefix is kept. It replacesdio.options.baseUrl, which the transport no longer reads.formatisCratestackWireFormat.cbor(the default) orCratestackWireFormat.json. It sets the request body format and theAcceptpreference. The answer is always decoded by its ownContent-Type, so an adapter set to one format still reads the other. A response of an unsupportedContent-Typeis aFormatExceptionon the JSON REST adapter too; it used to hand back the raw string.useRustTransport(REST only) marks every request withcratestackUseRustTransportExtraKeyinCratestackHttpRequest.extra, for a host interceptor that routes marked requests through the Rust runtime. It is never a header.cborSeqDecoder(RPC only) swaps the boundary scanner thatstream()uses; see Streaming.
cratestack_cbor by default, or package:cbor under --no-native-cbor. See The CBOR codec. The wire output is unchanged by the move to the seam.
Dio is one transport
CratestackDioTransport(dio) lives in its own generated file, lib/src/dio_transport.dart, the only file in lib/src/ that names Dio. The default and riverpod presets emit it and keep dio in their pubspecs; the fespalier preset emits neither.
It wraps the Dio you already have, so interceptors, base headers, certificate pinning and retry keep working. It sends with ResponseType.stream and validateStatus: (_) => true, copies extra into Options.extra, and cancels through a CancelToken, including while the body is still streaming. It maps Dio’s failures onto the transport kinds:
Because the adapter sends an absolute URI, an interceptor that reads
options.path now sees an absolute URI.
Errors
Three families, and they do not overlap:CratestackRestException.body is the decoded error body (CBOR or JSON), the text of a body of any other Content-Type, or null when there was none or it did not decode. Before this change REST had no typed remote error at all and a raw DioException leaked out of execute.
CratestackTransportException is sealed, with four kinds:
Because the type is sealed, a
switch over a caught CratestackTransportException is exhaustive:
DioException around a generated call now catches CratestackRestException or CratestackRpcException for an error answer and CratestackTransportException for the rest. on CratestackRestException means “the server said no” and nothing else.
Call options
CratestackCallOptions (REST) and CratestackRpcCallOptions (RPC) now carry the same three fields:
Streaming
CratestackRpcHttpAdapter.stream() no longer buffers the body. It reads an application/cbor-seq answer as it arrives, through CborSeqStreamTransformer, so frames are delivered before the body ends. It ships a pure-Dart CratestackCborSeqScanner as the default boundary scanner. Pass a CborSeqDecoderHandle factory as cborSeqDecoder: to use another, such as an FFI-backed one.
A final Tag(48900, RpcErrorBody) item is the in-band stream error and becomes a CratestackRpcException. A server that answers with the single-array application/cbor or a JSON answer is still read. See RPC transport for the server side of streaming.
Query encoding
A canonical query encoder,cratestackCanonicalizeQuery, now builds the REST query string, so no HTTP client’s own list or null rules apply:
- pairs go out in map order, with keys and values through
Uri.encodeQueryComponent; - a list repeats its key,
tag=a&tag=b; - a nested map nests with literal brackets,
filter[kind]=x; - every other value is its
toString(), percent-encoded (a double such as1e+21goes out as1e%2B21, so the+of the exponent survives); - a
nullvalue is omitted. Dio sent a barekey.
null rule is the one behavioural change, and it only matters to code that put a null into a hand-built query map.
Writing your own transport
Implementsend. Resolve for every status, throw a CratestackTransportException when there is no answer, and honour request.abort:
Known gaps
- Call options cannot carry transport attributes.
CratestackHttpRequest.extraexists and a transport forwards it, but neitherCratestackCallOptionsnorCratestackRpcCallOptionscan fill it, so a single call cannot tell the transport chain under it something that is not a header. Tracked in cratestack#1207. - An
AuthProviderrefusal cannot shape its response body. The generated handler encodes any auth error as the standard{code, message}envelope, so a client cannot distinguish refusal reasons by a body member. Tracked in cratestack#1208.
See also
- The
fespalierpreset — the Riverpod surface over fespalier’sHttpTransport, with no Dio - Migrating the Dart client to the HTTP transport
- Dart client generation — presets, flags and the Riverpod surface
- Client Runtime — how the Dio-direct and FFI-bridge paths relate