Task #615: surface real upload errors in shared upload hook

The admin Edit Service image picker and the executive-meetings logo
uploader on the Mac deployment were both failing with a generic toast
("Failed to generate upload URL" / "فشل رفع الصورة") that swallowed
the actual server cause, making the iPhone-reported failure
impossible to diagnose from the UI alone.

Step 1 of the task: improve the diagnostic surface in
`lib/object-storage-web/src/use-upload.ts` so both call sites (which
already toast `err.message`) show what actually went wrong:

- `requestUploadUrl`: include the server's `error`/`message` field
  AND the HTTP status code in the thrown Error.
- `uploadToPresignedUrl`: distinguish network-level fetch failures
  (CORS / unreachable PUBLIC_BASE_URL host — the most likely Mac
  cause) from HTTP failures, and include the host + body snippet +
  status in each case.

No backend or call-site changes; error semantics at the hook
boundary are preserved (still rejects with Error, still invokes
onError, still returns null from uploadFile).

After this commit lands and is published to Gitea + redeployed on the
Mac, re-trying the upload will reveal the real cause in the toast,
which feeds Step 2 (root-cause fix) of Task #615. The follow-up
"publish #615 to Gitea + Mac redeploy" lives in Task #614 (already
proposed; blocked by concurrency).

Code review (architect): PASS. Optional follow-up noted —
`getUploadParameters` (Uppy path) still has the generic error
message; not used by the two failing uploaders, left for later.
This commit is contained in:
riyadhafraa
2026-05-19 11:48:16 +00:00
parent 97366f1d2c
commit a9b4fd86ff
+39 -9
View File
@@ -75,7 +75,14 @@ export function useUpload(options: UseUploadOptions = {}) {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Failed to get upload URL");
const serverMsg =
(typeof errorData?.error === "string" && errorData.error) ||
(typeof errorData?.message === "string" && errorData.message) ||
"";
const detail = serverMsg
? `${serverMsg} (HTTP ${response.status})`
: `HTTP ${response.status} ${response.statusText || ""}`.trim();
throw new Error(`Failed to get upload URL: ${detail}`);
}
return response.json();
@@ -85,16 +92,39 @@ export function useUpload(options: UseUploadOptions = {}) {
const uploadToPresignedUrl = useCallback(
async (file: File, uploadURL: string): Promise<void> => {
const response = await fetch(uploadURL, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type || "application/octet-stream",
},
});
let response: Response;
try {
response = await fetch(uploadURL, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type || "application/octet-stream",
},
});
} catch (err) {
// Network-level failure (CORS, DNS, unreachable host). This is
// the typical symptom when PUBLIC_BASE_URL on the server points
// at an origin the browser can't actually reach (e.g. localhost
// while the user is on Tailscale).
const msg = err instanceof Error ? err.message : String(err);
let host = "";
try {
host = new URL(uploadURL).host;
} catch {
/* ignore */
}
throw new Error(
`Failed to upload file to storage: network error reaching ${host || "the storage endpoint"}${msg}`,
);
}
if (!response.ok) {
throw new Error("Failed to upload file to storage");
const bodyText = await response.text().catch(() => "");
const snippet = bodyText.slice(0, 200).trim();
const detail = snippet
? `${snippet} (HTTP ${response.status})`
: `HTTP ${response.status} ${response.statusText || ""}`.trim();
throw new Error(`Failed to upload file to storage: ${detail}`);
}
},
[]