{"record":{"id":"446981f0d1c36d65","repo":"fullstackhero/dotnet-starter-kit","slug":"uploaded-content-type-mismatch","errorCode":null,"errorMessage":"uploaded content-type mismatch","messagePattern":"uploaded content-type mismatch","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs","lineNumber":71,"sourceCode":"        // Allow declared+1% slack (S3 may differ slightly on multipart). Reject larger sizes.\n        var maxAllowed = asset.SizeBytes + Math.Max(1024L, asset.SizeBytes / 100);\n        if (head.SizeBytes > maxAllowed)\n        {\n            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n            db.FileAssets.Remove(asset);\n            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n            throw new CustomException(\n                $\"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.BadRequest);\n        }\n\n        if (!string.Equals(head.ContentType, asset.ContentType, StringComparison.OrdinalIgnoreCase))\n        {\n            await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n            db.FileAssets.Remove(asset);\n            await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n            throw new CustomException(\n                \"uploaded content-type mismatch\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.BadRequest);\n        }\n\n        var scanResult = await scanner.ScanAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);\n        asset.MarkAvailable(head.SizeBytes, scanResult);\n\n        // Debit quota with the actual bytes. Refunded on hard purge by PurgeDeletedFilesJob.\n        await quotas.RecordAsync(tenantId, QuotaResource.StorageBytes, head.SizeBytes, cancellationToken).ConfigureAwait(false);\n\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n\n        var correlationId = Activity.Current?.Id ?? Guid.NewGuid().ToString();\n\n        // Outbox rather than the bus: a crash between the SaveChanges above and delivery would\n        // otherwise leave the file marked available with no consumer ever told about it.\n        await outbox.AddAsync(new FileFinalizedIntegrationEvent(","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs#L53-L89","documentation":"Finalize compares the Content-Type reported by the storage HEAD request against the content type declared when the upload was requested (case-insensitive). On mismatch the handler deletes the uploaded blob, removes the FileAsset row, and throws CustomException with HTTP 400 BadRequest — the stored bytes cannot be trusted to match the registered media type.","triggerScenarios":"The client PUT the object with a Content-Type header different from the one bound into the presigned upload (e.g. browser defaulting to application/octet-stream, SDK omitting the header, or a proxy rewriting it).","commonSituations":"fetch/XHR uploads defaulting Content-Type when none is set; presign flow signed one content type but the uploader sent another (S3 then stores or rejects differently); MinIO storing application/octet-stream when the header was missing; upload code copied from a generic uploader that hardcodes a MIME type.","solutions":["Set the exact same Content-Type header on the upload PUT as was declared when creating the upload request","Re-create the upload request with the content type your client actually sends","Sniff the file's MIME type client-side before requesting the upload and pass it consistently through both steps","Check for proxies/gateways stripping or rewriting the Content-Type header"],"exampleFix":"// before\nawait fetch(uploadUrl, { method: 'PUT', body: file });\n// after\nawait fetch(uploadUrl, { method: 'PUT', body: file,\n  headers: { 'Content-Type': file.type } }); // must match declared contentType","handlingStrategy":"validation","validationCode":"if (file.type !== declaredContentType) {\n  throw new Error(`content-type ${file.type} does not match declared ${declaredContentType}`);\n}","typeGuard":null,"tryCatchPattern":"try { await api.finalizeUpload(assetId); }\ncatch (e) { if (e.status === 400 && e.message === 'uploaded content-type mismatch') { await requestNewUploadWith(file.type); } else throw e; }","preventionTips":["Always send the explicit Content-Type header matching the declared value on the upload PUT","Derive the MIME type once and reuse it for both the upload request and the PUT","Check proxies for header rewriting"],"tags":["content-type","upload","files","validation"],"backgroundTag":"content-type-mismatch","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}