DEV Community

Cover image for วิธีแก้ไข CORS Error: ดีบัก Access-Control-Allow-Origin
Thanawat Wongchai
Thanawat Wongchai

Posted on Originally published at apidog.com

วิธีแก้ไข CORS Error: ดีบัก Access-Control-Allow-Origin

แก้ปัญหา CORS: เข้าใจ Preflight และตั้งค่าเซิร์ฟเวอร์ให้ถูกต้อง

คุณเปิดตัวส่วนหน้า (frontend) ใหม่ เปิดคอนโซล แล้วพบข้อผิดพลาด CORS สีแดงว่า request ถูก “บล็อกโดยนโยบาย CORS” API ทำงานได้ใน Apidog หรือ curl แต่เบราว์เซอร์ไม่ยอมส่ง response ให้ JavaScript ปัญหานี้น่าหงุดหงิด แต่ไม่ได้ลึกลับอย่างที่คิด

ลองใช้ Apidog วันนี้

บทความนี้อธิบายการทำงานของ 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
Enter fullscreen mode Exit fullscreen mode

เบราว์เซอร์กำลังถามว่า “หน้าเว็บจาก 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
Enter fullscreen mode Exit fullscreen mode

หาก 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
Enter fullscreen mode Exit fullscreen mode

สำหรับ public API ที่ไม่ใช้ authentication อาจใช้:

Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

อย่าลืมเพิ่ม 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
Enter fullscreen mode Exit fullscreen mode

ควรตรวจสอบ 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);
});
Enter fullscreen mode Exit fullscreen mode

ทางเลือกที่ง่ายกว่าคือใช้ มิดเดิลแวร์ 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');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

ชื่อ headers ไม่คำนึงถึงตัวพิมพ์เล็กใหญ่ แต่ชื่อเมธอดคำนึงถึงตัวพิมพ์เล็กใหญ่และควรเขียนเป็นตัวพิมพ์ใหญ่

6. Preflight ถูก redirect

หาก preflight ได้รับ 301 หรือ 302 เบราว์เซอร์มักไม่ติดตาม redirect ให้ สาเหตุที่พบบ่อยคือ:

  • URL http redirect ไป 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
}));
Enter fullscreen mode Exit fullscreen mode

ติดตั้งก่อน 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

หากใช้ 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;
}
Enter fullscreen mode Exit fullscreen mode

แฟล็ก always สำคัญ เพราะหากไม่มี Nginx จะไม่เพิ่ม headers ใน response 4xx และ 5xx

ควรเลือกให้มีเพียงเลเยอร์เดียวจัดการ CORS หากทั้ง Nginx และแอปเพิ่ม headers ซ้ำกัน เบราว์เซอร์อาจได้รับค่าเช่น:

Access-Control-Allow-Origin: *, *
Enter fullscreen mode Exit fullscreen mode

และปฏิเสธ response

ดีบัก CORS นอกเบราว์เซอร์ด้วย Apidog

คอนโซลบอกเพียงว่าเบราว์เซอร์บล็อก response แต่ไม่ได้บอกว่าเซิร์ฟเวอร์ส่งอะไรกลับมา วิธีที่เร็วที่สุดคือทดสอบโดยนำเบราว์เซอร์ออกจากกระบวนการ

Apidog เป็นไคลเอนต์ API บนเดสก์ท็อป จึงไม่ถูกบังคับใช้ CORS แบบหน้าเว็บในเบราว์เซอร์ หาก request สำเร็จใน Apidog แสดงว่า API ทำงานได้ และปัญหาอยู่ที่ CORS headers หากล้มเหลวใน Apidog ด้วย ปัญหาอาจเป็นบั๊กของ API ทั่วไป

ใช้ขั้นตอนนี้:

  1. จำลอง request จริง

    คัดลอกจากแท็บ Network แล้วสร้าง request เดิมใน Apidog โดยใช้เมธอด, headers และ body เดียวกัน หากได้ 500 แสดงว่าไม่ใช่ปัญหา CORS เพียงอย่างเดียว

  2. ทดสอบ preflight ด้วยตนเอง

    สร้าง request แบบ OPTIONS และเพิ่ม headers:

   Origin: https://app.example.com
   Access-Control-Request-Method: POST
   Access-Control-Request-Headers: authorization, content-type
Enter fullscreen mode Exit fullscreen mode
  1. ตรวจสอบ response headers

    ตรวจสอบ Access-Control-Allow-Origin, Access-Control-Allow-Methods และ Access-Control-Allow-Headers ให้ตรงกับสิ่งที่ frontend ต้องการ

  2. ยืนยันการแก้ไข

    หลังแก้ server configuration ให้ส่ง request OPTIONS เดิมซ้ำและตรวจสอบ 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)