Cross-Transport Parity Invariant
Connectum exposes two transports for ConnectRPC services:
- the HTTP/2 transport — the production wire protocol;
- the in-process transport —
createLocalTransport(server)/Server.client(ServiceDesc), which delivers requests directly into the registered route handlers without serialization or a socket.
The framework guarantees a Behavioural Parity invariant between them:
For every observable behaviour of a ConnectRPC service, the in-process transport produces results that structurally match the HTTP/2 transport. Observable behaviours include response payloads, response headers and trailers,
ConnectErrorcodes / messages / metadata /details, the order of streaming messages, cancellation propagation, OpenTelemetry spans (modulo theconnectum.transportattribute), and metric label sets (modulo thetransportlabel).
This invariant is release-blocking: any PR that breaks it must either be revised or accompanied by a documented carve-out in the spec.
How the invariant is enforced
transportParityTest()driver (@connectum/testing/parity) runs every scenario twice — once overcreateGrpcTransport({ baseUrl })and once overcreateLocalTransport(server)— and performs a structural diff on the normalized result (response, headers, error, OTEL spans, metrics).- Parity suite — aggregated by
scripts/parity-suite.sh, covering interceptors, validation, authorization, streaming, error mapping, coexistence, and OTEL. parity-gateCI job (.github/workflows/parity-gate.yml) runs the suite on every pull request and every push tomain.
When you MUST add a parity scenario
Any change that touches observable RPC behaviour must add (or extend) a transportParityTest() scenario in the appropriate file under packages/testing/tests/parity/ (or packages/otel/tests/parity/ for observability). This includes, but is not limited to:
- a new interceptor in
@connectum/interceptors; - a new authentication or authorization rule in
@connectum/auth(programmatic or proto-declared); - a new validation rule wired through
protovalidate/buf.validate; - a new OpenTelemetry instrument, attribute, or span event in
@connectum/otel; - any change to
ConnectErrormapping in@connectum/coreor interceptors; - any change to header / trailer propagation;
- any change to streaming semantics (ordering, cancellation, back-pressure surfaces).
If the change is purely transport-local — for example, an HTTP-only header that has no in-process analogue — document the carve-out in the parity test file as a code comment and in the spec.
When parity does not apply
A small set of behaviours are explicitly transport-specific and are not asserted by the parity driver:
- Network-level concerns: TLS, HTTP/2 framing, TCP keepalive,
:authoritypseudo-header, realreq.urlhost/port. - Wire-format concerns: gzip/identity content-encoding negotiation, message framing on the wire.
- The synthetic origin
https://in-memory/<service>/<method>that the in-process transport injects for interceptors that readreq.url. - The
connectum.transportspan attribute and thetransportmetric label, which differ by design and are stripped before structural diff.
Pull request checklist
Every PR that touches a service-observable surface must:
- [ ] add or extend a
transportParityTest()scenario covering the change, or - [ ] explicitly mark the change as
parity: N/Ain the PR description and justify why.
The parity-gate CI job will block merge on any structural diff.
References
- In-process transport guide
- Parity coverage report
- Internal design notes: see ADR for in-process transport (registry-based local invoke)
