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:
@@ -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}`);
|
||||
}
|
||||
},
|
||||
[]
|
||||
|
||||
Reference in New Issue
Block a user