meta charset utf-8: 404 จากที่อยู่รูปภาพภาษาเกาหลีที่ครอว์เลอร์ของ AdSense ทำให้เสีย และ php-fpm ที่เต็ม
หากฝั่งที่อ่านหาการประกาศ meta charset utf-8 ในส่วนต้นของ HTML ไม่พบ และส่วนหัวการตอบกลับก็ไม่มี charset ฝั่งที่อ่านจะต้องเดาการเข้ารหัสอักขระของเอกสาร หรืออ่านด้วยค่าเริ่มต้น บนเซิร์ฟเวอร์ WordPress ที่ใช้ระบบ Nginx และ Apache ครอว์เลอร์ของ AdSense ส่งคำขอจำนวนมากไปยังที่อยู่รูปภาพภาษาเกาหลี (ไม่ใช่ ASCII) ที่เสีย จนการจัดการ 404 ทำให้ worker ของ PHP เต็มทั้งหมด หลังจากติดตามเรื่องนี้จากล็อก จึงได้เพิ่ม charset ให้ส่วนหัวการตอบกลับของหน้าแคช และให้ nginx ตอบไฟล์สแตติกที่ไม่มีอยู่ด้วย 404 ทันที เพื่อไม่ให้คำขอเดิมกลับมาทำให้ worker เต็มอีก
1️⃣ meta charset utf-8: เหตุผลที่ต้องอยู่ภายใน 1,024 ไบต์แรก
เซิร์ฟเวอร์บอกการเข้ารหัสอักขระของเอกสารได้สองที่ คือ charset ที่ต่อท้ายส่วนหัวการตอบกลับ [Content-Type] และบรรทัด meta charset ที่เขียนไว้ใน HTML
มาตรฐาน HTML กำหนดให้การประกาศการเข้ารหัสอักขระอยู่ภายใน 1,024 ไบต์แรกเท่านั้น และแนะนำให้ฝั่งที่อ่านอ่านล่วงหน้าเฉพาะ 1,024 ไบต์แรกนั้นเพื่อค้นหา ดังนั้นแม้จะอยู่ใน head แต่ meta charset ที่ถูกดันไปหลัง 1,024 ไบต์ก็อาจหาไม่พบสำหรับฝั่งที่อ่านบางตัว
เมื่อเลือกการเข้ารหัส ฝั่งที่อ่านจะดู BOM (เครื่องหมายลำดับไบต์) ก่อน ถัดมาคือ charset ในส่วนหัวการตอบกลับ แล้วจึงเป็น meta charset ที่พบจากการอ่านล่วงหน้า 1,024 ไบต์แรก หากไม่มีทั้งสามอย่าง ก็จะเดาจากลักษณะไบต์หรืออ่านด้วยค่าเริ่มต้นที่กำหนดไว้ และในตารางค่าเริ่มต้นที่มาตรฐาน HTML เสนอไว้ locale ส่วนใหญ่ใช้ windows-1252
หากอ่านเอกสาร UTF-8 เป็น windows-1252 อักขระหนึ่งตัวที่ประกอบด้วยหลายไบต์ เช่น ตัวอักษรภาษาเกาหลี จะกลายเป็นอักขระอื่นหลายตัว
2️⃣ ที่อยู่รูปภาพภาษาเกาหลี (ไม่ใช่ ASCII) ที่ครอว์เลอร์ของ AdSense อย่าง Mediapartners-Google ทำให้เสีย
เมื่อตรวจหน้าแคชก่อนแก้โดยยึด 1,024 ไบต์แรกเป็นเกณฑ์ ส่วนหัวการตอบกลับ Content-Type ไม่มี charset และบรรทัด meta charset อยู่ที่ไบต์ที่ 15,199 หน้าแคชที่ครอว์เลอร์ของ AdSense อย่าง Mediapartners-Google ได้รับจึงไม่มีการเข้ารหัสที่หาพบได้ทั้งสองที่
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlในระบบนี้ หากต่อสตริงคิวรีท้ายที่อยู่ จะข้ามหน้าแคชและได้การตอบกลับที่ PHP สร้างโดยตรง ซึ่งการตอบกลับนั้นมี charset ติดมาด้วย
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8ตำแหน่งที่ meta charset เริ่มต้นในไฟล์แคช ตรวจได้ละเอียดถึงระดับไบต์ด้วย [grep -b]
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charsetแท็ก img ในไฟล์แคชเดียวกันมีชื่อไฟล์หลายภาษาแบบ UTF-8 (ในกรณีนี้คือภาษาเกาหลี) อยู่ตามเดิมโดยไม่ได้เข้ารหัสแบบเปอร์เซ็นต์ หากอ่าน HTML นี้เป็น windows-1252 ชื่ออย่าง [대시보드-화면-예시.png] จะกลายเป็นอักขระอย่าง [대시보드-화면-예시.png] และเมื่อนำอักขระนั้นไปเข้ารหัสแบบเปอร์เซ็นต์เป็น UTF-8 อีกครั้งแล้วส่งคำขอ เซิร์ฟเวอร์ก็จะหาไฟล์ที่ไม่มีอยู่
เมื่อดึง 404 ของรูปภาพที่ครอว์เลอร์นี้ได้รับออกมาจากล็อกการเข้าถึง จะเห็นที่อยู่ชื่อเสียเรียงต่อกันไป
grep 'Mediapartners-Google' /var/log/nginx/access.log | grep ' 404 ' | grep 'GET /wp-content/uploads/' | cut -d' ' -f1,4,7,9 | head -2203.0.113.3 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---1024x682.png 404
203.0.113.1 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---300x159.jpg 404ที่อยู่ที่เหลืออยู่ในล็อกตรงกันทุกไบต์กับค่าที่ได้จากการอ่านไบต์ UTF-8 ของชื่อไฟล์เดิมเป็นอักขระ windows-1252 แล้วเข้ารหัสเป็น UTF-8 อีกครั้ง ในล็อกวันนั้น คำขอไปยังที่อยู่รูปภาพที่เสียแบบเดียวกันมาจากครอว์เลอร์นี้ 202 รายการ จากสคริปต์หนึ่งตัว (python-requests) 6 รายการ และจากผู้เยี่ยมชม 0 รายการ
เมื่อนำชื่อที่เสีย [대시보드-화면-예시.png] ไปแปลงกลับด้วย ตัวแปลง UTF-8 จะถูกตัดสินว่าเป็นกรณีที่อ่านไบต์ UTF-8 เป็น windows-1252 จนเสีย และได้ชื่อเดิม [대시보드-화면-예시.png] กลับมา
3️⃣ ขั้นตอนที่ 404 ของรูปภาพที่ไม่มีอยู่ทำให้ php-fpm เต็ม
เนื่องจากรูปภาพของที่อยู่ที่เสียไม่มีอยู่บนเซิร์ฟเวอร์ nginx จึงส่งคำขอต่อไปยัง Apache ตามรายการสุดท้ายของ try_files ในกฎ location ของรูปภาพ และ WordPress สร้างหน้า 404 ด้วย PHP เมื่อดู 404 จำนวน 587 รายการที่บันทึกในช่วงกลางวันของวันเดียวกัน หน้า 404 ที่ไม่มีแคชหนึ่งหน้าใช้เวลามัธยฐาน 1.4 วินาที
คำขอรูปภาพที่ชื่อเสียหลั่งไหลเข้ามากว่า 200 รายการในช่วง 3 นาทีตั้งแต่ 20:40 และ 133 รายการในนั้นจบด้วย 404 worker ของ php-fpm ถูกจัดสรรหนึ่งตัวต่อหนึ่งคำขอ และเมื่อคำขอยังเข้ามาต่อเนื่องขณะที่ 404 แต่ละรายการใช้เวลา 4 ถึง 6 วินาที พูลก็ถึงขีดจำกัดเมื่อ 20:40:55
ช่วงเวลาที่พูลถึงขีดจำกัดจะถูกบันทึกในล็อกข้อผิดพลาดของ php-fpm เป็น คำเตือน php-fpm max_children ดังนี้
grep 'max_children' /var/log/php-fpm/error.log[03-Nov-2026 20:40:55] WARNING: [pool www] server reached pm.max_children setting (20), consider raising itคำขอที่เข้ามาหลังพูลเต็มต้องรอ worker ที่ว่าง 404 ที่รอนานที่สุดจบใน 58 วินาที คำขอที่ครอว์เลอร์รอ 60 วินาทีแล้วตัดไปถูกบันทึกเป็น 499 และคำขอหนึ่งรายการที่ไม่ได้รับการตอบกลับจาก backend ถูกบันทึกเป็น 504
การโหลดแคชล่วงหน้าและงานตามกำหนดเวลา (Action Scheduler) ที่ทำงานในเวลาเดียวกัน รวมถึง wp-cron ที่ทำงานทุกนาที ได้ตรวจแล้วและตัดออกจากสาเหตุ การโหลดล่วงหน้าทำงานด้วยความเร็วต่ำเท่าเดิมตลอดทั้งวัน งานตามกำหนดเวลาในช่วงนั้นประมวลผลเฉพาะงานโหลดล่วงหน้า และ wp-cron ทำงานด้วย PHP แบบบรรทัดคำสั่งจึงไม่ใช้ worker ของ php-fpm
4️⃣ nginx charset utf-8 และ 404 ทันทีสำหรับไฟล์สแตติกที่ไม่มีอยู่
ได้เพิ่มบรรทัด [charset utf-8;] หนึ่งบรรทัดใน location ของ nginx ที่ส่งหน้าแคชออกไป เพื่อให้ส่วนหัวการตอบกลับมี charset และสำหรับรูปภาพกับไฟล์สแตติกในโฟลเดอร์อัปโหลด หากไม่มีอยู่บนดิสก์ nginx จะคืน 404 ทันทีโดยไม่ส่งต่อไปยัง WordPress
# บล็อก http: กำหนดค่า Cache-Control เฉพาะการตอบกลับ 404
map $status $up404_cc { 404 'no-store'; default ''; }
# location ที่ส่ง HTML จากแคชออกไปโดยตรง
location / {
try_files $cache_file @backend;
charset utf-8;
}
# โฟลเดอร์อัปโหลด: รูปภาพที่ไม่มีอยู่ได้ 404 ทันทีโดยไม่ส่งต่อไปยัง backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}map อยู่ในบล็อก http และเพราะ nginx ไม่ส่งส่วนหัวที่มีค่าเป็นสตริงว่าง การตอบกลับ 200 จึงไม่มี no-store ติดไป location แบบ regex จะใช้ตัวที่ตรงก่อนจากบนลงล่าง จึงวางไว้ก่อน location ของรูปภาพเดิม และเพิ่ม location แบบเดียวกันอีกหนึ่งตัวสำหรับไฟล์สแตติกอย่าง css, js และฟอนต์
หลังแก้แล้วส่งคำขอที่อยู่เดิมอีกครั้ง ส่วนหัวการตอบกลับของหน้าแคชก็มี charset
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html; charset=utf-8curl -sI https://example.com/wp-content/uploads/2025/07/no-such-image.png | grep -i -E '^(HTTP|content-type|cache-control)'# ก่อนแก้: 404 ที่ WordPress สร้าง
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# หลังแก้: 404 ที่ nginx คืนให้ทันที
HTTP/2 404
content-type: text/html
cache-control: no-store404 ของรูปภาพที่ไม่มีอยู่เปลี่ยนจากหน้า WordPress ที่ใช้ 1.01 วินาทีสร้าง 319,058 ไบต์ มาเป็นหน้าเริ่มต้นของ nginx ที่คืน 555 ไบต์ใน 0.015 วินาที การใส่ [Cache-Control: no-store] เฉพาะ 404 ทำให้ CDN ไม่เก็บ 404 ของรูปภาพที่ไม่มีอยู่ ส่วนการตอบกลับที่ PHP ส่งพร้อม charset อยู่แล้ว nginx จะไม่เติม charset ซ้ำ จึงมี charset ในส่วนหัวการตอบกลับเพียงครั้งเดียว
| คำขอที่ตรวจ | ก่อนแก้ | หลังแก้ |
|---|---|---|
| รูปภาพที่ไม่มีอยู่ | 319,058B, 1.01 วินาที (404 ของ WordPress) | 555B, 0.015 วินาที (404 ของ nginx) |
| รูปภาพที่ชื่อเสีย | 1.05 วินาที (404 ของ WordPress) | 0.014 วินาที (404 ของ nginx) |
| ส่วนหัวการตอบกลับของ HTML จากแคช | text/html | text/html; charset=utf-8 |
| ส่วนหัวการตอบกลับที่ PHP สร้างโดยตรง | text/html; charset=UTF-8 | เหมือนเดิม (charset ครั้งเดียว) |
| รูปภาพที่มีอยู่ | 200 | ยังเป็น 200 |
การจัดการ 404 แบบนี้ไม่เหมาะกับเว็บไซต์ที่ใช้ปลั๊กอินรูปภาพซึ่งสร้างภาพย่อหรือ WebP ด้วย PHP หลังมีคำขอเข้ามา เพราะ nginx คืน 404 ไปก่อน รูปภาพจึงไม่ถูกสร้าง หากระบบของคุณสร้างไฟล์ WebP ไว้ล่วงหน้าตอนอัปโหลด ก็ใช้ได้ตามเดิม
5️⃣ เหตุผลที่ meta charset ถูกดันไปข้างหลัง: WP Rocket Delay JS
แม้เพิ่ม charset ให้ส่วนหัวการตอบกลับแล้ว meta charset ใน HTML จากแคชก็ยังอยู่ที่ไบต์ที่ 15,199 เหมือนเดิม ในหน้าที่ PHP สร้างโดยตรง บรรทัดเดียวกันนี้อยู่ที่ไบต์ที่ 135
เมื่อเปิด Delay JS ของ WP Rocket ตอนสร้างไฟล์แคชจะใส่สคริปต์สำหรับหน่วงการทำงานอย่าง lazyload ไว้ถัดจาก head ทันที แล้วมีขั้นตอนย้าย meta charset กลับไปไว้หน้าสุดของ head ขั้นตอนย้ายนี้ใช้ regex กับทั้งเอกสาร ในโพสต์ขนาดใหญ่ประมาณ 1.2MB จึงเกินขีดจำกัด backtrack ของ PHP และล้มเหลว
เมื่อ regex ล้มเหลว WP Rocket จะแคช HTML ตามสภาพที่มีสคริปต์ใส่เข้าไปแล้วแต่ยังย้าย meta charset ไม่ได้ ผลคือ meta charset ถูกดันออกไปนอก 1,024 ไบต์แรก เมื่อทดลองซ้ำแยกต่างหากด้วยการตั้งค่า PHP เดียวกัน การย้ายล้มเหลวเมื่อขีดจำกัด backtrack เป็นค่าเริ่มต้น 1,000,000 และ 5,000,000 และสำเร็จตั้งแต่ 10,000,000 ขึ้นไป
ครั้งนี้แก้ด้วยการเพิ่ม charset ให้ส่วนหัวการตอบกลับ และไม่ได้แก้ตำแหน่งของ meta charset การเพิ่มขีดจำกัด backtrack ทำให้ WP Rocket กลับมาทำงานแบบเดิม แต่ขีดจำกัดของ regex ทุกตัวที่ทำงานใน PHP เดียวกันจะเพิ่มขึ้น 10 เท่า จึงไม่แนะนำ
🔢 FAQ & เนื้อหาแนะนำ
HTTP 404
ข้อผิดพลาด HTTP 404 (ข้อผิดพลาดฝั่งไคลเอนต์): สาเหตุและวิธีแก้
หน้าข้อผิดพลาด HTTP 404 ซึ่งเป็นหนึ่งในรหัสสถานะ HTTP จะแสดงเมื่อหาหน้าไม่พบ
AdSense
ตั้งค่า ads.txt ใน WordPress: แก้คำเตือน AdSense
ads.txt เป็นไฟล์ข้อความที่วางไว้ที่รูทของเว็บไซต์ เพื่อเปิดเผยรายชื่อผู้ขายที่ได้รับอนุญาตให้ขายพื้นที่โฆษณาของโดเมนนี้
แคช WP Rocket
WP Rocket กับ URL ภาษาเกาหลี: สาเหตุที่ nginx หาไฟล์แคชไม่เจอและวิธีแก้ด้วย njs
โพสต์ที่ใช้slug ภาษาอังกฤษ nginx จะส่งไฟล์แคชได้ทันที แต่โพสต์ที่ใช้slug ภาษาเกาหลี แม้จะมีไฟล์แคชเดียวกันอยู่ ก็จะส่งไฟล์นั้นได้หลังจาก PHP ทำงานไปหนึ่งครั้งแล้ว
ℹ️ ประกาศเรื่องลิงก์แอฟฟิลิเอต
เนื้อหาของเว็บไซต์นี้มีลิงก์แอฟฟิลิเอต หากผู้เข้าชมซื้อสินค้าหรือบริการผ่านลิงก์เหล่านี้ เว็บไซต์นี้จะได้รับค่าคอมมิชชันจากผู้ขาย ในกระบวนการนี้ ราคาที่ผู้ซื้อจ่าย(ราคาจะลดลงเมื่อมีโปรโมชัน↓)จะไม่สูงขึ้นแต่อย่างใด ราคา ส่วนลด และข้อมูลสต็อกที่แสดงเป็นข้อมูล ณ เวลาที่เขียนและอาจต่างจากความเป็นจริง โปรดตรวจสอบกับผู้ขายอีกครั้งก่อนซื้อ การเลือกและการประเมินสินค้าเขียนตามเกณฑ์ของเราเอง และการได้รับค่าคอมมิชชันหรือไม่ไม่มีผลต่อลำดับการแนะนำหรือเนื้อหาการประเมิน