diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 53f86c85ae31..4de093de4614 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -14,6 +14,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { responseWithRelease } from "../response-with-release.js"; import type { MSTeamsAttachmentLike } from "./types.js"; type InlineImageCandidate = @@ -576,52 +577,6 @@ export async function resolveAndValidateIP( /** Maximum number of redirects to follow in safeFetch. */ const MAX_SAFE_REDIRECTS = 5; -const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); - -function responseWithRelease(response: Response, release: () => Promise): Response { - let released = false; - const releaseOnce = async () => { - if (released) { - return; - } - released = true; - await release(); - }; - - if (!response.body || NULL_BODY_STATUSES.has(response.status)) { - void releaseOnce(); - return response; - } - - const reader = response.body.getReader(); - const body = new ReadableStream({ - async pull(controller) { - try { - const next = await reader.read(); - if (next.done) { - controller.close(); - await releaseOnce(); - return; - } - controller.enqueue(next.value); - } catch (err) { - await releaseOnce(); - throw err; - } - }, - async cancel(reason) { - void reader.cancel(reason).catch(() => {}); - await releaseOnce(); - }, - }); - - return new Response(body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); -} - /** * Fetch a URL with redirect: "manual", validating each redirect target * against the hostname allowlist and optional DNS-resolved IP (anti-SSRF). diff --git a/extensions/msteams/src/graph.ts b/extensions/msteams/src/graph.ts index 39210ecc6cc7..dce49865cd7c 100644 --- a/extensions/msteams/src/graph.ts +++ b/extensions/msteams/src/graph.ts @@ -4,13 +4,13 @@ import { fetchWithSsrFGuard, type MSTeamsConfig } from "../runtime-api.js"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { resolveMSTeamsSdkCloudOptions } from "./cloud.js"; import { createMSTeamsHttpError } from "./http-error.js"; +import { responseWithRelease } from "./response-with-release.js"; import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; import { resolveDelegatedAccessToken, resolveMSTeamsCredentials } from "./token.js"; import { buildUserAgent } from "./user-agent.js"; const GRAPH_BETA = "https://graph.microsoft.com/beta"; -const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); export type GraphUser = { id?: string; @@ -31,50 +31,6 @@ type GraphChannel = { export type GraphResponse = { value?: T[] }; -function responseWithRelease(response: Response, release: () => Promise): Response { - let released = false; - const releaseOnce = async () => { - if (released) { - return; - } - released = true; - await release(); - }; - - if (!response.body || NULL_BODY_STATUSES.has(response.status)) { - void releaseOnce(); - return response; - } - - const reader = response.body.getReader(); - const body = new ReadableStream({ - async pull(controller) { - try { - const next = await reader.read(); - if (next.done) { - controller.close(); - await releaseOnce(); - return; - } - controller.enqueue(next.value); - } catch (error) { - await releaseOnce(); - throw error; - } - }, - async cancel(reason) { - void reader.cancel(reason).catch(() => undefined); - await releaseOnce(); - }, - }); - - return new Response(body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); -} - export function normalizeQuery(value?: string | null): string { return value?.trim() ?? ""; } diff --git a/extensions/msteams/src/response-with-release.ts b/extensions/msteams/src/response-with-release.ts new file mode 100644 index 000000000000..054b45b24fc2 --- /dev/null +++ b/extensions/msteams/src/response-with-release.ts @@ -0,0 +1,45 @@ +const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); + +export function responseWithRelease(response: Response, release: () => Promise): Response { + let released = false; + const releaseOnce = async () => { + if (released) { + return; + } + released = true; + await release(); + }; + + if (!response.body || NULL_BODY_STATUSES.has(response.status)) { + void releaseOnce(); + return response; + } + + const reader = response.body.getReader(); + const body = new ReadableStream({ + async pull(controller) { + try { + const next = await reader.read(); + if (next.done) { + controller.close(); + await releaseOnce(); + return; + } + controller.enqueue(next.value); + } catch (error) { + await releaseOnce(); + throw error; + } + }, + async cancel(reason) { + void reader.cancel(reason).catch(() => undefined); + await releaseOnce(); + }, + }); + + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +}