Khi một tệp PDF lớn bị từ chối: giới hạn 100 MiB cho thân yêu cầu và lỗi chỉ sai hướng
Một thân yêu cầu được phép 100 MiB, tức 104,857,600 byte, tính cả phần đóng khung multipart; vượt giới hạn, endpoint thao tác trả 400 với mã bad_request và chi tiết là một trường multipart đọc thất bại chứ không phải 413, nên vấn đề kích thước trông như vấn đề tham số, còn với Idempotency-Key thì bộ đệm phản hồi có thêm giới hạn 100 MiB thứ hai, vượt qua sẽ nhận 500 và không có gì được cache.

Một tệp PDF lớn không tải lên được thường không phải vì tệp hỏng. Thân yêu cầu đã chạm giới hạn mỗi yêu cầu: 100 MiB, tức 104,857,600 byte. Con số này được đo trên toàn bộ thân yêu cầu, kể cả ranh giới multipart và header của từng trường; đúng chừng đó thì qua, thêm một byte là bị từ chối.
Lỗi trả về lại chỉ về hướng khác. Các endpoint thao tác trả 400 với code là bad_request và phần detail nói rằng đọc một trường multipart thất bại, không nhắc gì đến kích thước. Một client rẽ nhánh theo code sẽ xếp việc này vào lỗi tham số rồi đi kiểm tra tên trường, trong khi thứ cần sửa là kích thước tệp.
Cùng con số 100 MiB đó cũng chi phối chiều ngược lại. Một yêu cầu mang Idempotency-Key đọc thân phản hồi vào bộ nhớ trước khi cache nó, cũng theo đúng con số ấy; vượt qua thì người gọi nhận 500 trong khi thao tác đã chạy xong. Một con số, hai kiểu hỏng trái ngược.
100 MiB = 104,857,600 byte (đúng chừng này thì qua)
|
+------------------+------------------+
| |
vào (thân yêu cầu) ra (thân phản hồi)
toàn bộ thân, kể cả ranh giới chỉ khi có Idempotency-Key,
multipart và header của trường; cache trượt và thượng nguồn
một thao tác và pipeline dùng chung trả 2xx
vượt: 400 + bad_request vượt: 500, không cache gì
(chi tiết: một trường multipart đọc thất bại)
Chú thích hình: một giới hạn, nhưng phía vào và phía ra trả về hai mã trạng thái khác nhau với hệ quả trái ngược.
Giới hạn tính trên toàn bộ thân yêu cầu, kể cả ranh giới
100 MiB giới hạn thân của một yêu cầu, không phải kích thước một tệp và cũng không phải kích thước sau khi giải nén.
- Một thao tác đơn lẻ và một pipeline nhiều bước dùng chung con số này. Chia công việc thành 10 bước trong một lần gọi
/api/v1/pipelinekhông biến mức trần thành 1 GB; số bước chỉ ảnh hưởng thời gian chạy. - Chính giá trị ranh giới vẫn qua được: thân yêu cầu 104,857,600 byte đi qua, 104,857,601 byte thì không.
- Thân yêu cầu mang cả header của từng trường và dấu phân cách ranh giới, chứ không chỉ byte của tệp, nên phần dư dành cho một tệp luôn ít hơn 100 MiB một cách nghiêm ngặt. Một tệp đúng 104,857,600 byte sẽ bị từ chối.
Điểm cuối đó là chỗ thực hành dễ trượt nhất: curl -F tự thêm ranh giới cho bạn, nên đem kích thước tệp so với mốc ấy không bao giờ khớp.
Vượt giới hạn, bạn nhận 400 và bad_request
Phản hồi vượt giới hạn không dùng 413 và không hề có câu chữ nào về kích thước. Tái hiện bằng một thân yêu cầu hơn đúng một byte:
head -c 104857601 /dev/zero > /tmp/over.bin
curl -s -X POST "$API_BASE/api/v1/misc/compress-pdf" \
-H "X-API-KEY: $API_KEY" \
-F "fileInput=@/tmp/over.bin"
HTTP/1.1 400 Bad Request
content-type: application/problem+json
{ "code": "bad_request",
"detail": "failed to read multipart field: Error parsing `multipart/form-data` request",
"hint": "Fix request parameters or upload a valid PDF.",
"status": 400,
"title": "Bad Request",
"type": "https://pdf123.xyz/developers/errors#bad_request" }
Ba điều cần đọc cùng nhau:
- Trạng thái là 400. Đọc thân yêu cầu thất bại ở đây được phân loại là
bad_requestvà vẫn đi qua problem+json, nên một client viết theo kiểu vượt giới hạn nghĩa là 413 sẽ rẽ sai nhánh. codelàbad_request, một mục có thật trong bảng mã lỗi. Nó không rơi vào nhánh bắt tất cả, mà dùng chung một mã với tên trường gõ sai hay phần mã hóa multipart hỏng; không mục nào trong bảng đó liên quan đến kích thước.hintbảo bạn hãy tải lên một PDF hợp lệ. Tệp bạn vừa tải lên rất có thể chính là một PDF hợp lệ, chỉ lớn hơn vài trăm byte.
Dấu hiệu nhận biết vì thế không nằm ở mã trạng thái mà ở số byte của thân yêu cầu: với một 400 có detail chứa failed to read multipart field, hãy đo kích thước trước, thay vì dò lại biểu mẫu.
413 có xảy ra trên site này, chỉ là không ở các endpoint thao tác. Mọi điểm vào nhận tải tệp đều đã được nâng lên 100 MiB, còn những endpoint không nhận tải lên vẫn chạy theo mặc định 2 MiB của framework HTTP (axum của Rust), nơi thân vượt giới hạn nhận 413 kèm một dòng văn bản thuần:
head -c 2097153 /dev/zero > /tmp/big.json
curl -s -w '\n%{http_code}\n' -X POST "$API_BASE/api/v1/auth/login" \
-H 'Content-Type: application/json' \
--data-binary @/tmp/big.json
Failed to buffer the request body: length limit exceeded
413
Một sự thật, hai mã trạng thái và hai thân phản hồi tùy theo endpoint. Mang trải nghiệm của bên này áp sang bên kia sẽ khiến bạn sai.
Kiểu hỏng còn lại với cùng con số, ở chiều quay về
Một yêu cầu có Idempotency-Key sẽ cache phản hồi để lần thử lại có thể phát lại. Cache nghĩa là phải đọc thân phản hồi vào bộ nhớ trước, và lần đọc đó cũng bị chặn ở đúng 100 MiB, tuy điều kiện hẹp hơn nhiều:
- Yêu cầu mang
Idempotency-Key - Cache trượt, nên khóa này là mới
- Thao tác thượng nguồn trả về 2xx
Phản hồi thất bại không được cache mà đi thẳng qua, nên chỉ một đầu ra lớn thành công mới chạm giới hạn này.
Điều xảy ra ở đó mới là điểm đáng nhớ: bạn nhận 500, và không có gì được ghi vào cache. Thao tác đã chạy xong, vậy mà người gọi lại thấy thất bại; vì không có gì được cache, lần thử lại sẽ chạy lại toàn bộ. Khóa idempotency tồn tại để loại bỏ công việc trùng lặp lại hỏng đúng lúc cần nó nhất, khoác cho một tác vụ đã hoàn tất vẻ ngoài của một lỗi máy chủ. Cache nằm trong bộ nhớ tiến trình và không bao giờ ghi ra đĩa, nên khởi động lại là nó mất; về ngữ nghĩa, xem Idempotency-Key: thử lại an toàn cho tác vụ PDF.
100 MiB không phải một tham số nền tảng có thể cấu hình
Con số này không thể thay đổi. Không có biến môi trường nào nâng hay hạ nó, theo cả hai hướng; muốn một con số khác thì phải sửa mã và dựng lại image. Tìm nó như một thiết lập triển khai thì bạn sẽ không thấy.
Nó cũng không chỉ là một hạn mức. Byte của thân yêu cầu được đọc trọn vào bộ nhớ trước khi xử lý, nên mỗi lần tải lớn chạy song song đều giữ bên cạnh một lượng bộ nhớ tương đương. Nâng mức trần lên nghĩa là chấp nhận đỉnh bộ nhớ cao hơn theo; con số này cũng là thứ giữ cho một yêu cầu không kéo sập tiến trình.
Một bản triển khai tự lưu trữ mặc định không có reverse proxy, và portal không kiểm tra kích thước trước khi gửi, nên lần từ chối đó đến từ giới hạn của chính máy chủ. Đặt nginx lên trước thì bạn sẽ gặp nginx trước: client_max_body_size mặc định chỉ cho 1 MiB và trả 413, một hình dạng gần với cái trong phần so sánh trên và dễ bị nhầm là cùng một giới hạn.
Phải làm gì khi gặp giới hạn
Rẻ nhất trước:
- Đo trước khi gửi. So số byte của thân yêu cầu với 104,857,600 trước khi yêu cầu đi, và chừa chỗ cho phần ranh giới. Cách này tốt hơn đọc mã trạng thái sau đó.
- Nén tệp xuống dưới giới hạn. Kích thước một bản scan phần lớn đến từ lớp ảnh, và nén lại thường cắt được một phần đáng kể. Nén PDF chạy ngay trong trình duyệt, không cần script.
- Chia việc thành nhiều lần gọi. Khi nội dung chia được, hãy tách rồi gửi trong vài yêu cầu: ít rắc rối hơn nâng mức trần, và không đẩy bộ nhớ mà một yêu cầu giữ lên cao.
- Chỉ đổi giới hạn khi có nhu cầu bắt buộc gửi một lần. Điều đó nghĩa là sửa mã, dựng lại, và chấp nhận chi phí bộ nhớ ở phần trước.
Giới hạn này buộc client phải quyết định trước
Cả hai kiểu hỏng đều dẫn tới cùng một kết luận: con số 100 MiB phải do client tự tính trước khi gửi. Ở chiều vào, bạn được trả lời bằng bad_request, một mã không nói gì về kích thước; ở chiều ra, bằng 500, trông như lỗi máy chủ. Đếm số byte trước khi yêu cầu rời đi là cách duy nhất để phán đoán mà không phụ thuộc vào câu chữ của thông báo lỗi.