From 41857cf9cd246ab930158a3707632ffce7d81d1f Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 5 Jan 2026 14:31:57 +0800 Subject: [PATCH] Fix retry handling for request bodies --- sdks/nodejs-client/src/http/client.ts | 79 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/sdks/nodejs-client/src/http/client.ts b/sdks/nodejs-client/src/http/client.ts index 7af1c86cdc..074e1cbef7 100644 --- a/sdks/nodejs-client/src/http/client.ts +++ b/sdks/nodejs-client/src/http/client.ts @@ -119,6 +119,28 @@ const parseRetryAfterSeconds = (headerValue?: string): number | undefined => { return undefined; }; +const shouldParseJson = (contentType: string | null, text: string): boolean => { + if (contentType) { + return contentType.toLowerCase().includes("application/json"); + } + return text.length > 0; +}; + +const parseResponseBody = (text: string, contentType: string | null): unknown => { + if (!shouldParseJson(contentType, text)) { + return text; + } + if (!text) { + return null; + } + try { + return JSON.parse(text); + } catch { + // Fallback to raw text if JSON parsing fails + return text; + } +}; + const isReadableStream = (value: unknown): value is Readable => { if (!value || typeof value !== "object") { return false; @@ -126,6 +148,23 @@ const isReadableStream = (value: unknown): value is Readable => { return typeof (value as { pipe?: unknown }).pipe === "function"; }; +const createBodyFactory = ( + method: RequestMethod, + data?: unknown +): { getBody: () => BodyInit | undefined; canRetry: boolean } => { + if (method === "GET" || data === undefined) { + return { getBody: () => undefined, canRetry: true }; + } + if (isReadableStream(data)) { + return { getBody: () => data as unknown as BodyInit, canRetry: false }; + } + if (isFormData(data)) { + return { getBody: () => data as BodyInit, canRetry: true }; + } + const jsonBody = JSON.stringify(data); + return { getBody: () => jsonBody, canRetry: true }; +}; + const isUploadLikeRequest = (url: string): boolean => { if (!url) { return false; @@ -320,14 +359,7 @@ export class HttpClient { console.info(`dify-client-node request ${method} ${url}`); } - let body: BodyInit | undefined; - if (method !== "GET" && data !== undefined) { - if (isFormData(data) || isReadableStream(data)) { - body = data as BodyInit; - } else { - body = JSON.stringify(data); - } - } + const { getBody, canRetry: canRetryBody } = createBodyFactory(method, data); let attempt = 0; // `attempt` is a zero-based retry counter @@ -342,27 +374,17 @@ export class HttpClient { const response = await fetch(url, { method, headers: requestHeaders, - body, + body: getBody(), signal: abortController.signal, }); clearTimeout(timeoutId); if (!response.ok) { - const contentType = response.headers.get("content-type") || ""; - let responseBody: unknown; + const contentType = response.headers.get("content-type"); // Read body as text first to avoid "Body has already been read" error const text = await response.text(); - if (contentType.includes("application/json")) { - try { - responseBody = text ? JSON.parse(text) : null; - } catch { - // Fallback to raw text if JSON parsing fails - responseBody = text; - } - } else { - responseBody = text; - } + const responseBody = parseResponseBody(text, contentType); throw mapFetchError(new Error(`HTTP ${response.status}`), url, response, responseBody); } @@ -392,19 +414,10 @@ export class HttpClient { responseData = await response.arrayBuffer(); } else { // json or default - const contentType = response.headers.get("content-type") || ""; + const contentType = response.headers.get("content-type"); // Read body as text first to handle malformed JSON gracefully const text = await response.text(); - if (contentType.includes("application/json")) { - try { - responseData = text ? JSON.parse(text) : null; - } catch { - // Fallback to raw text if JSON parsing fails - responseData = text; - } - } else { - responseData = text; - } + responseData = parseResponseBody(text, contentType); } return { @@ -422,7 +435,7 @@ export class HttpClient { mapped = mapFetchError(error, url); } - if (!shouldRetry(mapped, attempt, maxRetries)) { + if (!canRetryBody || !shouldRetry(mapped, attempt, maxRetries)) { throw mapped; } const retryAfterSeconds =