วิธีแก้ error CORS (และทำไมมันถึงเกิดขึ้น)
error CORS หมายความว่าหน้าเว็บของคุณพยายามอ่านการตอบกลับจาก origin อื่น (โดเมน พอร์ต หรือโปรโตคอลที่ต่างกัน) และเซิร์ฟเวอร์นั้นไม่ได้ส่ง header ที่บอกว่าอนุญาต เบราว์เซอร์จึงบล็อกการตอบกลับเพื่อปกป้องผู้ใช้ คุณแก้ได้ที่เซิร์ฟเวอร์ โดยส่งคืน header Access-Control-Allow-Origin สำหรับ origin ของเว็บไซต์ และจัดการคำขอ preflight แบบ OPTIONS หรือหลีกเลี่ยงได้โดยส่งคำขอผ่าน back end ของคุณเองบน origin เดียวกัน
เป็นหนึ่งใน error ที่สับสนที่สุดในการพัฒนาเว็บ เพราะคำขอมักไปถึงเซิร์ฟเวอร์ได้ปกติ คู่มือนี้อธิบายว่าเกิดอะไรขึ้นและวิธีแก้แต่ละรูปแบบ
"origin" หมายความว่าอะไร
origin คือการรวมกันของโปรโตคอล โดเมน และพอร์ต:
| URL | Origin |
|---|---|
https://myapp.com/dashboard | https://myapp.com |
https://api.myapp.com/users | https://api.myapp.com (ซับโดเมนต่างกัน จึงเป็น origin ต่างกัน) |
http://localhost:5173 | http://localhost:5173 |
http://localhost:3000 | http://localhost:3000 (พอร์ตต่างกัน จึงเป็น origin ต่างกัน) |
เบราว์เซอร์ทำตาม same-origin policy: JavaScript ในหน้าอ่านการตอบกลับจาก origin ของตัวเองได้อย่างอิสระ แต่อ่านจาก origin อื่นไม่ได้ CORS (Cross-Origin Resource Sharing) คือระบบที่ให้เซิร์ฟเวอร์เลือกเปิดรับ: "การตอบกลับจากฉันอนุญาตให้หน้าจาก origin เหล่านี้อ่านได้"
มีสองผลที่ทำให้หลายคนประหลาดใจ:
- คำขอมักไปถึงเซิร์ฟเวอร์ CORS มักขวางโค้ดของคุณไม่ให้ อ่าน การตอบกลับ ไม่ได้ขวางเซิร์ฟเวอร์ไม่ให้รับคำขอ POST อาจถูกบันทึกแล้วแม้คุณจะเห็น error
- มีเพียงเบราว์เซอร์เท่านั้นที่บังคับใช้ curl, Postman และโค้ดฝั่งเซิร์ฟเวอร์จะไม่สนใจ CORS เลย นั่นคือเหตุผลที่ "ใช้ได้ใน Postman" ไม่บอกอะไรเกี่ยวกับ CORS
ขั้นที่ 1: อ่าน error ตรงตามที่แสดง
เปิดเครื่องมือสำหรับนักพัฒนา (คลิกขวา → Inspect) ไปที่ Console แล้วอ่านข้อความเต็ม ข้อความของ Chrome บอกว่าคุณมีปัญหาแบบไหน:
| ข้อความมีคำว่า | สิ่งที่ผิด |
|---|---|
No 'Access-Control-Allow-Origin' header is present | เซิร์ฟเวอร์ไม่ได้ส่ง header เลย หรือคำขอล้มเหลวก่อนถึงโค้ด CORS ของคุณ (เช่น 404 หรือ 500) |
The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin | เซิร์ฟเวอร์อนุญาต origin อื่นที่ไม่ใช่ของคุณ |
Response to preflight request doesn't pass access control check | คำขอ OPTIONS แบบ preflight ล้มเหลว |
...must not be the wildcard '*' when the request's credentials mode is 'include' | คุณส่งคุกกี้ และเซิร์ฟเวอร์ตอบด้วย * |
Request header field authorization is not allowed | เซิร์ฟเวอร์ไม่ได้ระบุ header นั้นใน Access-Control-Allow-Headers |
จากนั้นเปิดแท็บ Network หาคำขอที่ล้มเหลว (และคำขอ OPTIONS ก่อนหน้า) และดูสถานะ HTTP กับ header ของการตอบกลับ
ตรวจสถานะก่อน "error CORS" หลายครั้งเป็น error ฝั่งเซิร์ฟเวอร์ที่ปลอมตัวมา ถ้าเซิร์ฟเวอร์ crash หรือส่ง 404 การตอบกลับ error ของมักไม่มี header CORS เบราว์เซอร์จึงรายงานว่าเป็น CORS แทนที่จะเป็นปัญหาจริง แก้ 500 หรือ 404 ก่อน error CORS อาจหายไปเอง
ขั้นที่ 2: ตัดสินใจว่าควรเรียก API นั้นจากเบราว์เซอร์หรือไม่
ก่อนเพิ่ม header ให้ถามว่าเบราว์เซอร์ควรเรียก API นี้โดยตรงหรือไม่
- API ของบุคคลที่สามที่ต้องใช้คีย์ลับ (OpenAI, Stripe secret key, บริการอีเมลส่วนใหญ่) ไม่ควรเรียกจากเบราว์เซอร์เลย เพราะคีย์จะมองเห็นได้โดยผู้เยี่ยมชมทุกคน ให้เรียกจาก back end แทน และปัญหา CORS จะหายไป เพราะคำขอระหว่างเซิร์ฟเวอร์ไม่อยู่ภายใต้ CORS ดูวิธีรักษาความปลอดภัยของ API key
- API ของบุคคลที่สามที่คุณไม่ได้ควบคุม ที่ไม่ส่ง header CORS แก้จากฝั่งคุณไม่ได้ ให้เรียกจาก back end ของคุณ
- API ของคุณเอง แก้ได้โดยตั้งค่าให้ถูกต้อง ดำเนินการต่อด้านล่าง
ขั้นที่ 3: แก้ที่เซิร์ฟเวอร์ของคุณ
เซิร์ฟเวอร์ต้องเพิ่ม header ที่ถูกต้องในการตอบกลับ รวมถึงการตอบกลับ error
สำหรับคำขอจริง ส่ง:
Access-Control-Allow-Origin: https://myapp.com(origin ตรงของ frontend ของคุณ ไม่มี slash ท้าย)Vary: Originถ้าค่าขึ้นอยู่กับ origin ที่ขอ
สำหรับคำขอ preflight OPTIONS ตอบด้วย status สำเร็จ (200 หรือ 204) และ:
Access-Control-Allow-Origin: https://myapp.comAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETEAccess-Control-Allow-Headers: Content-Type, Authorization(ทุก header แบบกำหนดเองที่คุณส่ง)- ตัวเลือก
Access-Control-Max-Age: 600เพื่อให้เบราว์เซอร์แคช preflight ไว้สักพัก
เฟรมเวิร์กส่วนใหญ่มีแพ็กเกจหรือการตั้งค่าที่ทำเรื่องนี้ได้ ใน Express คือแพ็กเกจ cors ใน FastAPI คือ CORSMiddleware ใน ASP.NET Core คือ AddCors ใน Spring คือ @CrossOrigin หรือการตั้งค่า CORS ตัวอย่างการตั้งค่า Express ทั่วไป:
import cors from "cors";
app.use(cors({
origin: ["https://myapp.com", "http://localhost:5173"],
credentials: true,
}));
วางการจัดการ CORS ก่อน route และการยืนยันตัวตน เพื่อให้คำขอ preflight (ซึ่งไม่มีการล็อกอิน) ไม่ถูกปฏิเสธด้วย 401
กับเครื่องมือสร้างด้วย AI ให้อธิบายการตั้งค่าอย่างละเอียด:
frontend ที่
https://myapp.comเรียก API ที่https://api.myapp.comและได้ "Response to preflight request doesn't pass access control check" ตั้งค่า CORS บน API ให้อนุญาตhttps://myapp.comและ origin ของพรีวิวเท่านั้น อนุญาต GET, POST, PATCH, DELETE พร้อม header Content-Type และ Authorization จัดการ OPTIONS ก่อนการยืนยันตัวตน และใส่ header CORS ในการตอบกลับ error ด้วย
ขั้นที่ 4: จัดการคุกกี้และ credentials
ถ้า frontend ของคุณส่งคุกกี้ (เช่น session การล็อกอิน) ไปยัง origin อื่น ทั้งสองฝั่งต้องตกลงกัน:
- frontend ตั้ง
credentials: "include"ในfetchหรือwithCredentials: trueใน axios - เซิร์ฟเวอร์ส่ง
Access-Control-Allow-Credentials: true Access-Control-Allow-Originของเซิร์ฟเวอร์ต้องเป็น origin ตรง ไม่ใช่*
คุกกี้ข้ามไซต์ยังต้องใช้ SameSite=None; Secure บนคุกกี้ และบางเบราว์เซอร์จำกัดคุกกี้ของบุคคลที่สามเพิ่มเติม นั่นเป็นอีกเหตุผลหนึ่งที่ควรเลือกวิธีในขั้นที่ 5
ขั้นที่ 5: หลีกเลี่ยง CORS ด้วยการตั้งค่า origin เดียวกัน
บ่อยครั้งวิธีแก้ที่สะอาดที่สุดคือไม่ทำคำขอข้าม origin เลย ให้บริการ API จาก origin เดียวกับหน้า เช่น https://myapp.com/api/...:
- ในการพัฒนา เซิร์ฟเวอร์พัฒนาส่วนใหญ่ทำพร็อกซีได้ การตั้งค่า
server.proxyของ Vite ส่งต่อ/apiไปยัง back end บนพอร์ตอื่นได้ เบราว์เซอร์จึงคุยกับ origin เดียวเท่านั้น - ในโปรดักชัน route
/apiไปยัง back end ผ่านการตั้งค่า rewrite หรือ proxy ของโฮสต์ หรือให้บริการ frontend และ API จากเซิร์ฟเวอร์เดียวกัน
จากนั้น frontend เรียกพาธแบบ relative เช่น fetch("/api/orders") ไม่มีคำขอข้าม origin ไม่มี preflight ข้าม origin และไม่มีปัญหาเรื่องคุกกี้ นอกจากนี้ยังหมายความว่าคุณไม่ต้องฝัง API domain ที่ต่างกันระหว่างพรีวิวกับโปรดักชัน
เปลี่ยน frontend ให้เรียก back end ด้วยพาธ /api แบบ relative แทน URL เต็ม และตั้ง dev proxy ให้ /api ส่งต่อไปยังเซิร์ฟเวอร์ API จากนั้นลบการตั้งค่า CORS ที่ไม่จำเป็นแล้วออก
สิ่งที่ไม่ควรทำ
mode: "no-cors"ในfetchมันไม่ได้ข้ามอะไรเลย แต่ให้โค้ดของคุณได้การตอบกลับที่ว่างเปล่าและอ่านไม่ได้- ส่วนเสริมของเบราว์เซอร์ที่ปิด CORS มันเปลี่ยนแค่เบราว์เซอร์ของคุณ ผู้เยี่ยมชมทุกคนยังเห็น error
- บริการ "CORS proxy" สาธารณะ สำหรับแอปจริง เพราะมันอ่านและเปลี่ยนทุกอย่างที่ผ่านมันได้ และมันล่มได้
Access-Control-Allow-Origin: *บน API ที่ใช้การล็อกอิน มันใช้กับ credentials ไม่ได้อยู่แล้ว และเป็นค่าเริ่มต้นที่ผิดสำหรับข้อมูลส่วนตัว- สะท้อน
Originใด ๆ กลับไปพร้อมอนุญาต credentials ทำให้เว็บไซต์ใดก็ได้ส่งคำขอที่ล็อกอินแล้วในนามของผู้ใช้ของคุณ ให้ใช้ allowlist
สาเหตุที่พบบ่อยในแอปที่สร้างด้วย AI
| สถานการณ์ | วิธีแก้ |
|---|---|
| frontend เรียก API ของบุคคลที่สามด้วยคีย์ลับ | ย้ายการเรียกไปยัง back end |
| ทำงานในพรีวิว แต่ล้มเหลวบนโดเมนที่เผยแพร่ | เพิ่ม origin ของโดเมนที่เผยแพร่และโดเมนที่กำหนดเองลงใน allowlist หรือใช้พาธ /api แบบ relative |
| error เกิดเฉพาะกับ POST, PUT หรือ DELETE | ไม่ได้จัดการ preflight เพิ่มการจัดการ OPTIONS และ header ที่อนุญาต |
| error เกิดหลังจากเพิ่มการล็อกอิน | credentials ต้องใช้ origin ตรงและ Allow-Credentials |
| "error CORS" แต่แท็บ Network แสดง 500 | แก้ error ของเซิร์ฟเวอร์ก่อน |
URL ของ API ถูกฝังเป็น localhost | ใช้พาธ relative หรือการตั้งค่าต่อสภาพแวดล้อม |
CORS ใน Mythex
เมื่อโปรเจกต์ Mythex มีเว็บไซต์และเซิร์ฟเวอร์ API ทั้งสองจะรันเป็นบริการในโปรเจกต์เดียวกัน และคุณสลับระหว่างกันในพรีวิวได้ การขอให้เอเจนต์ทำให้ frontend เรียก back end ผ่านพาธ /api แบบ relative โดยพร็อกซีในการพัฒนาจะหลีกเลี่ยงปัญหา CORS ส่วนใหญ่ ถ้าคุณจำเป็นต้องเรียกข้าม origin จริง ๆ ให้ส่ง error ที่แน่นอนจาก console และ origin ที่เกี่ยวข้องให้เอเจนต์ และตรวจผลลัพธ์บนแอปที่เผยแพร่ด้วย เพราะที่อยู่ของมันต่างจากพรีวิว ดูMulti-service apps และAPI backends ในเอกสาร และคู่มือของเราเรื่องการดีบักแอปที่สร้างด้วย AI และfrontend กับ backend
เช็กลิสต์ CORS
- อ่าน error เต็มใน console และ status code ของคำขอที่ล้มเหลว
- ตัดความเป็นไปได้ของ 404 หรือ 500 ที่ซ่อนอยู่ออกแล้ว
- API ที่ใช้คีย์ลับถูกเรียกจาก back end ไม่ใช่เบราว์เซอร์
- เซิร์ฟเวอร์ส่ง
Access-Control-Allow-Originพร้อม origin ที่อนุญาตแบบตรง - ตอบ preflight
OPTIONSด้วย method และ header ที่อนุญาต ก่อนการยืนยันตัวตน - มี header CORS ในการตอบกลับ error ด้วย
- credentials: origin ตรง,
Allow-Credentials: trueและการตั้งค่าคุกกี้ที่ถูกต้อง - ครอบคลุม origin ของพรีวิว โปรดักชัน และโดเมนที่กำหนดเอง หรือใช้พาธ
/apiแบบ relative - ไม่มี
no-corsพร็อกซีสาธารณะ หรือทางลัดที่สะท้อนทุก origin
คำถาม
error CORS หมายความว่าอะไร?
หน้าของคุณที่โหลดจาก origin หนึ่งพยายามอ่านการตอบกลับจาก origin อื่น และเซิร์ฟเวอร์นั้นไม่ได้ส่ง header ที่อนุญาตให้ทำได้ เบราว์เซอร์จึงบล็อกไม่ให้โค้ดของคุณอ่านการตอบกลับ วิธีแก้อยู่ที่เซิร์ฟเวอร์ หรือวิธีที่คำขอถูก route ไม่ใช่ในโค้ด frontend
แก้ error CORS จาก frontend ได้ไหม?
ไม่ได้จริง ๆ การตั้งค่าอย่าง mode: 'no-cors' เพียงซ่อนการตอบกลับจากโค้ดของคุณ วิธีที่ถูกต้องคือให้เซิร์ฟเวอร์ส่ง header Access-Control-Allow-Origin ที่ถูกต้อง หรือเรียก API จาก back end ของคุณเองหรือพร็อกซีที่อยู่ origin เดียวกัน
Access-Control-Allow-Origin: * ปลอดภัยไหม?
ใช้ได้กับข้อมูลสาธารณะที่ใครก็อ่านได้ เช่น API สาธารณะที่ไม่ต้องล็อกอิน แต่ใช้กับคุกกี้หรือ credential อื่นไม่ได้ และสำหรับ API ส่วนตัวควรระบุ origin ที่อนุญาตให้ชัดเจน
ทำไมคำขอทำงานใน Postman หรือ curl แต่ไม่ทำงานในเบราว์เซอร์?
CORS ถูกบังคับใช้โดยเบราว์เซอร์เท่านั้น เครื่องมืออย่าง Postman และ curl ไม่ได้บังคับใช้ ดังนั้นคำขอจึงสำเร็จที่นั่นได้ และยังถูกบล็อกเมื่อ JavaScript ของหน้าส่งคำขอเอง
preflight request คืออะไร?
สำหรับคำขอที่ไม่ใช่แบบง่าย เช่น POST ที่เป็น JSON หรือคำขอที่มี header Authorization เบราว์เซอร์จะส่งคำขอ OPTIONS ก่อน เพื่อถามเซิร์ฟเวอร์ว่าอนุญาต method และ header ใดบ้าง ถ้าคำขอ OPTIONS นั้นล้มเหลวหรือไม่มี header ที่ถูกต้อง คำขอจริงจะไม่ถูกส่งออกไปเลย