File upload failed: find the cause before retrying
When an upload fails, open the browser’s Network panel and reproduce it once with a small, valid test file. Find the upload request, inspect its response, and match it to the server logs before changing limits or adding retries. This guide is for developers debugging an existing web uploader; you need access to its request handling code and logs to confirm the cause.
Capture one failed attempt
Open DevTools before uploading. Enable Preserve log if submission navigates away, then inspect the request’s URL, method, status, payload, response, and timing. Chrome’s Network panel guide shows where to find these details. Check every request in the upload flow: obtaining an upload URL, transferring bytes, and finalizing the upload can fail separately.
Record the time, file size and type, status, and any request ID supplied by the service. Use that ID to correlate proxy and application logs. Without an ID, use the timestamp, route, and test account. Keep cookies, authorization headers, signed URLs, and file contents out of shared reports.
Compare the failing file with a small file of the same supported format. If both fail, size alone does not explain the failure. If only the larger file fails, inspect size and timing evidence before choosing between a limit and an interrupted request.
Locate the failing layer
Treat the status as a lead. The response and matching logs identify which component rejected the request; a proxy and an application can return the same status.
| What you observe | What to check next |
|---|---|
| No upload request appears | Client validation, file selection, and JavaScript exceptions. Check for a failed earlier request in a multi-step flow. |
401 or 403 | Authentication, upload permission, expired credentials, and CSRF validation. Read the service’s error code. |
413 | Request-body limits at the proxy and file limits in the application. Find the component that logged the rejection. |
400, 415, or 422 | The expected field name, request encoding, and the application’s file-validation result. |
fetch() rejects without a readable response | Browser console details, CORS, connection errors, and cancellation. Check whether the server received the request. |
500, 502, 503, or 504 | Application errors, upstream availability, timeouts, and storage errors in server logs. |
A 2xx response, but no usable file | Response content, redirects, finalization, and any processing job’s status. |
These are investigation paths, not a universal mapping from status to cause. For example, 413
means the request content is too large, while 503 describes temporary service unavailability; it
does not identify a full disk. See the HTTP status definitions.
Keep HTTP failures visible in your code
fetch() resolves on HTTP error responses.
A rejected promise and a response with ok: false are different observations. Check response.ok
before parsing a body: an HTML error page from a proxy can otherwise become a misleading JSON parse
error.
For an existing multipart upload handler, this TypeScript helper preserves that distinction. It
accepts the selected file and your handler’s URL and sends one field named file. Use it only where
that matches your API; retain your application’s required authentication and CSRF handling when
adapting the request. The handler must already be running.
async function uploadForDiagnosis(
file: File | undefined,
url: string,
): Promise<Response | undefined> {
if (file === undefined) return
const body = new FormData()
body.append('file', file)
const response = await fetch(url, { method: 'POST', body })
if (!response.ok) {
throw new Error(`Upload returned HTTP ${response.status}`)
}
return response
}
Pass the selected File and upload URL from your existing submit handler, await the result, and
handle rejection there. A missing selection sends nothing; an empty file still makes a request.
Upload returned HTTP 413 means a readable HTTP response arrived. A browser network or CORS error
rejects the fetch() itself and passes through this helper. The helper does not schedule retries.
The returned response still needs your API’s normal success validation; a login redirect ending in
200, for example, is not proof of an accepted file.
Do not set Content-Type manually for this FormData request. The browser supplies the multipart
boundary; overriding the header can break parsing.
Keep duplicate submissions disabled while your existing upload handler is pending.
Change the component that rejected the upload
Trace a size limit through the request path
Suppose a small file succeeds and a larger one receives 413. If the proxy logs the rejection and
the application has no matching request, investigate the proxy first. Confirm that application
request logging is enabled before treating an absent log entry as evidence.
For NGINX, client_max_body_size
limits the request body and can be set at the http, server, or location level. Check the
configuration for the actual upload route. A multipart request contains fields and boundaries in
addition to the file, so a request-body limit needs room beyond the allowed file size. Raising an
application limit cannot remove an earlier proxy limit.
If the documented product limit should admit the file, adjust the rejecting layer within your storage and resource budget. Otherwise, keep the limit and explain it in the UI. Retest the original file, a file just within the allowed size, and one above it. The last should still be rejected.
Read the application’s rejection reason
For a malformed-request or unsupported-file response, compare the request with the handler’s
contract. Does it expect a multipart field named file, a different field name, or a raw body? Does
the request include the required metadata? Do not switch encodings or rename the file extension
until the response or log identifies a mismatch.
If the file’s actual format is unsupported, choose a supported source or convert it with an
appropriate tool. Changing .exe to .jpg does not convert the contents. Retest with a known-valid
file and keep a disallowed file as a negative check.
Separate CORS from a broken connection
A generic browser fetch error does not identify the cause. Inspect the console’s specific error alongside the Network panel and server logs.
- If an
OPTIONSpreflight fails, check the server’s allowed origin, method, and request headers. The browser may stop before sending the upload. - If the upload reaches the server but its response lacks the required CORS headers, JavaScript cannot read that response. The server may already have accepted the file. Check stored state before retrying.
- If the browser reports a connection or certificate error, investigate that connection. If your code aborted the request, find the cancellation or timeout that triggered it.
Configure CORS on the responding server, including its error responses. Credentialed cross-origin
requests need an explicit allowed origin and the appropriate credential settings; * is not a
substitute. MDN’s CORS guide explains
the preflight and response checks. Retest from the original browser origin with the original
authentication conditions. A successful cURL request does not establish that browser CORS works.
Do not use mode: 'no-cors' as a fix. It produces an opaque response whose status and body your code
cannot inspect. Retest both an accepted upload and an intentional rejection: both responses must
remain readable to the permitted origin.
Follow a server error to its failed operation
A 5xx response needs a matching server-side error. Identify whether the failure happened while
parsing the request, writing a temporary file, storing the final object, or running later
processing. For filesystem storage, inspect free space, quota, the actual destination path, and the
permissions of the service account. Check temporary storage too; a writable final directory does
not establish that the upload parser can spool data.
Use the underlying error to choose the fix. For example, Node’s
EACCES and ENOENT errors distinguish a
permission failure from a missing path. Repair the specific path or service permission, then repeat
the same upload and verify the stored bytes. Do not make the upload directory world-writable to
hide a permissions problem. If a gateway reports an unavailable upstream, confirm application
health before changing disk settings or timeouts.
Add resumability when interruptions are the problem
Once valid uploads work, repeated connection interruptions may justify resumable uploads. With tus, a client can query the stored offset and continue from there using a compatible server. Splitting a file into browser-side chunks alone does not provide that agreement about offsets, storage, and completion.
Resumability does not repair an invalid file, a denied request, CORS configuration, or exhausted storage. If you adopt it, test interruption and recovery and compare the completed file with the original. Use the tus implementations to choose a compatible client and server.
Verify the fix without removing safeguards
Run the original failing case again, then a valid small file and a deliberately disallowed file. Confirm the expected HTTP response, the application’s acceptance result, and the stored file or final processing result. For an unchanged upload, compare downloaded bytes or a checksum with the original. A completed progress bar only describes the transfer stage it measures.
Keep server-side size and content validation even if the UI checks first. Treat filenames and declared MIME types as untrusted, generate storage names, restrict access, and apply scanning or content disarm where the file type and risk require it. The OWASP file upload guidance describes these controls. A useful error tells the user what to change and gives support a request ID, while internal paths, stack traces, and credentials stay out of the response.
