เมื่ออัปโหลด PDF ขนาดใหญ่แล้วถูกปฏิเสธ: ขีดจำกัด 100 MiB ของบอดีคำขอ และข้อผิดพลาดที่ชี้ผิดทาง
บอดีของคำขอหนึ่งครั้งมีได้ถึง 100 MiB หรือ 104,857,600 ไบต์ รวมกรอบของ multipart ด้วย เมื่อเกินขีดจำกัด เอนด์พอยต์ของโอเปอเรชันจะตอบ 400 พร้อมรหัส bad_request ไม่ใช่ 413 ส่วนเมื่อมี Idempotency-Key จะมีขีดจำกัด 100 MiB อีกชั้นหนึ่งบนบัฟเฟอร์ของการตอบกลับ เกินแล้วได้ 500 และไม่มีอะไรถูกแคช

PDF ขนาดใหญ่ที่อัปโหลดไม่ขึ้นมักไม่ใช่ไฟล์ที่เสีย แต่เป็นเพราะบอดีของคำขอชนขีดจำกัดต่อคำขอหนึ่งครั้ง: 100 MiB หรือ 104,857,600 ไบต์ ตัวเลขนี้วัดจากบอดีทั้งก้อน รวมขอบเขตของ multipart และส่วนหัวของฟิลด์ และจำนวนนี้พอดีผ่าน ขณะที่เพิ่มอีก 1 ไบต์ถูกปฏิเสธ
ข้อผิดพลาดที่ตอบกลับมาชี้ไปคนละทาง เอนด์พอยต์ของโอเปอเรชันตอบ 400 โดยตั้ง code เป็น bad_request และรายละเอียดบอกว่าอ่านฟิลด์ multipart ไม่สำเร็จ โดยไม่มีการกล่าวถึงขนาดเลย ไคลเอนต์ที่แยกทางตาม code จะจัดมันเป็นข้อผิดพลาดด้านพารามิเตอร์ แล้วไปตรวจชื่อฟิลด์ ทั้งที่สิ่งที่ต้องแก้คือขนาดไฟล์
100 MiB เดียวกันยังคุมทิศทางตรงข้ามด้วย คำขอที่พา Idempotency-Key มาจะอ่านการตอบกลับเข้าหน่วยความจำก่อนแคช เทียบกับตัวเลขเดียวกัน และเมื่อเกิน ผู้เรียกจะได้รับ 500 ขณะที่โอเปอเรชันทำงานเสร็จไปแล้ว ตัวเลขเดียว กับรูปแบบความล้มเหลวสองแบบที่ตรงข้ามกัน
100 MiB = 104,857,600 ไบต์ (จำนวนนี้พอดีผ่าน)
|
+------------------+------------------+
| |
ขาเข้า (บอดีของคำขอ) ขาออก (บอดีของการตอบกลับ)
นับบอดีทั้งก้อน รวมขอบเขต multipart เฉพาะเมื่อมี Idempotency-Key
และส่วนหัวของฟิลด์; โอเปอเรชันหนึ่ง แคชพลาด และต้นทางตอบ 2xx
กับไปป์ไลน์ใช้ตัวเลขร่วมกัน
เกิน: 400 + bad_request เกิน: 500 และไม่เขียนแคช
(รายละเอียด: อ่านฟิลด์ multipart ไม่สำเร็จ)
หมายเหตุภาพ: ขีดจำกัดเดียว และสองฝั่งคือขาเข้ากับขาออกของมันให้รหัสสถานะต่างกันพร้อมผลลัพธ์ที่ตรงข้ามกัน
ขีดจำกัดนับบอดีของคำขอทั้งก้อน รวมขอบเขต
100 MiB จำกัดบอดีของคำขอเดียว ไม่ใช่ขนาดของไฟล์เดียว และไม่ใช่ขนาดหลังคลายการบีบอัด
- โอเปอเรชันเดียวกับไปป์ไลน์หลายขั้นใช้ตัวเลขร่วมกัน การซอยงานเป็น 10 ขั้นในคำขอ
/api/v1/pipelineครั้งเดียวไม่ได้ทำให้เพดานกลายเป็น 1 GB จำนวนขั้นมีผลกับเวลาทำงานเท่านั้น - ค่าที่ขอบพอดีผ่าน: บอดีของคำขอ 104,857,600 ไบต์ผ่านไป 104,857,601 ไบต์ไม่ผ่าน
- บอดีมีส่วนหัวของแต่ละฟิลด์และตัวคั่นขอบเขตมาด้วยพร้อมกับไบต์ของไฟล์ เพราะฉะนั้นโควตาที่เหลือให้ไฟล์เดียวจึงน้อยกว่า 100 MiB อย่างเคร่งครัด ไฟล์ขนาด 104,857,600 ไบต์พอดีจะถูกปฏิเสธ
ข้อสุดท้ายคือจุดที่ภาคปฏิบัติพลาดง่ายที่สุด: curl -F เติมขอบเขตให้เอง การเทียบขนาดไฟล์กับเส้นนั้นจึงไม่มีทางตรง
เกินขีดจำกัดแล้วได้ 400 กับ bad_request
การตอบกลับเมื่อเกินขีดจำกัดไม่ใช้ 413 และไม่มีถ้อยคำเกี่ยวกับขนาดเลย ลองสร้างซ้ำด้วยบอดีที่เกินไป 1 ไบต์:
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" }
สามอย่างที่ต้องอ่านประกอบกัน:
- สถานะคือ 400 การอ่านบอดีล้มเหลวถูกจัดเป็น
bad_requestที่นี่ และยังผ่าน problem+json ตามปกติ ไคลเอนต์ที่เขียนไว้สำหรับ "เกินขีดจำกัดหมายถึง 413" จึงเลือกทางผิด codeคือbad_requestซึ่งเป็นรายการจริงในตารางรหัสข้อผิดพลาด มันไม่ตกลงไปในทางสำรอง แต่ใช้รหัสร่วมกับชื่อฟิลด์ที่พิมพ์ผิดหรือการเข้ารหัส multipart ที่พัง และไม่มีรายการใดในตารางนั้นเกี่ยวกับขนาดhintบอกให้อัปโหลด PDF ที่ถูกต้อง ไฟล์ที่คุณอัปโหลดมีแนวโน้มสูงว่าเป็น PDF ที่ถูกต้อง ซึ่งบังเอิญใหญ่เกินไปไม่กี่ร้อยไบต์
สัญญาณบอกใบ้จึงไม่ใช่รหัสสถานะ แต่เป็นจำนวนไบต์ของบอดี: เมื่อได้ 400 ที่ detail มี failed to read multipart field ให้วัดขนาดต่อ ไม่ต้องย้อนไปตรวจฟอร์ม
413 เกิดขึ้นจริงบนเว็บนี้ เพียงแต่ไม่เกิดบนเอนด์พอยต์ของโอเปอเรชัน ทุกจุดเข้าที่รับอัปโหลดไฟล์ถูกยกเป็น 100 MiB ส่วนเอนด์พอยต์ที่ไม่รับอัปโหลดยังใช้ค่าเริ่มต้น 2 MiB ของเฟรมเวิร์ก HTTP (axum ของ Rust) ซึ่งบอดีที่เกินขีดจำกัดจะได้ 413 กับข้อความล้วนหนึ่งบรรทัด:
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
ข้อเท็จจริงเดียว ให้รหัสสถานะสองแบบและบอดีตอบกลับสองแบบ ขึ้นกับชนิดของเอนด์พอยต์ การยกประสบการณ์จากฝั่งหนึ่งไปอีกฝั่งจะทำให้เข้าใจผิด
ความล้มเหลวอีกแบบที่ใช้ตัวเลขเดียวกัน ทางขากลับ
คำขอที่มี Idempotency-Key จะแคชการตอบกลับไว้เพื่อให้การลองใหม่เล่นซ้ำได้ การแคชหมายถึงอ่านบอดีของการตอบกลับเข้าหน่วยความจำก่อน และการอ่านนั้นถูกจำกัดด้วย 100 MiB เดียวกัน แม้จะเกิดภายใต้เงื่อนไขที่แคบกว่ามาก:
- คำขอพา
Idempotency-Keyมา - แคชพลาด คีย์นี้จึงเป็นคีย์ใหม่
- โอเปอเรชันต้นทางตอบ 2xx
การตอบกลับที่ล้มเหลวไม่ถูกแคชและผ่านเลยออกไป เพราะฉะนั้นมีเพียงเอาต์พุตขนาดใหญ่ที่สำเร็จเท่านั้นที่มาถึงขีดจำกัดนี้
สิ่งที่เกิดขึ้นตรงนั้นคือประเด็นที่ควรจำ: คุณได้ 500 และไม่มีอะไรถูกเขียนลงแคช โอเปอเรชันทำงานไปแล้ว แต่ผู้เรียกกลับเห็นความล้มเหลว และเพราะไม่มีอะไรถูกแคช การลองใหม่จึงรันทุกอย่างซ้ำทั้งหมด คีย์ idempotency มีไว้กำจัดงานซ้ำ แต่กลับล้มเหลวพอดีในเวลาที่จำเป็นที่สุด และทำให้งานที่เสร็จแล้วดูเหมือนความผิดพลาดของเซิร์ฟเวอร์ แคชนี้อยู่ในหน่วยความจำของโปรเซสและไม่เคยเขียนลงดิสก์ รีสตาร์ทแล้วก็หายไป ส่วนความหมายดูได้ที่ Idempotency-Key: การลองใหม่ที่ปลอดภัยสำหรับงาน PDF
100 MiB ไม่ใช่ค่าตั้งของแพลตฟอร์มที่ปรับได้
ตัวเลขนี้เปลี่ยนไม่ได้ ไม่มีตัวแปรสภาพแวดล้อมใดเพิ่มหรือลดมันได้ทั้งสองทาง การจะได้ตัวเลขอื่นต้องแก้โค้ดและรีบิลด์อิมเมจ ถ้าไปหามันในฐานะค่าตั้งของการดีพลอยจะไม่เจอ
มันยังมากกว่าโควตาข้อหนึ่ง ไบต์ของบอดีคำขอถูกอ่านเข้าหน่วยความจำทั้งหมดก่อนประมวลผล ทุกการอัปโหลดขนาดใหญ่ที่ทำพร้อมกันจึงถือหน่วยความจำจำนวนใกล้เคียงกันไว้ด้วย การยกเพดานขึ้นจึงหมายถึงการยอมรับจุดสูงสุดของหน่วยความจำที่สูงขึ้นตามมา: ตัวเลขนี้ยังเป็นสิ่งที่กันไม่ให้คำขอเดียวดึงโปรเซสลงไปด้วย
การดีพลอยแบบโฮสต์เองตามค่าเริ่มต้นไม่มีรีเวิร์สพร็อกซี และพอร์ทัลไม่ตรวจขนาดก่อนส่ง เพราะฉะนั้นการปฏิเสธนั้นมาจากขีดจำกัดของเซิร์ฟเวอร์เอง ถ้าวาง nginx ไว้ข้างหน้า จะชน nginx ก่อน: client_max_body_size ปล่อยผ่านเพียง 1 MiB โดยค่าเริ่มต้นและตอบ 413 ซึ่งรูปร่างใกล้เคียงกับที่เทียบไว้ข้างบนและเข้าใจผิดได้ง่ายว่าเป็นขีดจำกัดเดียวกัน
ทำอย่างไรเมื่อเจอเข้า
เริ่มจากวิธีที่ถูกที่สุดก่อน:
- วัดก่อนส่ง เทียบจำนวนไบต์ของบอดีคำขอกับ 104,857,600 ก่อนที่คำขอจะออกไป และเผื่อที่ว่างให้ขอบเขตด้วย วิธีนี้ดีกว่าการไปอ่านรหัสสถานะทีหลัง
- บีบอัดไฟล์ให้ต่ำกว่าขีดจำกัด ขนาดของไฟล์สแกนมาจากชั้นภาพเป็นส่วนใหญ่ และการบีบอัดใหม่มักตัดออกได้เป็นสัดส่วนที่เห็นได้ชัด บีบอัด PDF รันในเบราว์เซอร์ ไม่ต้องเขียนสคริปต์
- แบ่งงานออกเป็นหลายคำขอ เมื่อเนื้อหาแบ่งได้ แยก PDF แล้วส่งเป็นคำขอสั้น ๆ ไม่กี่คำขอ: ยุ่งยากน้อยกว่าการยกเพดาน และไม่ดันหน่วยความจำที่คำขอเดียวถืออยู่ให้สูงขึ้น
- เปลี่ยนขีดจำกัดเฉพาะเมื่อจำเป็นต้องส่งในคำขอเดียวจริง ๆ นั่นหมายถึงแก้โค้ดและรีบิลด์ พร้อมยอมรับค่าใช้จ่ายด้านหน่วยความจำจากหัวข้อก่อนหน้า
ขีดจำกัดนี้ให้ไคลเอนต์ตัดสินใจล่วงหน้าเอง
ความล้มเหลวทั้งสองแบบชี้ไปที่ข้อสรุปเดียว: ไคลเอนต์ต้องคำนวณตัวเลข 100 MiB เองก่อนส่ง ทางขาเข้า คุณได้รับการตอบเป็น bad_request ซึ่งเป็นรหัสที่ไม่บอกอะไรเกี่ยวกับขนาด ทางขาออก ตอบด้วย 500 ซึ่งดูเหมือนความผิดพลาดของเซิร์ฟเวอร์ การนับไบต์ก่อนที่คำขอจะออกไปคือวิธีตัดสินเดียวที่ไม่ต้องพึ่งว่าข้อความข้อผิดพลาดพูดว่าอะไร