22/10/2026 12:00น.

CORS error คืออะไร ทำไมเบราว์เซอร์บล็อก และแก้ยังไงให้ถูกที่
#CORS
#CORS error
#CORS policy
#Access-Control-Allow-Origin
#Same-Origin Policy
#Preflight Request
#ติด CORS
ลองนึกภาพว่าคุณเพิ่งเขียนหน้าเว็บแรกด้วย Vite เสร็จ เปิดที่ http://localhost:5173 แล้วสั่ง fetch ไปดึงรายชื่อผู้ใช้จาก API ที่เพื่อนในทีมเปิดไว้ที่ http://localhost:3000 ลองยิงผ่าน curl แล้วได้ข้อมูลครบ แต่พอเรียกจากหน้าเว็บ หน้าจอกลับว่างเปล่า เปิด Console ขึ้นมาเจอข้อความสีแดงยาวหนึ่งบรรทัด มีคำว่า CORS policy อยู่ตรงกลาง
ไม่ต้องตกใจ และไม่ได้แปลกเลย เกือบทุกคนที่เริ่มต่อหน้าเว็บเข้ากับ API จะติด CORS ครั้งแรกในจังหวะแบบนี้พอดี ที่ทำให้งงคือโค้ดดูไม่มีอะไรผิด และเซิร์ฟเวอร์ก็ทำงานปกติ วันนี้ Superdev Academy จะพาไปดูว่า CORS error คืออะไร ทำไมเบราว์เซอร์ถึงบล็อก คำขอไปถึงเซิร์ฟเวอร์หรือยัง และที่สำคัญที่สุดคือต้องแก้ที่ไหน เพราะคำตอบไม่ได้อยู่ในโค้ดหน้าเว็บอย่างที่หลายคนคิด
ข้อความสีแดงนี้บอกอะไรเราบ้าง
ทีมเราจำลองสถานการณ์ข้างบนขึ้นมาจริงบนเครื่อง ด้วยเซิร์ฟเวอร์ Node.js สองตัว ตัวหนึ่งเสิร์ฟหน้าเว็บที่พอร์ต 5173 อีกตัวเป็น API ที่พอร์ต 3000 ซึ่งยังไม่ได้ตั้งค่าอะไรเรื่อง CORS เลย แล้วเปิดหน้าเว็บด้วย Google Chrome ข้อความที่ได้ใน Console คือบรรทัดนี้
Access to fetch at 'http://localhost:3000/api/users' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.อ่านทีละท่อนจะเห็นว่ามันบอกครบทุกอย่างแล้ว ท่อนแรกบอกว่าคำขอวิ่งไปที่ไหน ท่อนที่สองบอกว่าหน้าเว็บของเรามาจาก origin ไหน และท่อนสุดท้ายบอกเหตุผลว่า คำตอบจากเซิร์ฟเวอร์ไม่มี Header ชื่อ Access-Control-Allow-Origin ติดมาด้วย
ส่วนในโค้ด JavaScript ของเรา สิ่งที่ catch จับได้มีแค่ TypeError: Failed to fetch ไม่มีรายละเอียดอื่นเลย เรื่องนี้ไม่ใช่บั๊ก MDN อธิบายไว้ในคู่มือ CORS ว่ารายละเอียดของความผิดพลาดแบบนี้ถูกซ่อนจาก JavaScript โดยตั้งใจเพื่อความปลอดภัย โค้ดรู้แค่ว่ามี error เกิดขึ้น ทางเดียวที่จะรู้ว่าพังเพราะอะไรคือเปิดดูใน Console ของเบราว์เซอร์ นี่คือเหตุผลที่เวลาเจอ Failed to fetch เราควรเปิด Console ก่อนเสมอ ก่อนจะไปไล่แก้โค้ด
Origin คืออะไร ทำไม localhost กับ localhost ถึงนับเป็นคนละที่
คำที่โผล่ในข้อความ error บ่อยที่สุดคือ origin ซึ่ง MDN นิยามไว้ ว่าประกอบด้วยสามส่วนของ URL คือ Scheme (โปรโตคอล เช่น http หรือ https), Host (ชื่อโดเมน) และ Port สองที่จะนับเป็น origin เดียวกันก็ต่อเมื่อตรงกันครบทั้งสามส่วน ส่วน path ด้านหลังไม่มีผล
http://localhost:5173กับhttp://localhost:3000: คนละ origin เพราะพอร์ตต่างกันhttp://example.comกับhttps://example.com: คนละ origin เพราะ Scheme ต่างกันhttp://example.comกับhttp://www.example.com: คนละ origin เพราะ Host ต่างกันhttp://example.com/app1กับhttp://example.com/app2: origin เดียวกัน เพราะต่างกันแค่ path
พูดง่ายๆ ถึงหน้าเว็บกับ API จะรันอยู่บนเครื่องเดียวกัน ใช้ชื่อ localhost เหมือนกัน แต่ถ้าพอร์ตไม่ตรงกัน เบราว์เซอร์ก็มองว่าเป็นคนละบ้านแล้ว ซึ่งตอนพัฒนาเว็บแทบจะเป็นแบบนี้เสมอ เพราะ Vite ใช้พอร์ต 5173 เป็นค่าเริ่มต้น ส่วน API มักจะรันอีกพอร์ตหนึ่ง
Same-Origin Policy: เบราว์เซอร์กำลังปกป้องใครอยู่
กฎที่อยู่เบื้องหลังข้อความนี้ชื่อ Same-Origin Policy ตามคำอธิบายของ MDN มันคือกลไกความปลอดภัยที่จำกัดว่าเอกสารหรือสคริปต์จาก origin หนึ่ง จะยุ่งกับข้อมูลของอีก origin ได้แค่ไหน โดยทั่วไปการอ่านข้อมูลข้าม origin จะถูกห้ามไว้ก่อน
ตัวอย่างที่ MDN ยกไว้ช่วยให้เห็นภาพชัดมาก ลองนึกว่าคุณล็อกอินเว็บเมลค้างไว้ในเบราว์เซอร์ แล้วบังเอิญเปิดเว็บแปลกๆ อีกแท็บ ถ้าไม่มีกฎนี้ สคริปต์ในเว็บแปลกๆ นั้นก็สั่ง fetch ไปที่เว็บเมล แล้วอ่านอีเมลของคุณกลับไปได้ทันที เพราะเบราว์เซอร์แนบ Cookie ของคุณไปด้วย
CORS error ไม่ได้มีไว้กันเรา มันมีไว้กันคนอื่นไม่ให้อ่านข้อมูลของผู้ใช้ผ่านเบราว์เซอร์ของผู้ใช้เอง
ส่วน CORS ที่ย่อมาจาก Cross-Origin Resource Sharing คือช่องทางให้เซิร์ฟเวอร์ประกาศผ่าน HTTP Header ว่ายอมให้ origin ไหนบ้างเข้ามาอ่านข้อมูลได้ สังเกตว่าคนที่ประกาศคือเซิร์ฟเวอร์ ไม่ใช่หน้าเว็บ ประโยคนี้คือหัวใจของการแก้ปัญหาทั้งหมดในบทความนี้
คำขอไปถึงเซิร์ฟเวอร์แล้ว แต่เบราว์เซอร์ไม่ยอมส่งคำตอบให้โค้ด
จุดที่มือใหม่เข้าใจผิดบ่อยที่สุดคือคิดว่าเบราว์เซอร์บล็อกคำขอตั้งแต่ยังไม่ออกจากเครื่อง ในการทดลองของเรา Log ฝั่ง API บันทึกไว้แบบนี้
[api] GET /api/users Origin=http://localhost:5173แปลว่าคำขอ GET วิ่งไปถึงเซิร์ฟเวอร์จริง เซิร์ฟเวอร์ก็ตอบกลับข้อมูลครบ แต่เบราว์เซอร์ตรวจแล้วไม่เจอ Header ที่อนุญาต origin ของเรา จึงไม่ยอมส่งคำตอบนั้นให้โค้ด JavaScript อ่าน ปัญหาจึงอยู่ที่ขั้นตอน "อ่านคำตอบ" ไม่ใช่ขั้นตอน "ส่งคำขอ"
ข้อนี้ยังอธิบายอีกเรื่องที่ทำให้หลายคนงง คือทำไมยิงด้วย curl หรือ Postman แล้วได้ข้อมูลปกติ เราลองยิงไปที่ API ตัวเดิมด้วยคำสั่งนี้
curl -i -H "Origin: http://localhost:5173" http://localhost:3000/api/usersผลคือได้ HTTP/1.1 200 OK พร้อมข้อมูล JSON ครบ ทั้งที่ไม่มี Header ของ CORS สักตัว เพราะ CORS เป็นกฎที่เบราว์เซอร์บังคับใช้ curl กับ Postman ไม่ได้บังคับ ดังนั้นการที่ curl ผ่านไม่ได้พิสูจน์ว่าเบราว์เซอร์จะผ่าน ถ้าอยากทวนเรื่อง Request, Response และ Status Code ก่อนไปต่อ ลองอ่าน HTTP Status Code มีอะไรบ้าง ประกอบได้
Preflight คืออะไร ทำไมบางครั้งเจอคำขอ OPTIONS โผล่มาก่อน
ถ้าเปิดแท็บ Network ดูแล้วเห็นคำขอ method OPTIONS วิ่งไปก่อนคำขอจริง นั่นคือ Preflight Request หรือคำขอที่เบราว์เซอร์ส่งไปถามเซิร์ฟเวอร์ก่อนว่า "ถ้าจะส่งแบบนี้ รับไหม"
ตามคู่มือ CORS ของ MDN คำขอที่ไม่ต้องมี Preflight เรียกว่า Simple Request ซึ่งต้องเข้าเงื่อนไขครบ เช่น method เป็น GET, HEAD หรือ POST เท่านั้น และถ้ามี Content-Type ต้องเป็นหนึ่งในสามค่านี้ application/x-www-form-urlencoded, multipart/form-data หรือ text/plain เมื่อไหร่ที่หลุดเงื่อนไข เช่น ส่ง POST พร้อม Content-Type: application/json หรือแนบ Header Authorization เบราว์เซอร์จะส่ง Preflight ก่อน โดยแนบ Access-Control-Request-Method และ Access-Control-Request-Headers ไปบอกว่าคำขอจริงจะใช้อะไร
ในการทดลองเดียวกัน เราลองส่ง POST แบบ JSON ไปที่ API ที่ยังไม่ตั้งค่า CORS Log ฝั่งเซิร์ฟเวอร์เห็นแค่บรรทัด OPTIONS /api/users ไม่มี POST ตามมาเลย และ Console ขึ้นข้อความว่า
... has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.สรุปคือ ถ้าเป็นคำขอที่ต้องมี Preflight แล้ว Preflight ไม่ผ่าน คำขอจริงจะไม่ถูกส่งออกไปเลย ต่างจากกรณี GET ธรรมดาที่คำขอถึงเซิร์ฟเวอร์แล้วแต่อ่านคำตอบไม่ได้ ถ้าเจอคำว่า preflight ในข้อความ error ให้ไปดูว่าเซิร์ฟเวอร์ตอบคำขอ OPTIONS ถูกหรือยัง
แก้ให้ถูกที่: ตั้งค่า Header ฝั่งเซิร์ฟเวอร์
เพราะคนที่มีสิทธิ์อนุญาตคือเซิร์ฟเวอร์ การแก้ที่ถาวรจึงอยู่ที่ API ไม่ใช่หน้าเว็บ ตัวอย่างนี้คือ API ตัวเดิมที่เราเพิ่มการตั้งค่า CORS เข้าไป เขียนด้วยโมดูล http ของ Node.js ล้วนๆ เพื่อให้เห็นว่าจริงๆ แล้วมันคือการใส่ Header ไม่กี่ตัว
import http from 'node:http';
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
res.setHeader('Vary', 'Origin');
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '600');
res.writeHead(204);
return res.end();
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ users: ['Mint', 'Beam'] }));
}).listen(3000);Access-Control-Allow-Origin: origin ที่อนุญาตให้อ่านคำตอบ ระบุให้ตรงกับหน้าเว็บของเราVary: Origin: MDN แนะนำให้ใส่เมื่อระบุ origin แบบเจาะจง เพื่อบอก cache ว่าคำตอบเปลี่ยนตาม origin ที่ขอมาAccess-Control-Allow-MethodsและAccess-Control-Allow-Headers: ตอบ Preflight ว่ารับ method และ Header อะไรบ้างAccess-Control-Max-Age: ให้เบราว์เซอร์จำผล Preflight ไว้กี่วินาที ในตัวอย่างนี้ตั้งไว้ 600
วิธีตรวจว่าตั้งค่าถูกแล้ว ใช้ curl จำลอง Preflight ได้เลย
curl -i -X OPTIONS \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
http://localhost:3000/api/usersผลที่เราได้คือ HTTP/1.1 204 No Content พร้อม Access-Control-Allow-Origin: http://localhost:5173 และ Header ที่เหลือครบ พอเปิดหน้าเว็บอีกครั้ง ทั้ง GET และ POST แบบ JSON ได้ข้อมูลกลับมาปกติ โดยไม่ต้องแก้โค้ดหน้าเว็บแม้แต่บรรทัดเดียว
ในงานจริง แทบไม่มีใครเขียน Header เองทีละตัวแบบนี้ เฟรมเวิร์กส่วนใหญ่มี Middleware สำหรับ CORS ให้ใช้ ถ้าอยากเห็นว่า Middleware ทำงานแทรกกลางคำขอยังไง ลองอ่าน Go กับ HTTP Middleware ซึ่งยกการจัดการ CORS เป็นหนึ่งในงานที่ Middleware ทำบ่อย หรือถ้ายังไม่เคยเขียน API เองเลย เริ่มจาก การสร้าง API ด้วย Node.js และ Go ก่อนก็ได้
ตอนพัฒนาบนเครื่อง ใช้ Proxy ของ Vite แทนได้
บางครั้งเราไม่ได้เป็นเจ้าของ API หรือยังไม่อยากแตะโค้ดเซิร์ฟเวอร์ระหว่างพัฒนา อีกทางที่ถูกต้องคือทำให้เบราว์เซอร์ไม่ต้องข้าม origin เลย เอกสารของ Vite มีตัวเลือก server.proxy สำหรับให้ Dev Server ส่งต่อคำขอที่ขึ้นต้นด้วย path ที่กำหนดไปยังเซิร์ฟเวอร์ปลายทาง
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:3000',
},
},
})จากนั้นในโค้ดหน้าเว็บให้เรียกแค่ fetch('/api/users') โดยไม่ต้องใส่โดเมน เบราว์เซอร์จะมองว่าเป็นคำขอไปที่ localhost:5173 ซึ่งเป็น origin เดียวกับหน้าเว็บ แล้ว Vite จะส่งต่อไปที่พอร์ต 3000 ให้เองแบบเซิร์ฟเวอร์คุยกับเซิร์ฟเวอร์ ซึ่งไม่มีกฎ CORS มาเกี่ยว ทีมเราลองกับ Vite 8.3.1 โดยใช้ API ตัวที่ยังไม่ได้ตั้งค่า CORS เลย ผลคือหน้าเว็บได้ข้อมูลครบ ไม่มีข้อความสีแดงใน Console
ข้อควรจำคือ server.proxy ทำงานเฉพาะตอนรัน Dev Server เท่านั้น พอ build ขึ้น Production ต้องกลับไปตอบให้ได้ว่าหน้าเว็บกับ API อยู่ origin เดียวกันไหม ถ้าไม่ ก็ต้องตั้ง Header ฝั่งเซิร์ฟเวอร์ตามหัวข้อก่อนหน้าอยู่ดี
ทางลัดที่ดูเหมือนแก้ได้ แต่ไม่ควรใช้
ปิด Security ของเบราว์เซอร์ หรือลง Extension ที่ปิด CORS ได้ไหม?
ทำได้จริง และข้อความสีแดงจะหายไปจริงบนเครื่องของคุณ แต่มันหายแค่บนเครื่องของคุณเครื่องเดียว ผู้ใช้จริงที่เปิดเว็บด้วยเบราว์เซอร์ปกติจะยังเจอปัญหาเดิมทุกคน แถมเบราว์เซอร์ที่ปิดกฎนี้ไว้ก็ไม่มีอะไรกันเว็บอื่นมาอ่านข้อมูลของคุณตามตัวอย่างเว็บเมลข้างบน ทางลัดนี้จึงแค่เลื่อนปัญหาไปเจอตอนขึ้น Production ไม่ได้แก้อะไร
ใส่ Access-Control-Allow-Origin: * ไปเลยจะได้จบ?
เครื่องหมาย * แปลว่าอนุญาตทุก origin ใช้ได้กับ API สาธารณะที่ไม่ต้องล็อกอิน แต่พอคำขอต้องแนบ Cookie หรือข้อมูลยืนยันตัวตน (Credentials) คู่มือ CORS ของ MDN เขียนไว้ชัดว่าเซิร์ฟเวอร์ต้องไม่ใช้ * และต้องระบุ origin แบบเจาะจงแทน เราลองแล้วจริงด้วยการเรียก fetch พร้อม credentials: 'include' ไปที่ API ที่ตอบ * ผลคือ Chrome บล็อกพร้อมข้อความ
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.ส่วนคำขอแบบเดียวกันที่ไม่แนบ Credentials ผ่านปกติ ข้อสรุปคือ * ไม่ใช่ทางลัดสำหรับระบบที่มีล็อกอิน ถ้าต้องรองรับหลาย origin ให้เซิร์ฟเวอร์เก็บรายชื่อ origin ที่อนุญาตไว้ แล้วตอบกลับ origin ที่ตรงกับคำขอทีละตัว พร้อม Vary: Origin
สรุป: CORS error คืออะไร และต้องแก้ที่ไหน
CORS error คือเบราว์เซอร์บอกว่าหน้าเว็บจาก origin หนึ่งพยายามอ่านคำตอบจากอีก origin โดยที่เซิร์ฟเวอร์ปลายทางไม่ได้อนุญาตไว้ origin นับจาก Scheme, Host และ Port ครบทั้งสามส่วน ต่างกันแค่พอร์ตก็นับเป็นคนละที่ กฎนี้มีไว้ปกป้องผู้ใช้ และคำขอแบบธรรมดามักไปถึงเซิร์ฟเวอร์แล้วด้วยซ้ำ แค่โค้ดของเราไม่ได้รับคำตอบ
ทางแก้จึงมีสองทางที่ถูกต้อง คือตั้ง Access-Control-Allow-Origin และ Header ที่เกี่ยวข้องที่ฝั่งเซิร์ฟเวอร์ หรือใช้ server.proxy ของ Vite ระหว่างพัฒนา ครั้งหน้าที่เจอข้อความสีแดงนี้ ลองเปิด Console อ่านเหตุผลท้ายประโยค แล้วยิง curl พร้อม Header Origin ดูว่าเซิร์ฟเวอร์ตอบ Header อะไรกลับมา แค่สองขั้นนี้ก็รู้แล้วว่าต้องไปแก้ตรงไหน ถ้าอยากทวนภาพใหญ่ว่าหน้าเว็บคุยกับเซิร์ฟเวอร์ยังไง ลองอ่าน การทำงานกับ HTTP Requests และ Responses ต่อได้
ติดตามบทความและเทคนิคสำหรับคนเริ่มต้นสายโปรแกรมเมอร์ได้ที่ช่องทางของเรา
Facebook: Superdev Academy Thailand — https://www.facebook.com/superdev.academy.th
YouTube: Superdev Academy Channel — https://www.youtube.com/@SuperdevAcademy
Instagram: @superdevacademy — https://www.instagram.com/superdevacademy/
TikTok: @superdevacademy — https://www.tiktok.com/@superdevacademy?lang=th-TH
FAQ: คำถามที่พบบ่อยเกี่ยวกับบทความนี้
รวมคำถามและคำตอบที่ช่วยให้คุณเข้าใจเนื้อหาในบทความนี้ได้ดียิ่งขึ้น