Bug
After IBKR's Warning 1102: "Connectivity between IBKR and Trader Workstation has been restored - data maintained" fires (a brief
server-side disconnect/reconnect with session state preserved),
the server re-emits contractDetails messages for previously-
qualified contracts. Wrapper.contractDetails accesses
self._results[reqId] directly, which raises KeyError because
the entry was cleaned up by _endReq when the original
qualifyContracts future resolved.
The exception is caught by Decoder.interpret so it isn't fatal,
but produces noisy ERROR logs (one per re-emitted contract) that
look alarming and bury legitimate errors.
Observed behavior
Production deployment qualifying 4 contracts at startup
(IB.qualifyContractsAsync), then handling a Warning 1102
event a few hours later, produced the following sequence (4
KeyErrors immediately followed by the 1102 warning):
ERROR ib_async.Decoder Error handling fields: ['10', '17', 'ZN', 'FUT', '20260618 ...
Traceback (most recent call last):
File "/app/deps/ib_async/decoder.py", line 181, in interpret
handler(fields)
File "/app/deps/ib_async/decoder.py", line 344, in contractDetails
self.wrapper.contractDetails(int(reqId), cd)
File "/app/deps/ib_async/wrapper.py", line 874, in contractDetails
self._results[reqId].append(contractDetails)
KeyError: 17
ERROR ib_async.Decoder Error handling fields: ['10', '19', 'ES', 'FUT', '20260618 ...
... (KeyError: 19) ...
ERROR ib_async.Decoder Error handling fields: ['10', '13', 'ZB', 'FUT', '20260618 ...
... (KeyError: 13) ...
ERROR ib_async.Decoder Error handling fields: ['10', '15', 'SPY', 'STK', '' ...
... (KeyError: 15) ...
ERROR ib_async.wrapper Error 1102, reqId -1: Connectivity between IBKR and Trader
Workstation has been restored - data maintained. All data farms are connected:
usfuture; usfarm; ushmds; secdefnj.
Note: each KeyError's reqId (17/19/13/15) matches a reqId from
the prior IB.qualifyContractsAsync call ~6 hours earlier. The
futures had already been qualified, Future resolved, and
_results[reqId] popped by _endReq at that point.
Root cause
wrapper.py:873-874:
def contractDetails(self, reqId: int, contractDetails: ContractDetails):
self._results[reqId].append(contractDetails)
_results[reqId] is created by _startReq and popped by
_endReq(reqId) (which fires on contractDetailsEnd). The
normal request/response cycle is fully covered, but the
1102-driven server-side re-emission arrives after the cycle is
already complete.
Suggested fix
Defensive lookup — drop silently (or at DEBUG level) when the
request has already been ended:
def contractDetails(self, reqId: int, contractDetails: ContractDetails):
if reqId in self._results:
self._results[reqId].append(contractDetails)
bondContractDetails = contractDetails at line 876 then inherits
the fix.
I'd be happy to send a PR.
Why not reproduce minimally
Warning 1102 is server-side (IBKR-initiated brief disconnect with
session state preserved); we can't trigger it deterministically
from a client-side test. The simplest synthetic repro would be to
unit-test Wrapper.contractDetails(int, ContractDetails) after
calling _endReq(reqId) directly — that would exercise the same
KeyError path without needing a real Gateway session.
Related
Issue #128 (KeyError: 'completedOrders') is a similar-shape
late-arrival pattern in a different handler; the underlying
"handler doesn't tolerate post-_endReq arrivals" issue likely
applies more broadly than just contractDetails.
Environment
ib_async 2.1.0 (PyPI)
- Python 3.14.4 on Debian 12 (python:3.14-slim)
- IB Gateway v10.45 (paper, native install, socat-localhost-
injection on a dedicated VM)
- Connection clientId=2, qualify pattern:
IB.qualifyContractsAsync(Future(...)) × 4 at startup, then
long-running event loop with reqMktData subscriptions.
Bug
After IBKR's
Warning 1102: "Connectivity between IBKR and Trader Workstation has been restored - data maintained"fires (a briefserver-side disconnect/reconnect with session state preserved),
the server re-emits
contractDetailsmessages for previously-qualified contracts.
Wrapper.contractDetailsaccessesself._results[reqId]directly, which raisesKeyErrorbecausethe entry was cleaned up by
_endReqwhen the originalqualifyContractsfuture resolved.The exception is caught by
Decoder.interpretso it isn't fatal,but produces noisy ERROR logs (one per re-emitted contract) that
look alarming and bury legitimate errors.
Observed behavior
Production deployment qualifying 4 contracts at startup
(
IB.qualifyContractsAsync), then handling aWarning 1102event a few hours later, produced the following sequence (4
KeyErrors immediately followed by the 1102 warning):
Note: each KeyError's reqId (17/19/13/15) matches a reqId from
the prior
IB.qualifyContractsAsynccall ~6 hours earlier. Thefutures had already been qualified,
Futureresolved, and_results[reqId]popped by_endReqat that point.Root cause
wrapper.py:873-874:_results[reqId]is created by_startReqand popped by_endReq(reqId)(which fires oncontractDetailsEnd). Thenormal request/response cycle is fully covered, but the
1102-driven server-side re-emission arrives after the cycle is
already complete.
Suggested fix
Defensive lookup — drop silently (or at DEBUG level) when the
request has already been ended:
bondContractDetails = contractDetailsat line 876 then inheritsthe fix.
I'd be happy to send a PR.
Why not reproduce minimally
Warning 1102 is server-side (IBKR-initiated brief disconnect with
session state preserved); we can't trigger it deterministically
from a client-side test. The simplest synthetic repro would be to
unit-test
Wrapper.contractDetails(int, ContractDetails)aftercalling
_endReq(reqId)directly — that would exercise the sameKeyError path without needing a real Gateway session.
Related
Issue #128 (
KeyError: 'completedOrders') is a similar-shapelate-arrival pattern in a different handler; the underlying
"handler doesn't tolerate post-
_endReqarrivals" issue likelyapplies more broadly than just
contractDetails.Environment
ib_async2.1.0 (PyPI)injection on a dedicated VM)
IB.qualifyContractsAsync(Future(...))× 4 at startup, thenlong-running event loop with
reqMktDatasubscriptions.