You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(contracts): propagate routed errors from blocking matching_symbols (#735)
* fix(contracts): propagate routed errors from blocking matching_symbols
Retires the one-shot IncomingMessages::Error arms, the other half of the
class #734 proved dead. fold_one_shot runs its processor only on Some(Ok(_)),
and RoutedItem::into_legacy yields that only for RoutedItem::Response, so an
error frame reaches no decoder. Thirteen sites go: seven decode_*_message
dispatchers (accounts x3, contracts, scanner, config x2) and six inline
message_type() == Error guards in contracts::{sync,async} and
market_data::historical::{sync,async}. connection/common.rs keeps its arm —
it reads frames during the handshake, before a dispatcher exists.
One dead arm was hiding a live bug. Blocking matching_symbols read its
response with `if let Some(Ok(mut message))`, so a routed error fell through
to Ok(Vec::new()) — a rejected pattern was indistinguishable from "no symbols
matched". The async twin already returned Err(e). Both now match.
MessageBusStub classifies error frames like the dispatcher. Removing the arms
was about to cost a third batch of tests deleted for the same reason (#734
dropped 17), so the fixture was fixed instead of the tests: routed_items()
runs each fixture through determine_routing/classify_error, so an error frame
arrives as RoutedItem::Error/Notice as it would on the wire. Both tests that
would have been deleted then passed unmodified, and the sync matching_symbols
test became a real regression guard — reverting the fix reproduces Ok(vec![]).
Renames two config tests that claimed the removed behaviour in their names
and asserted only is_err(), which would have passed through the change
silently; they now pin UnexpectedResponse.
* fix(orders): surface rejected what-if orders from OrderBuilder::analyze
Restores the two historical streaming-error tests deleted in #734. They pass
unmodified against the classifying stub, which shows the #734 reasoning was
wrong in principle: "covered at the transport layer" conflates delivery with
consumption. The transport tests prove the dispatcher produces
RoutedItem::Error; they say nothing about whether a given public API hands it
to the caller.
Sweeping every Some(Ok(..)) consumption site for that shape found one more
API that did not. OrderBuilder::analyze dropped routed errors on both sides —
`if let Ok(..)` inside the loop on sync, `while let Some(Ok(..))` on async —
returning UnexpectedEndOfStream instead of the rejection, on the one API
where rejection is a routine outcome (code 201). Every other site handles
Some(Err(e)).
The four existing analyze tests missed it because they exercise a
re-implementation: the builder test modules define their own analyze on
OrderBuilder<'a, MockOrderClient> returning Vec<PlaceOrder>, so the
production methods on Client had no coverage at all. Added two tests at the
real seam; reverting either fix reproduces UnexpectedEndOfStream. The rest of
that mock surface is recorded as a follow-up.
* refactor: /simplify pass on the routed-error changes
Stub classification now covers the intercepted *set*, not just Error:
`_ => Response(message)` reproduced `RoutingDecision::Error` by name, so
Shutdown still reached decoders as a Response. Both types the dispatcher
intercepts are handled, matching DISPATCHER_INTERCEPTED, and the fixture
conversion goes through the existing From<ResponseMessage> impl.
Dead code the arm removals left behind: From<ResponseMessage> for Error and
From<&ResponseMessage> for Error had zero callers once the decoders stopped
using them; deleted with the two tests that only exercised the impls
themselves. Config decoder doc still described its deleted Error arm.
Both analyze loops collapse to `?` — the three-arm match with `Ok(_) => {}`
and `Err(e) => return Err(e)` is what `?` does, and the sync version was a
145-column line. The async mock shadow carried the same discard this PR
fixed, so it was corrected in place.
Tests now use existing helpers instead of hand-rolling: the two orders and
two historical tests build their client via
create_{,blocking_}test_client_with_ordered_proto_responses, and the
historical pair asserts with assert_tws_error_message rather than
to_string().contains(), which pins the error code too. Dropped their dead
time_zone setup (no bars decoded) and unified the sync/async shapes.
Three identical async stub bodies extracted to seeded_subscription, and
routed_items gained a direct test — the Shutdown arm had none.
Deferred as restructuring: fold_one_shot adoption at ~14 hand-rolled sites,
an expect_type helper for the seven identical dispatchers, and deleting the
order-builder mock shadows. All recorded in the plan.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
24
24
### Fixed
25
25
26
+
-`OrderBuilder::analyze()` (what-if orders, blocking and async) now returns the TWS rejection instead of `Error::UnexpectedEndOfStream`. A rejected what-if order arrives as a routed error, which the response read discarded — the blocking path via `if let Ok(..)` inside the loop, the async path by ending its `while let Some(Ok(..))` loop — so the caller lost the reason (e.g. code 201, `Order rejected - reason:...`) and got a generic end-of-stream error. Rejection is a routine outcome for a what-if order, so this was the likeliest path to hit it (#735).
27
+
28
+
-`matching_symbols()` on the blocking client now returns the TWS error instead of an empty list. A routed error arrives as `Some(Err(_))`, which the `if let Some(Ok(_))` read discarded, so a rejected pattern silently returned `Ok(vec![])` — indistinguishable from "no symbols matched". The async client already propagated it (#735).
29
+
26
30
-`TickTypes::MarketDataType` now reaches `Client::market_data` subscriptions. The message type was missing from the request-id routing allow-list, so TWS's market-data-type notifications (real-time / frozen / delayed / delayed-frozen, sent on subscribe and whenever the feed switches) were routed to a shared channel nobody subscribes to and dropped. The decoder has produced the variant since #516; nothing could ever yield it (#730).
27
31
28
32
- Decimal-typed wire fields no longer fall back to `0` when the value fails to parse; a malformed value now surfaces as `Error::Parse` and fails the request or subscription instead of being silently swallowed. Covers order quantities, execution shares, positions, contract-detail sizes, bar volume/WAP, tick and market-depth sizes (#716).
0 commit comments