แก้ปัญหา CORS: เข้าใจ Preflight และตั้งค่าเซิร์ฟเวอร์ให้ถูกต้อง
คุณเปิดตัวส่วนหน้า (frontend) ใหม่ เปิดคอนโซล แล้วพบข้อผิดพลาด CORS สีแดงว่า request ถูก “บล็อกโดยนโยบาย CORS” API ทำงานได้ใน Apidog หรือ curl แต่เบราว์เซอร์ไม่ยอมส่ง response ให้ JavaScript ปัญหานี้น่าหงุดหงิด แต่ไม่ได้ลึกลับอย่างที่คิด
บทความนี้อธิบายการทำงานของ CORS, preflight request, ข้อผิดพลาดที่พบบ่อย 6 แบบ และตัวอย่างการตั้งค่าสำหรับ Express, Spring Boot และ Nginx รวมถึงวิธีดีบักโดยไม่ต้องพึ่งคอนโซลของเบราว์เซอร์
CORS คืออะไร
CORS ย่อมาจาก Cross-Origin Resource Sharing เบราว์เซอร์ใช้ same-origin policy เป็นค่าเริ่มต้น ดังนั้น JavaScript บน https://app.example.com จึงอ่าน response จาก https://api.example.com ไม่ได้ เพราะ scheme, host หรือ port แตกต่างกัน
CORS คือกลไกที่เซิร์ฟเวอร์ใช้เพื่ออนุญาตการเข้าถึงข้าม origin โดยดูรายละเอียดได้จาก เอกสาร MDN CORS และ Fetch specification
ประเด็นสำคัญมี 3 ข้อ:
-
เบราว์เซอร์เป็นผู้บังคับใช้ การเรียกจากเซิร์ฟเวอร์ถึงเซิร์ฟเวอร์,
curlและไคลเอนต์ API บนเดสก์ท็อปจะไม่ตรวจสอบ CORS - เซิร์ฟเวอร์เป็นผู้กำหนดสิทธิ์ เบราว์เซอร์จะตัดสินจาก response headers ที่เซิร์ฟเวอร์ส่งกลับ
- request อาจไปถึงเซิร์ฟเวอร์แล้ว สำหรับ simple request เซิร์ฟเวอร์อาจประมวลผลและตอบกลับสำเร็จ แต่เบราว์เซอร์จะซ่อน response จาก JavaScript
CORS ไม่ใช่กำแพงป้องกัน API โดยตรง แต่ช่วยป้องกันเว็บไซต์อันตรายไม่ให้ใช้ข้อมูลรับรองของผู้ใช้เพื่ออ่านข้อมูลข้าม origin
ดังนั้นเมื่อพบ CORS error ให้ตรวจสอบ response headers และการตั้งค่าเซิร์ฟเวอร์ก่อน อย่าเริ่มจากการแก้โค้ด frontend หรือปิด CORS ในเบราว์เซอร์
Preflight request ทำงานอย่างไร
ก่อนส่ง cross-origin request บางประเภท เบราว์เซอร์จะส่ง request สำรวจแบบ OPTIONS เรียกว่า preflight
มักเกิดขึ้นเมื่อ request:
- ใช้เมธอดนอกเหนือจาก
GET,HEADหรือPOST - มี custom headers เช่น
Authorization - ใช้
Content-Type: application/json
ตัวอย่าง preflight request:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
เบราว์เซอร์กำลังถามว่า “หน้าเว็บจาก app.example.com สามารถส่ง POST พร้อม headers เหล่านี้ได้หรือไม่?” เซิร์ฟเวอร์ควรตอบกลับประมาณนี้:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
หาก response ขาด header หรือค่าไม่ตรงกัน เบราว์เซอร์จะยกเลิก request จริง ปลายทาง API จะไม่ถูกเรียก และ log จะเห็นเพียง OPTIONS
Access-Control-Max-Age กำหนดระยะเวลาที่เบราว์เซอร์จะ cache ผลการตรวจสอบ preflight ในตัวอย่างคือ 86,400 วินาที
เวลาแก้ปัญหา ให้ตอบคำถามนี้ก่อน:
Preflight ล้มเหลว หรือ request จริงล้มเหลว?
ข้อผิดพลาด CORS ที่พบบ่อย 6 แบบ
1. ไม่มี Access-Control-Allow-Origin
เซิร์ฟเวอร์ส่ง response โดยไม่มี CORS header เบราว์เซอร์จึงไม่อนุญาตให้ JavaScript อ่านข้อมูล
แก้ไขโดยระบุ origin ที่อนุญาต:
Access-Control-Allow-Origin: https://app.example.com
สำหรับ public API ที่ไม่ใช้ authentication อาจใช้:
Access-Control-Allow-Origin: *
อย่าลืมเพิ่ม CORS headers ให้ response ทุกสถานะ รวมถึง 401, 403 และ 500 ไม่ใช่เฉพาะ response สำเร็จ หาก API คืน 403 Forbidden แต่ไม่มี CORS headers เบราว์เซอร์อาจแสดง CORS error แทนข้อผิดพลาดจริงของเซิร์ฟเวอร์
2. ใช้ wildcard * ร่วมกับ credentials
ข้อความมักระบุว่า:
ค่าของ
Access-Control-Allow-Originต้องไม่ใช่ wildcard*เมื่อ credentials mode เป็นinclude
หาก frontend ส่งคุกกี้หรือ authentication headers พร้อม credentials: 'include' เซิร์ฟเวอร์จะใช้ Access-Control-Allow-Origin: * ไม่ได้ เพราะจะทำให้ทุกเว็บไซต์สามารถอ่าน response ที่ผ่านการตรวจสอบสิทธิ์
ให้ระบุ origin และเปิด credentials:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
ควรตรวจสอบ origin กับ allowlist ก่อนสะท้อนค่ากลับเสมอ อย่าสะท้อน origin ใด ๆ แบบไม่ตรวจสอบ
3. Preflight ไม่ผ่านการตรวจสอบ
เซิร์ฟเวอร์อาจ:
- ไม่รองรับ
OPTIONS - คืนค่า
404หรือ405 - ให้ authentication middleware ปฏิเสธด้วย
401
Preflight มักไม่มี token ดังนั้นควรจัดการ OPTIONS ก่อน authentication middleware:
app.options('/v1/orders', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
});
res.sendStatus(204);
});
ทางเลือกที่ง่ายกว่าคือใช้ มิดเดิลแวร์ cors สำหรับ Express และติดตั้งไว้ก่อน authentication middleware
4. ค่า header ไม่ตรงกับ origin
เซิร์ฟเวอร์อาจส่ง Access-Control-Allow-Origin แต่เป็นค่าที่ไม่ตรงกับ request เช่น:
- ตั้งค่า production ไว้ แต่ทดสอบจาก
http://localhost:5173 -
httpกับhttpsไม่ตรงกัน - มี trailing slash เช่น
https://app.example.com/
เปรียบเทียบ origin แบบตรงตัวและส่ง Vary: Origin:
const allowed = [
'https://app.example.com',
'http://localhost:5173'
];
if (allowed.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Vary', 'Origin');
}
scheme, host และ port ต้องตรงกันทั้งหมด
5. ไม่อนุญาต header หรือเมธอดที่ request ใช้
ตัวอย่างข้อความผิดพลาด:
-
authorizationไม่ได้รับอนุญาตโดยAccess-Control-Allow-Headers -
PUTไม่ได้รับอนุญาตโดยAccess-Control-Allow-Methods
หมายความว่า preflight ถูกส่งกลับมาแล้ว แต่ response ไม่ครอบคลุมสิ่งที่ frontend ต้องการ เช่น Authorization, X-Request-Id หรือ PUT
ขยาย allowlist ให้ครบ:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
ชื่อ headers ไม่คำนึงถึงตัวพิมพ์เล็กใหญ่ แต่ชื่อเมธอดคำนึงถึงตัวพิมพ์เล็กใหญ่และควรเขียนเป็นตัวพิมพ์ใหญ่
6. Preflight ถูก redirect
หาก preflight ได้รับ 301 หรือ 302 เบราว์เซอร์มักไม่ติดตาม redirect ให้ สาเหตุที่พบบ่อยคือ:
- URL
httpredirect ไปhttps - URL ขาด trailing slash
- gateway redirect
/v1/ordersไป/v1/orders/
ให้ชี้ frontend ไปยัง URL สุดท้ายโดยตรง ใช้ https ตั้งแต่ต้น และตรวจสอบรูปแบบ trailing slash ให้ตรงกับ router
ทดสอบด้วย OPTIONS โดยตรงและตรวจสอบว่า response เป็น 2xx ไม่ใช่ 3xx
ตัวอย่างการตั้งค่าเซิร์ฟเวอร์
Express
ใช้ มิดเดิลแวร์ cors แทนการเขียน headers เอง:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
}));
ติดตั้งก่อน authentication middleware เพื่อให้ preflight ไม่ถูกปฏิเสธเพราะไม่มี token
สำหรับ Flask สามารถใช้ ส่วนขยาย Flask-CORS ซึ่งจัดการตรรกะ headers ในลักษณะเดียวกัน
Spring Boot
ตั้งค่าแบบ global ผ่าน WebMvcConfigurer:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(86400);
}
}
หากใช้ Spring Security ให้เรียก .cors(Customizer.withDefaults()) ใน security filter chain ด้วย ไม่เช่นนั้น security layer อาจบล็อก preflight ก่อนที่ MVC configuration จะทำงาน
ดูตัวเลือกเพิ่มเติมใน เอกสาร Spring CORS
Nginx
หาก Nginx อยู่หน้าแอป ให้ตอบ preflight ที่ edge:
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
แฟล็ก always สำคัญ เพราะหากไม่มี Nginx จะไม่เพิ่ม headers ใน response 4xx และ 5xx
ควรเลือกให้มีเพียงเลเยอร์เดียวจัดการ CORS หากทั้ง Nginx และแอปเพิ่ม headers ซ้ำกัน เบราว์เซอร์อาจได้รับค่าเช่น:
Access-Control-Allow-Origin: *, *
และปฏิเสธ response
ดีบัก CORS นอกเบราว์เซอร์ด้วย Apidog
คอนโซลบอกเพียงว่าเบราว์เซอร์บล็อก response แต่ไม่ได้บอกว่าเซิร์ฟเวอร์ส่งอะไรกลับมา วิธีที่เร็วที่สุดคือทดสอบโดยนำเบราว์เซอร์ออกจากกระบวนการ
Apidog เป็นไคลเอนต์ API บนเดสก์ท็อป จึงไม่ถูกบังคับใช้ CORS แบบหน้าเว็บในเบราว์เซอร์ หาก request สำเร็จใน Apidog แสดงว่า API ทำงานได้ และปัญหาอยู่ที่ CORS headers หากล้มเหลวใน Apidog ด้วย ปัญหาอาจเป็นบั๊กของ API ทั่วไป
ใช้ขั้นตอนนี้:
จำลอง request จริง
คัดลอกจากแท็บ Network แล้วสร้าง request เดิมใน Apidog โดยใช้เมธอด, headers และ body เดียวกัน หากได้500แสดงว่าไม่ใช่ปัญหา CORS เพียงอย่างเดียวทดสอบ preflight ด้วยตนเอง
สร้าง request แบบOPTIONSและเพิ่ม headers:
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
ตรวจสอบ response headers
ตรวจสอบAccess-Control-Allow-Origin,Access-Control-Allow-MethodsและAccess-Control-Allow-Headersให้ตรงกับสิ่งที่ frontend ต้องการยืนยันการแก้ไข
หลังแก้ server configuration ให้ส่ง requestOPTIONSเดิมซ้ำและตรวจสอบ headers ใหม่ ไม่จำเป็นต้อง deploy frontend หรือล้างแคชทันที
แนวทางนี้ช่วยแยกได้อย่างรวดเร็วว่าเป็นปัญหาของ API หรือการตั้งค่า CORS เช่นเดียวกับ เทคนิคการทดสอบ API ทั่วไป
CORS checklist ใน 30 วินาที
ก่อนรายงานบั๊ก ตรวจสอบสิ่งต่อไปนี้:
- Response ที่ล้มเหลวมี
Access-Control-Allow-Originหรือไม่? - ค่าตรงกับ origin ของหน้าเว็บแบบครบถ้วนหรือไม่: scheme, host, port และไม่มี trailing slash?
- ใช้คุกกี้หรือ authentication หรือไม่? ถ้าใช่ ต้องใช้ origin ที่ระบุชัดเจนและ
Access-Control-Allow-Credentials: true -
OPTIONSคืนค่า2xxพร้อมเมธอดและ headers ที่ request ต้องการหรือไม่? - URL ของ preflight มี redirect หรือไม่?
- Response
401,403และ500มี CORS headers เหมือน response สำเร็จหรือไม่?
ส่วนใหญ่ปัญหาจะอยู่ในหกข้อนี้ ทดสอบ OPTIONS ใน Apidog แก้ server configuration แล้วจึงกลับไปทดสอบ frontend
คำถามที่พบบ่อย
ทำไม CORS error เกิดเฉพาะในเบราว์เซอร์?
เพราะมีเพียงเบราว์เซอร์ที่บังคับใช้ same-origin policy และตรวจสอบ Access-Control-Allow-Origin สำหรับ cross-origin response
curl, backend services และไคลเอนต์เดสก์ท็อปไม่มีกฎนี้ หาก request สำเร็จทุกที่ยกเว้นเบราว์เซอร์ แสดงว่า API มักทำงานปกติ แต่เซิร์ฟเวอร์ขาดหรือตั้งค่า CORS headers ไม่ถูกต้อง
CORS ใช้กับ Postman หรือ Apidog หรือไม่?
ไม่ Postman และ Apidog เป็นแอปพลิเคชันเดสก์ท็อป ไม่ใช่เว็บเพจที่ทำงานใน sandbox ของเบราว์เซอร์ จึงแสดง raw response headers จากเซิร์ฟเวอร์โดยไม่ผ่าน CORS
ดังนั้น การทดสอบ Postman CORS ที่สำเร็จไม่ได้พิสูจน์ว่า request จะทำงานในเบราว์เซอร์ แต่ช่วยยืนยันได้ว่าเลเยอร์ API ยังทำงานอยู่
CORS error เป็นฟีเจอร์ความปลอดภัยหรือบั๊ก?
เป็นฟีเจอร์ความปลอดภัย เบราว์เซอร์กำลังป้องกันไม่ให้สคริปต์อ่านข้อมูลข้าม origin โดยไม่ได้รับอนุญาต
การปิด CORS ด้วยแฟล็กหรือส่วนขยายช่วยเพียงซ่อนปัญหาบนเครื่องของคุณ ผู้ใช้คนอื่นยังพบปัญหาเดิม ควรแก้ response headers ของเซิร์ฟเวอร์
ใช้ Access-Control-Allow-Origin: * ได้ทุกที่หรือไม่?
ใช้ได้กับ public API แบบอ่านอย่างเดียวที่ไม่มีคุกกี้หรือ authentication เท่านั้น
หากมี authentication ให้ใช้ origin allowlist, สะท้อนเฉพาะ origin ที่ตรงกัน และส่ง Vary: Origin เพื่อให้ shared cache แยก response ของแต่ละ origin
Top comments (0)