fix: show server-canceled upload copy instead of connection error

A session the server moves to Canceled throws MediaUploadCanceledError, which
safeMediaUploadErrorMessage did not map, so it fell through to the generic
'Check your connection and try again' copy and blamed the network for a
server-side cancellation. Map it to 'The upload session was canceled.' ahead
of the user-abort branch, and cover the Canceled terminal state end to end
through the scan-polling path.
This commit is contained in:
Alexandre Brandizzi 2026-09-24 21:45:27 -03:00
parent 7d9d8ed47b
commit f4d22e2b3b
2 changed files with 41 additions and 0 deletions

View file

@ -88,6 +88,9 @@ export function safeMediaUploadErrorMessage(error: Error): string {
if (error instanceof MediaUploadScanTimeoutError) {
return "The upload is still processing. Check back shortly to see the result.";
}
if (error instanceof MediaUploadCanceledError) {
return "The upload session was canceled.";
}
if (error.name === "AbortError") {
return "Upload canceled.";
}

View file

@ -9,6 +9,10 @@ import {
uploadFileResumable,
} from "@/api/media-upload-engine";
import { computeSha256Hex, createMediaUploadEndpoints } from "@/api/media-upload-client";
import {
MediaUploadCanceledError,
safeMediaUploadErrorMessage,
} from "@/api/media-upload-contracts";
type CapturedRequest = { method: string; url: string; headers: Headers; body: unknown };
@ -500,6 +504,40 @@ describe("resumable media upload engine", () => {
});
});
describe("safeMediaUploadErrorMessage", () => {
it("maps a server-canceled session to its own copy, distinct from a user abort", async () => {
const file = fileOf(8);
handler = async (request) => {
if (request.method === "POST" && request.url === BASE) {
return jsonResponse(createdSession(8, 8));
}
if (request.method === "PUT" && /chunks\/0$/.test(request.url)) {
return jsonResponse({ index: 0, sha256: "x", sizeBytes: 8, receivedChunks: [0] });
}
if (request.method === "POST" && request.url.endsWith("/commit")) {
return jsonResponse({ ...createdSession(8, 8), state: "Scanning" }, 202);
}
if (request.method === "GET" && request.url === `${BASE}/u1`) {
return jsonResponse({ ...createdSession(8, 8), state: "Canceled" });
}
return jsonResponse({});
};
const error = await uploadFileResumable({
file,
endpoints: endpointsWith(),
surface: "WorkOrderTable",
pollDelayMs: 0,
}).catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(MediaUploadCanceledError);
expect(safeMediaUploadErrorMessage(error as Error)).toBe("The upload session was canceled.");
expect(safeMediaUploadErrorMessage(new DOMException("aborted", "AbortError"))).toBe(
"Upload canceled.",
);
});
});
describe("chunk math helpers", () => {
it("computes chunk counts and missing indices", () => {
expect(chunkCountFor(0, 5_242_880)).toBe(1);