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 generated lib/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 CratestackTransportException when there is no answer, never with a stack-specific type.
  • Send request.body exactly 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:
  • baseUri is the scheme, authority and optional path prefix every request path is joined onto: https://api.example.com or https://example.com/v1. A prefix is kept. It replaces dio.options.baseUrl, which the transport no longer reads.
  • format is CratestackWireFormat.cbor (the default) or CratestackWireFormat.json. It sets the request body format and the Accept preference. The answer is always decoded by its own Content-Type, so an adapter set to one format still reads the other. A response of an unsupported Content-Type is a FormatException on the JSON REST adapter too; it used to hand back the raw string.
  • useRustTransport (REST only) marks every request with cratestackUseRustTransportExtraKey in CratestackHttpRequest.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 that stream() uses; see Streaming.
The CBOR codec is the native 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:
Code that used to catch 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 as 1e+21 goes out as 1e%2B21, so the + of the exponent survives);
  • a null value is omitted. Dio sent a bare key.
For every value the generated query builders emit, the output is byte-identical to what Dio produced. The 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

Implement send. 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.extra exists and a transport forwards it, but neither CratestackCallOptions nor CratestackRpcCallOptions can fill it, so a single call cannot tell the transport chain under it something that is not a header. Tracked in cratestack#1207.
  • An AuthProvider refusal 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