{"record":{"id":"bf985f4efdf4b1d2","repo":"transloadit/uppy","slug":"s3mini-missing-etag-in-uploadpart-response-heade-bf985f","errorCode":null,"errorMessage":"[s3mini] Missing ETag in uploadPart response headers","messagePattern":"\\[s3mini\\] Missing ETag in uploadPart response headers","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/@uppy/aws-s3/src/s3-client/S3mini.ts","lineNumber":270,"sourceCode":"    signal,\n  }: IT.UploadPartParams) {\n    this._validateUploadPartParams(key, uploadId, partNumber)\n\n    const { xhr } = await this.request({\n      request: {\n        method: 'PUT',\n        key,\n        uploadId,\n        partNumber,\n      },\n      data,\n      onProgress,\n      signal,\n    })\n\n    const etag = U.sanitizeETag(xhr.getResponseHeader('etag'))\n    if (etag == null) {\n      throw new Error(\n        `${C.ERROR_PREFIX}Missing ETag in uploadPart response headers`,\n      )\n    }\n\n    return { etag }\n  }\n\n  /**\n   * Core XHR upload implementation using @uppy/core/utils fetcher.\n   *\n   * Features:\n   * - Automatic retry with exponential backoff (3 attempts)\n   * - Offline detection with automatic resume on reconnect\n   * - Stall detection via ProgressTimeout\n   */\n  private async request({\n    request,\n    data,","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/transloadit/uppy/blob/5d4dedd02a1ac0ae022c75c54aca76558f88e256/packages/@uppy/aws-s3/src/s3-client/S3mini.ts#L252-L288","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (bucket CORS missing expose headers)\n[{ \"AllowedOrigins\": [\"*\"], \"AllowedMethods\": [\"PUT\",\"POST\"], \"AllowedHeaders\": [\"*\"] }]\n// after\n[{ \"AllowedOrigins\": [\"*\"], \"AllowedMethods\": [\"PUT\",\"POST\"], \"AllowedHeaders\": [\"*\"], \"ExposeHeaders\": [\"ETag\"] }]","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"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 }","preventionTips":["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."],"tags":["aws-s3","etag","cors","multipart-upload","headers"],"backgroundTag":"cors-expose-headers-missing","analyzedSha":"5d4dedd02a1ac0ae022c75c54aca76558f88e256","analyzedAt":"2026-08-28T12:18:41.267Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}