Bound feedback requests and verify streamed upload limits

This commit is contained in:
webadderall
2026-09-23 18:18:36 +10:00
parent e6b626701b
commit ec5f48e5e8
3 changed files with 59 additions and 8 deletions
+26 -7
View File
@@ -72,15 +72,34 @@ it("cleans partial uploads and does not expose backend errors", async () => {
expect(await response.json()).toEqual({ code: "SUBMISSION_FAILED" });
expect(backend.remove).toHaveBeenCalledWith([expect.stringMatching(/^user-id\//)]);
});
it("caps streamed request bodies even without a content-length header", async () => {
it("caps a valid multipart stream before consuming the entire oversized upload", async () => {
const multipart = request([new File([new Uint8Array(16 * 1024 * 1024)], "large.bin")]);
const bytes = new Uint8Array(await multipart.arrayBuffer());
let produced = 0;
let cancelled = false;
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (produced === bytes.length) {
controller.close();
return;
}
const end = Math.min(produced + 64 * 1024, bytes.length);
controller.enqueue(bytes.slice(produced, end));
produced = end;
},
cancel() {
cancelled = true;
},
});
const req = new Request("https://example.test", {
method: "POST",
headers: {
Authorization: "Bearer session",
"Content-Type": "multipart/form-data; boundary=x",
},
body: new Uint8Array(12 * 1024 * 1024),
});
headers: multipart.headers,
body,
duplex: "half",
} as RequestInit);
expect(req.headers.has("content-length")).toBe(false);
expect((await handle(req)).status).toBe(400);
expect(backend.reserve).not.toHaveBeenCalled();
expect(cancelled).toBe(true);
expect(produced).toBeLessThan(bytes.length);
});
+32
View File
@@ -47,3 +47,35 @@ it("rejects unconfirmed submissions", async () => {
api.invoke.mockResolvedValue({ data: {}, error: null });
await expect(submitFeedback(input)).rejects.toThrow("not confirmed");
});
it("aborts a stalled request after one minute and permits a subsequent submission", async () => {
const { FunctionsClient } = await import("@supabase/functions-js");
vi.useFakeTimers();
let signal: AbortSignal | null | undefined;
try {
const functions = new FunctionsClient("https://example.test/functions/v1", {
customFetch: (_url, options) =>
new Promise((_resolve, reject) => {
signal = options?.signal;
signal?.addEventListener(
"abort",
() => reject(new DOMException("Aborted", "AbortError")),
{ once: true },
);
}),
});
api.invoke.mockImplementation(functions.invoke.bind(functions));
const pending = submitFeedback(input).catch((error) => error);
await vi.advanceTimersByTimeAsync(59_999);
expect(signal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(1);
expect(signal?.aborted).toBe(true);
expect(feedbackErrorMessage(await pending)).toBe(
"Could not send feedback. Please try again.",
);
api.invoke.mockResolvedValue({ data: { success: true }, error: null });
await expect(submitFeedback(input)).resolves.toBeUndefined();
} finally {
vi.useRealTimers();
}
});
+1 -1
View File
@@ -44,7 +44,7 @@ export async function submitFeedback(input: {
body.set("message", input.message.trim());
if (input.logs !== null) body.set("logs", input.logs);
for (const file of input.files) body.append("files", file);
const result = await client.functions.invoke("submit-feedback", { body });
const result = await client.functions.invoke("submit-feedback", { body, timeout: 60_000 });
if (result.error) {
const response = result.error.context;
if (response instanceof Response) {