ibapi 4.1.0

A Rust implementation of the Interactive Brokers TWS API, providing a reliable and user friendly interface for TWS and IB Gateway. Designed with a focus on simplicity and performance.
Documentation
---
id: proto-aware-accessors
title: ResponseMessage accessors must be proto-aware
cluster: wire
status: active
triggers:
  - adding a &self accessor on ResponseMessage
  - adding a public API on a proto inbound message type
  - subscription routing silently returns no data
  - a new IncomingMessages variant correlates to a request
symbols: [ResponseMessage, peek_int, request_id, order_id, execution_id, routes_by_request_id, text_request_id_field, debug_assert_request_id_routable, first_unroutable_by_request_id]
related: [proto-only-decoding, coverage-floor]
precedents: ["#519", "#647", "#730"]
memory: [feedback_request_id_index_registration, feedback_sync_protobuf_routing, project_protobuf_only]
---

Any `&self` accessor on `ResponseMessage` that reads by text-field index — `request_id`,
`order_id`, `execution_id`, `peek_int`, and any future sibling — needs a `raw_bytes`-first
branch. Every production inbound message arrives proto-framed: `fields` holds only the
message id and the payload lives in `raw_bytes`.

Don't decode the whole proto struct to read one field. Define a minimal `prost::Message`
envelope and let prost length-skip the rest — `ProtoIdEnvelope` (`int32 @ tag 1`) covers most
messages, `ExecutionDetailsMinimal` shows the nested case.

**A field's tag is not always the same across messages, and that decides the shape of the
accessor.** `request_id` and `order_id` sit at tag 1 everywhere, so one envelope serves them.
`contract_id` does not: it is nested inside a `Contract` sub-message that sits at tag 2 for
`ContractData` / `OpenOrder` / `ExecutionDetails` / `Position`, tag 1 for `PortfolioValue` /
`CompletedOrder`, and tag 3 for `PositionMulti`. An accessor for it needs
`match self.message_type()` and one envelope per tag position — check the tags in
`src/proto/protobuf.rs` before assuming a single envelope will do.

**A new `pub fn` here can trip `-D warnings` before it has a caller.** `ResponseMessage` is
`pub(crate)` (#581), so an accessor nothing calls yet is dead code, and the same applies
per-feature: see `is_shutdown`'s `#[cfg_attr(not(feature = "sync"), allow(dead_code))]`, whose
only caller is the sync transport.

**Adding a public API on a proto inbound message type also requires an entry in
`text_request_id_field` (`src/messages.rs`).**

## Why

`ResponseMessage::request_id()` short-circuits on a missing `text_request_id_field` entry
*before* it reaches the protobuf-envelope branch, so an unregistered message type silently
never routes — the subscription just receives nothing.

That table is a deliberate allow-list, not a sentinel. It prevents misrouting messages where
`int @ tag 1` means something other than a request id (`MarketRule.market_rule_id`,
`OrderBound.perm_id`). `routes_by_request_id()` is a thin wrapper over it;
`text_request_id_field` is the single source of truth and carries the text-frame field index
(1 vs 2) — populate the right bucket. No production message reaches the text path now, but
tests still construct text-framed fixtures.

**`MessageBusStub` tests structurally bypass the dispatcher and pass with the registration
missing.** PR #647 shipped exactly this bug, then refactored the table; before #730 only a
live-gateway smoke test surfaced the gap.

## The gate

`debug_assert_request_id_routable` (`src/subscriptions/common.rs`) runs in both subscription
constructors and panics when a `request_id`-keyed subscription is built for a decoder whose
`RESPONSE_MESSAGE_IDS` names a type the dispatcher cannot route to it. The classification is
`first_unroutable_by_request_id` in `src/transport/routing.rs`, whose two accepting arms
mirror `determine_routing`: order-scoped types, and anything with a `text_request_id_field`
entry.

`Error` was a third accepting arm until #734. It is not routable by `request_id` — it has no
`text_request_id_field` entry — but sixteen decoders declared it, so the guard exempted it to
keep them passing: const declares, guard exempts, circular. `determine_routing` classifies
`Error` before the allow-list, so it reaches a subscription as `RoutedItem::Error`/`Notice`
and never as a `Response` for `decode` to see. The declarations were the wrong half; they were
removed and the exemption with them.

Stub tests bypass the dispatcher but not the constructor, so this fires in exactly the tests
that used to pass silently. It is compiled out of release builds — the invariant is over
static tables and cannot depend on caller input.

Its reach is the set of decoders some test instantiates. A decoder with no test at all is
still unguarded, which is one more reason for
[coverage floor](../testing/coverage-floor.md).

On the minimal-envelope point: the dispatcher calls these accessors three or four times per
inbound message. A full decode of `OpenOrder` or `ExecutionDetails` costs roughly twenty
String allocations each; an envelope holding just `id @ tag 1` is essentially free.

Cursor primitives return `Result<T, Error>`. Never reintroduce a panicking variant — a
panicking accessor was the root of PR #519's bug class.

## Precedents

- #519 — a panicking accessor on a proto-framed message.
- #647 — shipped a missing routing registration, then split the table into
  `routes_by_request_id` + `text_request_id_field`.
- #730 — replaced the prose warning with the constructor guard. It found a second instance on
  its first run: `MarketDataType`, declared by the `TickTypes` decoder since #516 and missing
  from the table ever since, so `TickTypes::MarketDataType` never reached a `market_data`
  subscription.