From ccdcd45858bcdf4c3e7ffacbb916d337f0660eb2 Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Wed, 11 Feb 2026 15:21:27 +0000 Subject: [PATCH] retry on ECONNABORTED --- .../serviceSearchClient/jest.debug.config.ts | 2 +- .../src/live-serviceSearch-client.ts | 23 +++++++++++++++---- .../tests/live-serviceSearch-client.test.ts | 16 +++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/serviceSearchClient/jest.debug.config.ts b/packages/serviceSearchClient/jest.debug.config.ts index a30627383..fa3a3ff87 100644 --- a/packages/serviceSearchClient/jest.debug.config.ts +++ b/packages/serviceSearchClient/jest.debug.config.ts @@ -1,4 +1,4 @@ -import config from "./jest.config" +import config from "./jest.config.ts" import type {JestConfigWithTsJest} from "ts-jest" const debugConfig: JestConfigWithTsJest = { diff --git a/packages/serviceSearchClient/src/live-serviceSearch-client.ts b/packages/serviceSearchClient/src/live-serviceSearch-client.ts index 3c138d6ae..06b5234e9 100644 --- a/packages/serviceSearchClient/src/live-serviceSearch-client.ts +++ b/packages/serviceSearchClient/src/live-serviceSearch-client.ts @@ -1,7 +1,7 @@ import {Logger} from "@aws-lambda-powertools/logger" import {getSecret} from "@aws-lambda-powertools/parameters/secrets" import axios, {AxiosError, AxiosInstance} from "axios" -import axiosRetry from "axios-retry" +import axiosRetry, {isNetworkOrIdempotentRequestError} from "axios-retry" import {handleUrl} from "./handleUrl" import {ServiceSearchClient} from "./serviceSearch-client" @@ -82,7 +82,11 @@ export class LiveServiceSearchClient implements ServiceSearchClient { v3: process.env.ServiceSearch3ApiKey !== undefined }) this.axiosInstance = axios.create() - axiosRetry(this.axiosInstance, {retries: 3}) + axiosRetry(this.axiosInstance, { + retries: 3, + onRetry: this.onAxiosRetry, + retryCondition: this.retryCondition + }) this.axiosInstance.interceptors.request.use((config) => { config.headers["request-startTime"] = Date.now() @@ -102,7 +106,7 @@ export class LiveServiceSearchClient implements ServiceSearchClient { // reject with a proper Error object let err: Error if (error instanceof Error) { - logger.error("Error in serviceSearch request", {error}) + this.logger.error("Error in serviceSearch request", {error}) err = error } else if ((error as AxiosError).message) { // Only report the interesting subset of the error object. @@ -121,10 +125,10 @@ export class LiveServiceSearchClient implements ServiceSearchClient { } } - logger.error("Axios error in serviceSearch request", {axiosErrorDetails}) + this.logger.error("Axios error in serviceSearch request", {axiosErrorDetails}) err = new Error("Axios error in serviceSearch request") } else { - logger.error("Unknown error in serviceSearch request", {error}) + this.logger.error("Unknown error in serviceSearch request", {error}) err = new Error("Unknown error in serviceSearch request") } return Promise.reject(err) @@ -268,4 +272,13 @@ export class LiveServiceSearchClient implements ServiceSearchClient { } }) } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onAxiosRetry = (retryCount: number, error: any) => { + this.logger.warn(error) + this.logger.warn(`Call to serviceSearch failed - retrying. Retry count ${retryCount}`, {retryCount: retryCount}) + } + retryCondition(error: AxiosError): boolean { + return isNetworkOrIdempotentRequestError(error) || error.code === "ECONNABORTED" + } } diff --git a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts index 980dcf881..3c4e48335 100644 --- a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts +++ b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts @@ -284,8 +284,15 @@ describe("live serviceSearch client", () => { .onGet(serviceSearchUrl).replyOnce(500) .onGet(serviceSearchUrl).replyOnce(500) .onGet(serviceSearchUrl).reply(200, validUrlData) + const warnSpy = jest.spyOn(Logger.prototype, "warn") + client = new LiveServiceSearchClient(logger) + const result = await client.searchService("z", dummyCorrelationId) expect(result).toEqual(new URL(validUrlData.value[0].URL)) + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Call to serviceSearch failed - retrying. Retry count"), + expect.objectContaining({retryCount: 1}) + ) }) test("fails after exceeding retries", async () => { @@ -293,6 +300,15 @@ describe("live serviceSearch client", () => { await expect(client.searchService("z", dummyCorrelationId)).rejects.toThrow("Request failed with status code 500") }) + test("retries on timeout (ECONNABORTED) error", async () => { + mock.onGet(serviceSearchUrl).timeoutOnce() + .onGet(serviceSearchUrl).timeoutOnce() + .onGet(serviceSearchUrl).timeoutOnce() + .onGet(serviceSearchUrl).reply(200, validUrlData) + const result = await client.searchService("z", dummyCorrelationId) + expect(result).toEqual(new URL(validUrlData.value[0].URL)) + }) + test("logs duration in info on success and failure", async () => { const infoSpy = jest.spyOn(Logger.prototype, "info") mock.onGet(serviceSearchUrl).networkError()