From 043e807186093e1bcd8cd741d250301112d54e4f Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Wed, 13 Mar 2024 16:23:14 +0900 Subject: [PATCH] =?UTF-8?q?abort=20signal=E3=82=92=E3=82=B5=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../k8s-client/client.ts | 41 +++-- .../test/fixtures/k8sApiClient.ts | 155 +++++++++--------- 2 files changed, 103 insertions(+), 93 deletions(-) diff --git a/examples/generate-all-k8s-client/k8s-client/client.ts b/examples/generate-all-k8s-client/k8s-client/client.ts index 63259b95..3e03cc73 100644 --- a/examples/generate-all-k8s-client/k8s-client/client.ts +++ b/examples/generate-all-k8s-client/k8s-client/client.ts @@ -99,8 +99,9 @@ type RetryOptions = { maxRetries?: number } -type HttpHeaderOptions = { +type HttpOptions = { headers?: Record | undefined + signal?: AbortSignal } export type WatchEventType = 'ADDED' | 'Modified' | 'Deleted' | 'BOOKMARK' @@ -110,12 +111,12 @@ export type WatchExtraOptions = { object: T }) => MaybePromise } -export type Options = RetryOptions & HttpHeaderOptions +export type Options = RetryOptions & HttpOptions export async function apiClient( arguments_: QueryArgumentsSpec, - extraOptions: Options & WatchExtraOptions -): Promise { + extraOptions: Options | (Options & WatchExtraOptions) = {} +): Promise { const maxRetries = extraOptions.maxRetries ?? 3 const defaultRetryCondition: RetryConditionFunction = ({ ...object }) => { @@ -124,15 +125,24 @@ export async function apiClient( return false } - if ( - typeof error === 'object' && - error !== null && - 'toString' in error && - typeof error.toString === 'function' && - error.toString().includes('TypeError: fetch failed') - ) { - return true + if (typeof error === 'object' && error !== null) { + const isAbortError = Boolean( + 'name' in error && error.name === 'AbortError' + ) + + if (isAbortError) { + return false + } + + if ( + 'toString' in error && + typeof error.toString === 'function' && + error.toString().includes('TypeError: fetch failed') + ) { + return true + } } + if (res && res.status >= 500) { return true } @@ -226,6 +236,7 @@ export async function apiClient( // https://github.com/nodejs/node/issues/48977 dispatcher: httpsOptions.agent, body, + signal: extraOptions.signal, }) ) @@ -245,7 +256,9 @@ export async function apiClient( let buffer = '' while (true) { const { value, done } = await reader.read() - if (done) break + if (done) { + return void 0 + } buffer += textDecoder.decode(value, { stream: true }) while (true) { @@ -257,8 +270,6 @@ export async function apiClient( await extraOptions.watchEventHandler(JSON.parse(line)) } } - - return JSON.parse(await response.text()) as Response } return (await response.json()) as Response } diff --git a/packages/kubernetes-typescript-client-codegen-openapi/test/fixtures/k8sApiClient.ts b/packages/kubernetes-typescript-client-codegen-openapi/test/fixtures/k8sApiClient.ts index 63259b95..66cd9d29 100644 --- a/packages/kubernetes-typescript-client-codegen-openapi/test/fixtures/k8sApiClient.ts +++ b/packages/kubernetes-typescript-client-codegen-openapi/test/fixtures/k8sApiClient.ts @@ -99,9 +99,10 @@ type RetryOptions = { maxRetries?: number } -type HttpHeaderOptions = { - headers?: Record | undefined -} +type HttpOptions = { + headers?: Record | undefined; + signal?: AbortSignal; +}; export type WatchEventType = 'ADDED' | 'Modified' | 'Deleted' | 'BOOKMARK' export type WatchExtraOptions = { @@ -110,52 +111,59 @@ export type WatchExtraOptions = { object: T }) => MaybePromise } -export type Options = RetryOptions & HttpHeaderOptions +export type Options = RetryOptions & HttpOptions; export async function apiClient( arguments_: QueryArgumentsSpec, - extraOptions: Options & WatchExtraOptions -): Promise { - const maxRetries = extraOptions.maxRetries ?? 3 + extraOptions: Options | (Options & WatchExtraOptions) = {} +): Promise { + const maxRetries = extraOptions.maxRetries ?? 3; const defaultRetryCondition: RetryConditionFunction = ({ ...object }) => { - const { res, attempt, error } = object + const { res, attempt, error } = object; if (attempt > maxRetries) { - return false + return false; } - if ( - typeof error === 'object' && - error !== null && - 'toString' in error && - typeof error.toString === 'function' && - error.toString().includes('TypeError: fetch failed') - ) { - return true + if (typeof error === 'object' && error !== null) { + const isAbortError = Boolean('name' in error && error.name === 'AbortError'); + + if (isAbortError) { + return false; + } + + if ( + 'toString' in error && + typeof error.toString === 'function' && + error.toString().includes('TypeError: fetch failed') + ) { + return true; + } } + if (res && res.status >= 500) { - return true + return true; } - return false - } + return false; + }; const options = { maxRetries, backoff: defaultBackoff, retryCondition: defaultRetryCondition, ...removeNullableProperties(extraOptions), - } + }; - let { path, method, params, body, contentType } = { ...arguments_ } + let { path, method, params, body, contentType } = { ...arguments_ }; let httpsOptions: https.RequestOptions = { path, headers: { ...options.headers, }, - } + }; if (method) { - httpsOptions.method = method + httpsOptions.method = method; } for (const interceptor of interceptors) { @@ -165,13 +173,10 @@ export async function apiClient( opts: httpsOptions, }, options - ) + ); } - if ( - !httpsOptions.agent && - (httpsOptions.ca || httpsOptions.cert || httpsOptions.key) - ) { + if (!httpsOptions.agent && (httpsOptions.ca || httpsOptions.cert || httpsOptions.key)) { const agent = new Agent({ connect: removeNullableProperties({ ca: httpsOptions.ca, @@ -179,42 +184,42 @@ export async function apiClient( key: httpsOptions.key, port: httpsOptions.port ? Number(httpsOptions.port) : undefined, }), - }) + }); // https://github.com/nodejs/node/issues/48977 - httpsOptions.agent = agent as any + httpsOptions.agent = agent as any; } if (!httpsOptions.protocol) { - httpsOptions.protocol = 'http:' + httpsOptions.protocol = 'http:'; } - const host = httpsOptions.host || httpsOptions.hostname - let baseUrl = `${httpsOptions.protocol}//${host}` - const searchParameters = toSearchParameters(params) + const host = httpsOptions.host || httpsOptions.hostname; + let baseUrl = `${httpsOptions.protocol}//${host}`; + const searchParameters = toSearchParameters(params); if (searchParameters.size > 0) { - baseUrl += (baseUrl.includes('?') ? '&' : '?') + toSearchParameters(params) + baseUrl += (baseUrl.includes('?') ? '&' : '?') + toSearchParameters(params); } - const url = new URL(baseUrl) + const url = new URL(baseUrl); if (httpsOptions.port) { - url.port = httpsOptions.port.toString() + url.port = httpsOptions.port.toString(); } if (httpsOptions.path) { - url.pathname = httpsOptions.path + url.pathname = httpsOptions.path; } - let isJson = false + let isJson = false; if (isPlainObject(body) || Array.isArray(body)) { - isJson = true - body = JSON.stringify(body) + isJson = true; + body = JSON.stringify(body); } const headers: Record = { ...(httpsOptions.headers as any), - } + }; if (contentType) { - headers['Content-Type'] = contentType + headers['Content-Type'] = contentType; } else if (!httpsOptions.headers?.['Content-Type'] && isJson) { - headers['Content-Type'] = 'application/json' + headers['Content-Type'] = 'application/json'; } - let retry = 0 + let retry = 0; while (true) { try { const response = await fetch( @@ -226,53 +231,47 @@ export async function apiClient( // https://github.com/nodejs/node/issues/48977 dispatcher: httpsOptions.agent, body, + signal: extraOptions.signal, }) - ) + ); - const isSuccess = response.status >= 200 && response.status < 300 - const contentType = response.headers.get('content-type') - const isJsonResponse = contentType?.includes('application/json') ?? false + const isSuccess = response.status >= 200 && response.status < 300; + const contentType = response.headers.get('content-type'); + const isJsonResponse = contentType?.includes('application/json') ?? false; if (isSuccess && isJsonResponse) { - if ( - 'watch' in params && - params.watch && - response.body && - 'watchEventHandler' in extraOptions - ) { - const reader = response.body.getReader() - const textDecoder = new TextDecoder() - let buffer = '' + if ('watch' in params && params.watch && response.body && 'watchEventHandler' in extraOptions) { + const reader = response.body.getReader(); + const textDecoder = new TextDecoder(); + let buffer = ''; while (true) { - const { value, done } = await reader.read() - if (done) break + const { value, done } = await reader.read(); + if (done) { + return void 0; + } - buffer += textDecoder.decode(value, { stream: true }) + buffer += textDecoder.decode(value, { stream: true }); while (true) { - const newlineIndex = buffer.indexOf('\n') - if (newlineIndex === -1) break - const line = buffer.slice(0, newlineIndex) - buffer = buffer.slice(newlineIndex + 1) + const newlineIndex = buffer.indexOf('\n'); + if (newlineIndex === -1) break; + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); - await extraOptions.watchEventHandler(JSON.parse(line)) + await extraOptions.watchEventHandler(JSON.parse(line)); } } - - return JSON.parse(await response.text()) as Response } - return (await response.json()) as Response + return (await response.json()) as Response; } // helpful message for debugging - const text = await response.text() + const text = await response.text(); if (response.status === 404 && text.includes('404 page not found')) { - console.info( - `Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}` - ) + console.info(`Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}`); } - throw new Error(text) + throw new Error(text); } catch (error: any) { - retry++ + retry++; if ( !(await options.retryCondition({ @@ -283,10 +282,10 @@ export async function apiClient( options: options, })) ) { - throw error + throw error; } - await options.backoff(retry, options.maxRetries) + await options.backoff(retry, options.maxRetries); } } }