fix(upload): attach CORS guidance to direct upload failures

A new user's top first-upload failure is a missing R2 CORS policy (browser
PUT/POST blocked) -- and object-scoped R2 tokens often cannot auto-set CORS.
The connection test already surfaces buildCorsGuidance on the opaque
"Failed to fetch", but a user who skips the test and uploads directly got a
bare "Upload failed: Failed to fetch". The upload error handler now detects the
browser CORS/network rejection (isLikelyCorsError -- Chrome "Failed to fetch",
Firefox "NetworkError", Safari "Load failed") and appends the same actionable
guidance. Updated the R2 follow-up TODO (credential audit done; CORS reviewed).

Test: tests/services/r2-cors-error-classifier.test.mjs. Full suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-17 05:47:12 -04:00
1 parent 200e5cec80
commit 7f5dcd7e50
3 files changed
+79 -15

No files matched your search

+29 -4
View File
@@ -103,6 +103,25 @@ function buildCorsGuidance({ accountId } = {}) {
].join(" ");
}
/**
* A browser CORS preflight/rejection surfaces as an opaque network error
* ("Failed to fetch" / "NetworkError" / Safari's "Load failed") with no HTTP
* status — indistinguishable from a real network drop. We treat these as
* likely-CORS so the upload path can attach actionable guidance instead of a
* bare "Failed to fetch".
*/
export function isLikelyCorsError(err) {
const message = (
(err && (err.message || err.toString && err.toString())) ||
""
).toLowerCase();
return (
message.includes("failed to fetch") ||
message.includes("networkerror") ||
message.includes("load failed")
);
}
function createDefaultSettings() {
return {
accountId: "",
@@ -1251,10 +1270,16 @@ export class R2Service {
return publishOutcome;
} catch (err) {
userLogger.error("Cloudflare upload failed:", err);
this.setCloudflareUploadStatus(
err?.message ? `Upload failed: ${err.message}` : "Upload failed.",
"error"
);
let message = err?.message
? `Upload failed: ${err.message}`
: "Upload failed.";
// A CORS rejection during the browser PUT/POST looks like an opaque
// "Failed to fetch" — attach actionable guidance so users who skipped the
// connection test still know what to fix (the #1 first-upload failure).
if (isLikelyCorsError(err)) {
message += ` ${buildCorsGuidance({ accountId })}`;
}
this.setCloudflareUploadStatus(message, "error");
return false;
} finally {
this.setCloudflareUploading(false);