transloadit/uppy · critical · Error
[s3mini] Missing ETag in uploadPart response headers
Error message
[s3mini] Missing ETag in uploadPart response headers
What it means
uploadPart relies on the ETag response header to identify the uploaded part for the final CompleteMultipartUpload call. After the PUT succeeds, s3mini reads and sanitizes the `etag` header; if it's absent or unusable (null), it throws this Error because completing the multipart upload would be impossible without the part ETag.
Source
Thrown at packages/@uppy/aws-s3/src/s3-client/S3mini.ts:270
signal,
}: IT.UploadPartParams) {
this._validateUploadPartParams(key, uploadId, partNumber)
const { xhr } = await this.request({
request: {
method: 'PUT',
key,
uploadId,
partNumber,
},
data,
onProgress,
signal,
})
const etag = U.sanitizeETag(xhr.getResponseHeader('etag'))
if (etag == null) {
throw new Error(
`${C.ERROR_PREFIX}Missing ETag in uploadPart response headers`,
)
}
return { etag }
}
/**
* Core XHR upload implementation using @uppy/core/utils fetcher.
*
* Features:
* - Automatic retry with exponential backoff (3 attempts)
* - Offline detection with automatic resume on reconnect
* - Stall detection via ProgressTimeout
*/
private async request({
request,
data,View on GitHub (pinned to 5d4dedd02a)
Solutions
- Add `Access-Control-Expose-Headers: ETag` (and typically `ETag` in allowed headers) to the S3 bucket CORS configuration or proxy response headers.
- If a reverse proxy fronts S3/MinIO, ensure it forwards the ETag response header and doesn't strip it.
- Verify with browser devtools that the PUT part response actually includes an ETag header; if not, fix the server side.
Example fix
// before (bucket CORS missing expose headers)
[{ "AllowedOrigins": ["*"], "AllowedMethods": ["PUT","POST"], "AllowedHeaders": ["*"] }]
// after
[{ "AllowedOrigins": ["*"], "AllowedMethods": ["PUT","POST"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"] }] Defensive patterns
Strategy: fallback
Try / catch
try { return await s3.uploadPart({ key, uploadId, partNumber, body }) } catch (e) { if (/Missing ETag/.test(String(e?.message))) { console.error('ETag stripped by CORS/proxy — add ExposeHeaders: ETag'); throw e } throw e } Prevention
- Set ExposeHeaders: ["ETag"] in bucket CORS config from day one.
- If proxying S3, test that ETag survives the proxy (curl -I a PUT).
- Automate a browser-based multipart smoke test in CI to catch header stripping.
When it happens
Trigger: An S3-compatible backend or proxy that strips/renames the ETag header; missing `Access-Control-Expose-Headers: ETag` CORS header so the browser hides it from JS; gateway (nginx/CloudFront) dropping the header; a non-S3 endpoint returning 200 without ETag.
Common situations: Browser uploads where the S3/proxy CORS config doesn't expose ETag — by far the most common cause; self-hosted MinIO/garage behind a reverse proxy; custom middleware that filters response headers.
Related errors
- [s3mini] key must be a non-empty string
- [s3mini] uploadId must be a non-empty string
- [s3mini] partNumber must be a positive integer
- [s3mini] fileType must be a string
- [s3mini] Failed to create multipart upload: ${JSON.stringify
AI-assisted analysis of transloadit/uppy@5d4dedd02a (2026-08-28).
Data as JSON: /api/errors/bf985f4efdf4b1d2.
Report an issue: GitHub.