fullstackhero/dotnet-starter-kit · error · CustomException
uploaded content-type mismatch
Error message
uploaded content-type mismatch
What it means
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.
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
Example fix
// before
await fetch(uploadUrl, { method: 'PUT', body: file });
// after
await fetch(uploadUrl, { method: 'PUT', body: file,
headers: { 'Content-Type': file.type } }); // must match declared contentType Defensive patterns
Strategy: validation
Validate before calling
if (file.type !== declaredContentType) {
throw new Error(`content-type ${file.type} does not match declared ${declaredContentType}`);
} Try / catch
try { await api.finalizeUpload(assetId); }
catch (e) { if (e.status === 400 && e.message === 'uploaded content-type mismatch') { await requestNewUploadWith(file.type); } else throw e; } Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Declared size must be positive.
- uploaded size ( ) exceeds declared ( )
- Cannot finalize file in status
- Actual size must be positive.
- Unknown visibility value
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/446981f0d1c36d65.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Features/v1/FinalizeUpload/FinalizeUploadCommandHandler.cs:71
// Allow declared+1% slack (S3 may differ slightly on multipart). Reject larger sizes.
var maxAllowed = asset.SizeBytes + Math.Max(1024L, asset.SizeBytes / 100);
if (head.SizeBytes > maxAllowed)
{
await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);
db.FileAssets.Remove(asset);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
throw new CustomException(
$"uploaded size ({head.SizeBytes}) exceeds declared ({asset.SizeBytes})",
(IEnumerable<string>?)null,
HttpStatusCode.BadRequest);
}
if (!string.Equals(head.ContentType, asset.ContentType, StringComparison.OrdinalIgnoreCase))
{
await storage.RemoveAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);
db.FileAssets.Remove(asset);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
throw new CustomException(
"uploaded content-type mismatch",
(IEnumerable<string>?)null,
HttpStatusCode.BadRequest);
}
var scanResult = await scanner.ScanAsync(asset.StorageKey, cancellationToken).ConfigureAwait(false);
asset.MarkAvailable(head.SizeBytes, scanResult);
// Debit quota with the actual bytes. Refunded on hard purge by PurgeDeletedFilesJob.
await quotas.RecordAsync(tenantId, QuotaResource.StorageBytes, head.SizeBytes, cancellationToken).ConfigureAwait(false);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
var correlationId = Activity.Current?.Id ?? Guid.NewGuid().ToString();
// Outbox rather than the bus: a crash between the SaveChanges above and delivery would
// otherwise leave the file marked available with no consumer ever told about it.
await outbox.AddAsync(new FileFinalizedIntegrationEvent(View on GitHub (pinned to 3f2959e683)