Vấn đề nằm ở IFormFile + model binding: framework buffer toàn bộ request vào bộ nhớ (hoặc file tạm) trước khi action chạy. Với file lớn thì phải chuyển sang streaming.
Ba việc cần làm:
1. Tắt model binding cho form bằng một IResourceFilter gỡ các form value provider, để không có gì đọc body trước bạn.
2. Nới giới hạn kích thước ở đúng chỗ — có nhiều tầng và thiếu một tầng là ra 413:
- [RequestSizeLimit(...)] hoặc [DisableRequestSizeLimit] trên action.
- FormOptions.MultipartBodyLengthLimit cho phần multipart.
- Giới hạn của reverse proxy (client_max_body_size ở Nginx, maxAllowedContentLength ở IIS) — tầng này hay bị bỏ sót vì lỗi phát sinh trước khi request tới app.
3. Đọc body bằng MultipartReader và copy thẳng sang đích:
[HttpPost, DisableFormValueModelBinding, RequestSizeLimit(2L * 1024 * 1024 * 1024)]
public async Task<IActionResult> Upload(CancellationToken ct)
{
var boundary = MediaTypeHeaderValue.Parse(Request.ContentType)
.Boundary.Value!.Trim('"');
var reader = new MultipartReader(boundary, Request.Body);
while (await reader.ReadNextSectionAsync(ct) is { } section)
{
await using var target = System.IO.File.Create(TempPathFor(section));
await section.Body.CopyToAsync(target, ct); // constant memory
}
return Ok();
}Với hệ thống thật, hướng tốt hơn là không cho file đi qua app server: backend cấp một pre-signed URL (S3, Azure Blob) để client upload thẳng lên storage, còn API chỉ nhận metadata và xác nhận. Bỏ được cả giới hạn kích thước lẫn băng thông của app.
Dù đi hướng nào cũng nhớ: validate content type và đặt lại tên file (không dùng tên client gửi), quét virus, và lưu ngoài web root.