mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(vydra): preserve errors for broken response bodies (#110478)
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
This commit is contained in:
co-authored by
Pick-cat
parent
3659642ac7
commit
1f160696dc
@@ -142,6 +142,34 @@ describe("downloadVydraAsset", () => {
|
||||
expect(result).toMatchObject({ name: "ProviderHttpError", status: 304, statusCode: 304 });
|
||||
});
|
||||
|
||||
it("preserves HTTP metadata when the error body stream fails", async () => {
|
||||
const result = await downloadVydraAsset({
|
||||
url: "https://cdn.vydra.example/generated/test.png",
|
||||
kind: "image",
|
||||
timeoutMs: 250,
|
||||
fetchFn: async () =>
|
||||
new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.error(new Error("broken error body"));
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 502,
|
||||
headers: { "x-request-id": "req-vydra-broken-body" },
|
||||
},
|
||||
),
|
||||
maxBytes: 1024 * 1024,
|
||||
}).catch((error: unknown) => error);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
name: "ProviderHttpError",
|
||||
status: 502,
|
||||
statusCode: 502,
|
||||
requestId: "req-vydra-broken-body",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not bound a dripping body when only chunk idle timeout is used", async () => {
|
||||
// Negative control: chunkTimeoutMs resets on every drip, so idle alone never fires.
|
||||
server = http.createServer((_req, res) => {
|
||||
|
||||
@@ -38,6 +38,8 @@ type VydraAuthStore = Parameters<typeof resolveApiKeyForProvider>[0]["store"];
|
||||
|
||||
type VydraMediaKind = "audio" | "image" | "video";
|
||||
|
||||
class VydraDownloadBodyTimeoutError extends Error {}
|
||||
|
||||
type VydraJobPayload = {
|
||||
id?: string;
|
||||
jobId?: string;
|
||||
@@ -246,9 +248,13 @@ function resolveVydraDownloadBodyTimeout(params: {
|
||||
chunkTimeoutMs: params.timeoutMs,
|
||||
timeoutMs: Math.max(1, params.deadlineMs - Date.now()),
|
||||
onIdleTimeout: ({ chunkTimeoutMs }: { chunkTimeoutMs: number }) =>
|
||||
new Error(`Vydra ${params.kind} download stalled after ${chunkTimeoutMs}ms`),
|
||||
new VydraDownloadBodyTimeoutError(
|
||||
`Vydra ${params.kind} download stalled after ${chunkTimeoutMs}ms`,
|
||||
),
|
||||
onTimeout: () =>
|
||||
new Error(`Vydra ${params.kind} download timed out after ${params.timeoutMs}ms`),
|
||||
new VydraDownloadBodyTimeoutError(
|
||||
`Vydra ${params.kind} download timed out after ${params.timeoutMs}ms`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,15 +273,27 @@ export async function downloadVydraAsset(params: {
|
||||
if (!response.ok) {
|
||||
// Shared assertOkOrThrowHttpError reads error detail without a wall-clock bound; a dripping
|
||||
// non-2xx body would hang before the success-path reader runs.
|
||||
const prefix = await readResponseTextPrefix(
|
||||
response,
|
||||
16 * 1024,
|
||||
resolveVydraDownloadBodyTimeout({ kind: params.kind, timeoutMs, deadlineMs }),
|
||||
);
|
||||
let errorBody: string | null = null;
|
||||
try {
|
||||
errorBody =
|
||||
(
|
||||
await readResponseTextPrefix(
|
||||
response,
|
||||
16 * 1024,
|
||||
resolveVydraDownloadBodyTimeout({ kind: params.kind, timeoutMs, deadlineMs }),
|
||||
)
|
||||
).text || null;
|
||||
} catch (error) {
|
||||
if (error instanceof VydraDownloadBodyTimeoutError) {
|
||||
throw error;
|
||||
}
|
||||
// The shared formatter historically ignored unreadable error bodies. Preserve the known
|
||||
// HTTP status and request id even when the provider closes a failed response mid-stream.
|
||||
}
|
||||
// Re-run the bounded body through the shared formatter so provider error metadata and
|
||||
// sensitive-text redaction remain identical to the previous assertOkOrThrowHttpError path.
|
||||
throw await createProviderHttpError(
|
||||
new Response(prefix.text || null, {
|
||||
new Response(errorBody, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
|
||||
Reference in New Issue
Block a user