diff --git a/generators/typescript/sdk/changes/unreleased/fix-undici-timeout-handling.yml b/generators/typescript/sdk/changes/unreleased/fix-undici-timeout-handling.yml new file mode 100644 index 000000000000..a5a9713ceeef --- /dev/null +++ b/generators/typescript/sdk/changes/unreleased/fix-undici-timeout-handling.yml @@ -0,0 +1,17 @@ +- summary: | + Detect Node.js undici timeout errors (UND_ERR_HEADERS_TIMEOUT, UND_ERR_BODY_TIMEOUT) + and classify them as `reason: "timeout"` instead of `reason: "unknown"`. This fixes + an issue where Node's built-in fetch has a hard 300s headersTimeout that fires before + the SDK's timeoutInSeconds, causing generic "fetch failed" errors. + type: fix + +- summary: | + Include the full error cause chain in unknown error messages so underlying error codes + (e.g., undici error codes, ECONNRESET) surface to users instead of just "fetch failed". + type: fix + +- summary: | + Retry on network-level failures (e.g., connection resets, socket errors) in addition + to retryable HTTP status codes (408, 429, 5xx). Previously, thrown errors from the + fetch call bypassed retry logic entirely. + type: feat diff --git a/generators/typescript/utils/core-utilities/src/core/fetcher/Fetcher.ts b/generators/typescript/utils/core-utilities/src/core/fetcher/Fetcher.ts index f59409ce0d3c..b3ff95a7b710 100644 --- a/generators/typescript/utils/core-utilities/src/core/fetcher/Fetcher.ts +++ b/generators/typescript/utils/core-utilities/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/accept-header/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/accept-header/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/accept-header/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/accept-header/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/alias-extends/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/alias-extends/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/alias-extends/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/alias-extends/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/alias/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/alias/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/alias/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/alias/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/allof-inline/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/allof-inline/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/allof-inline/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/allof-inline/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/allof/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/allof/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/allof/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/allof/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/api-wide-base-path/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/api-wide-base-path/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/basic-auth-pw-omitted/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/basic-auth-pw-omitted/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/basic-auth-pw-omitted/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/basic-auth-pw-omitted/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/basic-auth/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/basic-auth/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/basic-auth/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/basic-auth/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/bytes-download/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/bytes-download/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/bytes-download/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/bytes-download/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/bytes-upload/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/bytes-upload/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/bytes-upload/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/bytes-upload/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/circular-references-advanced/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/circular-references-advanced/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/circular-references-extends/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/circular-references-extends/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/circular-references-extends/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/circular-references-extends/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/circular-references/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/circular-references/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/circular-references/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/circular-references/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/client-side-params/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/client-side-params/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/client-side-params/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/client-side-params/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/content-type/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/content-type/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/content-type/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/content-type/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/dollar-string-examples/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/dollar-string-examples/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/empty-clients/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/empty-clients/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/empty-clients/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/empty-clients/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/enum/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/enum/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/enum/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/enum/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/error-property/union-utils/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/error-property/union-utils/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/errors/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/errors/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/errors/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/errors/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/local-files/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/local-files/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/local-files/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/local-files/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/extends/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/extends/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/extends/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/extends/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/extra-properties/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/extra-properties/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/extra-properties/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/extra-properties/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload-openapi/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload-openapi/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/inline/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/inline/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/inline/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/inline/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/folders/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/folders/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/folders/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/folders/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/header-auth/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/header-auth/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/header-auth/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/header-auth/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/http-head/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/http-head/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/http-head/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/http-head/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/idempotency-headers/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/idempotency-headers/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/idempotency-headers/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/idempotency-headers/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/inferred-auth-implicit-reference/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/inferred-auth-implicit-reference/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/inferred-auth-implicit-reference/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/inferred-auth-implicit-reference/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/license/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/license/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/license/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/license/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/literal/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/literal/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/literal/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/literal/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/literals-unions/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/literals-unions/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/literals-unions/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/literals-unions/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/multi-line-docs/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/multi-line-docs/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/multi-line-docs/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/multi-line-docs/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/multi-url-environment-reference/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/multi-url-environment-reference/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/multi-url-environment-reference/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/multi-url-environment-reference/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/multi-url-environment/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/multi-url-environment/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/multi-url-environment/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/multi-url-environment/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/no-content-response/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/no-content-response/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/no-content-response/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/no-content-response/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/no-environment/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/no-environment/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/no-environment/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/no-environment/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/no-retries/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/no-retries/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/no-retries/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/no-retries/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/null-type/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/null-type/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/null-type/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/null-type/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/nullable-optional/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/nullable-optional/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/nullable-optional/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/nullable-optional/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/nullable-request-body/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/nullable-request-body/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/nullable-request-body/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/nullable-request-body/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/nullable/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/nullable/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/nullable/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/nullable/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-openapi/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-openapi/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-openapi/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-openapi/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/object/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/object/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/object/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/object/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/objects-with-imports/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/objects-with-imports/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/objects-with-imports/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/objects-with-imports/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/openapi-request-body-ref/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/openapi-request-body-ref/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/openapi-request-body-ref/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/openapi-request-body-ref/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/optional/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/optional/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/optional/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/optional/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/package-yml/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/package-yml/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/package-yml/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/package-yml/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/pagination-custom/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/pagination-custom/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/pagination-custom/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/pagination-custom/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/pagination-uri-path/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/pagination-uri-path/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/plain-text/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/plain-text/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/plain-text/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/plain-text/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/public-object/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/public-object/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/public-object/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/public-object/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-param-name-conflict/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-param-name-conflict/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-param-name-conflict/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-param-name-conflict/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/required-nullable/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/required-nullable/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/required-nullable/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/required-nullable/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/reserved-keywords/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/reserved-keywords/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/reserved-keywords/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/reserved-keywords/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/response-property/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/response-property/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/response-property/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/response-property/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/schemaless-request-body-examples/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/schemaless-request-body-examples/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/schemaless-request-body-examples/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/schemaless-request-body-examples/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/server-sent-events-openapi/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/server-sent-events-openapi/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/server-sent-events-openapi/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/server-sent-events-openapi/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/server-url-templating/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/server-url-templating/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/server-url-templating/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/server-url-templating/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/bundle/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/bundle/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/jsr/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/jsr/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/Fetcher.ts index f59409ce0d3c..b3ff95a7b710 100644 --- a/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/naming-shorthand/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/naming-shorthand/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/naming-shorthand/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/naming-shorthand/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/naming/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/naming/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/naming/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/naming/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/simple-fhir/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/simple-fhir/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/simple-fhir/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/simple-fhir/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/single-url-environment-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/single-url-environment-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/streaming-parameter/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/streaming-parameter/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/streaming-parameter/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/streaming-parameter/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/streaming/wrapper/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/streaming/wrapper/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/trace/exhaustive/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/trace/exhaustive/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/trace/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/trace/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/trace/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/trace/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/ts-express-casing/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-express-casing/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/ts-express-casing/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/ts-express-casing/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/union-query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/union-query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/union-query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/union-query-parameters/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/unions-with-local-date/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/unions-with-local-date/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/unions/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/unions/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/unions/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/unions/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/unions/serde-layer/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/unions/serde-layer/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/unions/serde-layer/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/unions/serde-layer/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/url-form-encoded/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/url-form-encoded/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/url-form-encoded/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/url-form-encoded/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/validation/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/validation/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/validation/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/validation/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/variables/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/variables/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/variables/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/variables/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/version-no-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/version-no-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/version-no-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/version-no-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/version/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/version/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/version/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/version/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/webhook-audience/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/webhook-audience/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/webhook-audience/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/webhook-audience/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket-multi-url/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket-multi-url/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket-multi-url/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket-multi-url/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket/no-serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket/no-serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/websocket/serde/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/websocket/serde/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/websocket/serde/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/websocket/serde/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; } diff --git a/seed/ts-sdk/x-fern-default/src/core/fetcher/Fetcher.ts b/seed/ts-sdk/x-fern-default/src/core/fetcher/Fetcher.ts index 943ec1bfe9c9..95c2411e995c 100644 --- a/seed/ts-sdk/x-fern-default/src/core/fetcher/Fetcher.ts +++ b/seed/ts-sdk/x-fern-default/src/core/fetcher/Fetcher.ts @@ -254,6 +254,33 @@ async function getHeaders(args: Fetcher.Args): Promise { return newHeaders; } +const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]); + +function isTimeoutError(error: Error): boolean { + if ("cause" in error && error.cause instanceof Error && "code" in error.cause) { + const { code } = error.cause as Error & { code: unknown }; + if (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code)) { + return true; + } + } + return false; +} + +function getErrorMessageWithCause(error: Error): string { + let message = error.message; + let current: unknown = "cause" in error ? error.cause : undefined; + while (current instanceof Error) { + const suffix = "code" in current && typeof (current as Error & { code: unknown }).code === "string" + ? ` [${(current as Error & { code: string }).code}]` + : ""; + if (current.message) { + message += ` -> ${current.message}${suffix}`; + } + current = "cause" in current ? current.cause : undefined; + } + return message; +} + export async function fetcherImpl(args: Fetcher.Args): Promise> { let url = args.url; if (args.queryString != null && args.queryString.length > 0) { @@ -370,12 +397,30 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise Promise, maxRetries: number = DEFAULT_MAX_RETRIES, ): Promise { - let response: Response = await requestFn(); + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let i = 0; i <= maxRetries; i++) { + try { + const response = await requestFn(); + lastResponse = response; + lastError = undefined; + + if (!([408, 429].includes(response.status) || response.status >= 500)) { + return response; + } + + if (i === maxRetries) { + return response; + } - for (let i = 0; i < maxRetries; ++i) { - if ([408, 429].includes(response.status) || response.status >= 500) { const delay = getRetryDelayFromHeaders(response, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + } catch (error) { + lastError = error; + lastResponse = undefined; + if (i === maxRetries || !isRetryableError(error)) { + throw error; + } + + const delay = addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** i, MAX_RETRY_DELAY)); await new Promise((resolve) => setTimeout(resolve, delay)); - response = await requestFn(); - } else { - break; } } - return response!; + + if (lastResponse != null) { + return lastResponse; + } + throw lastError; }