Skip to content

Commit 34df256

Browse files
NitayRabiclaude
andcommitted
fix: retry flex query polling on all transient IB error codes
The polling loop in executeQuery only treated error code 1019 as "not ready yet", so other transient codes IB returns while a statement is being prepared (notably 1001 "Statement could not be generated at this time") were surfaced immediately as terminal errors. Introduce a TRANSIENT_GET_STATEMENT_ERROR_CODES set covering 1001, 1004-1009, 1018, 1019, and 1021, plus a "try again" substring fallback, and add regression tests for the transient codes and a guard for terminal errors (e.g. 1015 invalid token). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 05c78ae commit 34df256

2 files changed

Lines changed: 97 additions & 7 deletions

File tree

src/flex-query-client.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ export interface FlexStatementResponse {
2424
* API Documentation: https://www.interactivebrokers.com/en/software/am/am/reports/flex_web_service_version_3.htm
2525
*/
2626
export class FlexQueryClient {
27+
// IB Flex Web Service error codes that mean "try again shortly" — the statement
28+
// is being prepared. All other Fail codes are terminal (bad token, invalid query, etc.).
29+
private static readonly TRANSIENT_GET_STATEMENT_ERROR_CODES = new Set([
30+
"1001", // Statement could not be generated at this time
31+
"1004", // Statement is incomplete at this time
32+
"1005", // Settlement data is not ready
33+
"1006", // FIFO P/L data is not ready
34+
"1007", // MTM P/L data is not ready
35+
"1008", // MTM and FIFO P/L data is not ready
36+
"1009", // Server is under heavy load
37+
"1018", // Too many requests from this token
38+
"1019", // Statement generation in progress
39+
"1021", // Statement could not be retrieved at this time
40+
]);
41+
2742
private client: AxiosInstance;
2843
private token: string;
2944
// Fixed: gdcdyn → ndcdyn, /Universal/servlet → /AccountManagement/FlexWebService
@@ -182,16 +197,20 @@ export class FlexQueryClient {
182197
const statementResponse = await this.getStatement(sendResponse.referenceCode);
183198

184199
if (statementResponse.error) {
185-
// Check if it's a "not ready yet" error
186-
if (
187-
statementResponse.errorCode === "1019" || // Statement generation in progress
200+
const code = statementResponse.errorCode ?? "";
201+
const isTransient =
202+
FlexQueryClient.TRANSIENT_GET_STATEMENT_ERROR_CODES.has(code) ||
188203
statementResponse.error.includes("in progress") ||
189-
statementResponse.error.includes("not ready")
190-
) {
191-
Logger.log(`[FLEX-QUERY] Statement not ready yet, retrying...`);
204+
statementResponse.error.includes("not ready") ||
205+
statementResponse.error.includes("try again");
206+
207+
if (isTransient) {
208+
Logger.log(
209+
`[FLEX-QUERY] Statement not ready yet (code ${code || "?"}: ${statementResponse.error}), retrying...`
210+
);
192211
continue;
193212
}
194-
213+
195214
// It's a real error
196215
return statementResponse;
197216
}

test/flex-query-client.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,77 @@ describe('FlexQueryClient', () => {
269269
expect(mockGet).toHaveBeenCalledTimes(3);
270270
});
271271

272+
it.each([
273+
['1001', 'Statement could not be generated at this time. Please try again shortly.'],
274+
['1009', 'The server is under heavy load and statement could not be generated at this time. Please try again shortly.'],
275+
['1019', 'Statement generation in progress. Please try again shortly.'],
276+
['1021', 'Statement could not be retrieved at this time. Please try again shortly.'],
277+
])('should retry on transient GetStatement error code %s', async (code, message) => {
278+
const sendResponseXml = `<?xml version="1.0" encoding="UTF-8"?>
279+
<FlexStatementResponse timestamp="26 August, 2023 01:59 PM EDT">
280+
<Status>Success</Status>
281+
<ReferenceCode>${mockReferenceCode}</ReferenceCode>
282+
</FlexStatementResponse>`;
283+
284+
const transientXml = `<?xml version="1.0" encoding="UTF-8"?>
285+
<FlexStatementResponse timestamp="26 August, 2023 02:00 PM EDT">
286+
<Status>Fail</Status>
287+
<ErrorCode>${code}</ErrorCode>
288+
<ErrorMessage>${message}</ErrorMessage>
289+
</FlexStatementResponse>`;
290+
291+
const statementXml = `<?xml version="1.0" encoding="UTF-8"?>
292+
<FlexQueryResponse queryName="Test Query" type="AF">
293+
<FlexStatements count="1">
294+
<FlexStatement accountId="U12345" />
295+
</FlexStatements>
296+
</FlexQueryResponse>`;
297+
298+
const mockGet = vi.fn()
299+
.mockResolvedValueOnce({ data: sendResponseXml })
300+
.mockResolvedValueOnce({ data: transientXml })
301+
.mockResolvedValueOnce({ data: statementXml });
302+
303+
(axios.create as any).mockReturnValue({ get: mockGet });
304+
client = new FlexQueryClient({ token: mockToken });
305+
306+
const result = await client.executeQuery(mockQueryId, 3, 10);
307+
308+
expect(result).toEqual({ data: statementXml });
309+
expect(mockGet).toHaveBeenCalledTimes(3);
310+
});
311+
312+
it('should not retry on terminal GetStatement errors (e.g. 1015 invalid token)', async () => {
313+
const sendResponseXml = `<?xml version="1.0" encoding="UTF-8"?>
314+
<FlexStatementResponse timestamp="26 August, 2023 01:59 PM EDT">
315+
<Status>Success</Status>
316+
<ReferenceCode>${mockReferenceCode}</ReferenceCode>
317+
</FlexStatementResponse>`;
318+
319+
const terminalXml = `<?xml version="1.0" encoding="UTF-8"?>
320+
<FlexStatementResponse timestamp="26 August, 2023 02:00 PM EDT">
321+
<Status>Fail</Status>
322+
<ErrorCode>1015</ErrorCode>
323+
<ErrorMessage>Token is invalid.</ErrorMessage>
324+
</FlexStatementResponse>`;
325+
326+
const mockGet = vi.fn()
327+
.mockResolvedValueOnce({ data: sendResponseXml })
328+
.mockResolvedValueOnce({ data: terminalXml });
329+
330+
(axios.create as any).mockReturnValue({ get: mockGet });
331+
client = new FlexQueryClient({ token: mockToken });
332+
333+
const result = await client.executeQuery(mockQueryId, 5, 10);
334+
335+
expect(result).toEqual({
336+
error: 'Token is invalid.',
337+
errorCode: '1015',
338+
});
339+
// Should bail after the first GetStatement attempt — no further retries
340+
expect(mockGet).toHaveBeenCalledTimes(2);
341+
});
342+
272343
it('should return error when sendRequest fails', async () => {
273344
const sendResponseXml = `<?xml version="1.0" encoding="UTF-8"?>
274345
<FlexStatementResponse timestamp="26 August, 2023 01:59 PM EDT">

0 commit comments

Comments
 (0)