Skip to content

Commit 7bd69a6

Browse files
authored
Merge pull request #28 from code-rabi/fix/flex-query-transient-error-codes
fix: retry flex query polling on all transient IB error codes
2 parents 3b527cc + ec5604b commit 7bd69a6

2 files changed

Lines changed: 134 additions & 10 deletions

File tree

src/flex-query-client.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ 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,21 @@ 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
188-
statementResponse.error.includes("in progress") ||
189-
statementResponse.error.includes("not ready")
190-
) {
191-
Logger.log(`[FLEX-QUERY] Statement not ready yet, retrying...`);
200+
const code = statementResponse.errorCode ?? "";
201+
const normalizedError = statementResponse.error.toLowerCase();
202+
const isTransient =
203+
FlexQueryClient.TRANSIENT_GET_STATEMENT_ERROR_CODES.has(code) ||
204+
normalizedError.includes("in progress") ||
205+
normalizedError.includes("not ready") ||
206+
normalizedError.includes("try again");
207+
208+
if (isTransient) {
209+
Logger.log(
210+
`[FLEX-QUERY] Statement not ready yet (code ${code || "?"}: ${statementResponse.error}), retrying...`
211+
);
192212
continue;
193213
}
194-
214+
195215
// It's a real error
196216
return statementResponse;
197217
}
@@ -226,4 +246,3 @@ export class FlexQueryClient {
226246
}
227247

228248

229-

test/flex-query-client.test.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,112 @@ 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+
343+
it('should retry when fallback transient message casing differs', async () => {
344+
const sendResponseXml = `<?xml version="1.0" encoding="UTF-8"?>
345+
<FlexStatementResponse timestamp="26 August, 2023 01:59 PM EDT">
346+
<Status>Success</Status>
347+
<ReferenceCode>${mockReferenceCode}</ReferenceCode>
348+
</FlexStatementResponse>`;
349+
350+
const transientXml = `<?xml version="1.0" encoding="UTF-8"?>
351+
<FlexStatementResponse timestamp="26 August, 2023 02:00 PM EDT">
352+
<Status>Fail</Status>
353+
<ErrorCode>9999</ErrorCode>
354+
<ErrorMessage>Statement could not be retrieved. Try Again Shortly.</ErrorMessage>
355+
</FlexStatementResponse>`;
356+
357+
const statementXml = `<?xml version="1.0" encoding="UTF-8"?>
358+
<FlexQueryResponse queryName="Test Query" type="AF">
359+
<FlexStatements count="1">
360+
<FlexStatement accountId="U12345" />
361+
</FlexStatements>
362+
</FlexQueryResponse>`;
363+
364+
const mockGet = vi.fn()
365+
.mockResolvedValueOnce({ data: sendResponseXml })
366+
.mockResolvedValueOnce({ data: transientXml })
367+
.mockResolvedValueOnce({ data: statementXml });
368+
369+
(axios.create as any).mockReturnValue({ get: mockGet });
370+
client = new FlexQueryClient({ token: mockToken });
371+
372+
const result = await client.executeQuery(mockQueryId, 3, 10);
373+
374+
expect(result).toEqual({ data: statementXml });
375+
expect(mockGet).toHaveBeenCalledTimes(3);
376+
});
377+
272378
it('should return error when sendRequest fails', async () => {
273379
const sendResponseXml = `<?xml version="1.0" encoding="UTF-8"?>
274380
<FlexStatementResponse timestamp="26 August, 2023 01:59 PM EDT">
@@ -375,4 +481,3 @@ describe('FlexQueryClient', () => {
375481
});
376482
});
377483
});
378-

0 commit comments

Comments
 (0)