[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"academy-blogs-th-1-1-all-cors-error-explained-how-to-fix-all--*":3,"academy-blog-faqs-kf88oxcm9qxulws-th":93,"academy-blog-translations-kf88oxcm9qxulws":118},{"data":4,"page":92,"perPage":92,"totalItems":92,"totalPages":92},[5],{"alt":6,"collectionId":7,"collectionName":8,"content":9,"cover_image":10,"cover_image_l_url":11,"cover_image_m_url":12,"cover_image_path":13,"cover_image_s_url":14,"created":15,"created_by":16,"expand":17,"id":85,"keywords":86,"locale":55,"published_at":87,"scheduled_at":72,"school_blog":81,"short_description":88,"status":79,"title":89,"updated":90,"updated_by":91,"slug":82,"views":84},"ปกบทความ CORS error คืออะไร ภาพทางเข้าอาคารยามเย็นมองจากด้านนอก มีประตูกั้นกระจกรักษาความปลอดภัยและเคาน์เตอร์ยามว่างเปล่า แทนเบราว์เซอร์ที่ตรวจ origin พร้อมแผงข้อมูลคนละ origin: หน้าเว็บ http:\u002F\u002Flocalhost:5173, API http:\u002F\u002Flocalhost:3000 และ Header Access-Control-Allow-Origin","sclblg987654321","school_blog_translations","\u003Cp>ลองนึกภาพว่าคุณเพิ่งเขียนหน้าเว็บแรกด้วย Vite เสร็จ เปิดที่ \u003Ccode>http:\u002F\u002Flocalhost:5173\u003C\u002Fcode> แล้วสั่ง \u003Ccode>fetch\u003C\u002Fcode> ไปดึงรายชื่อผู้ใช้จาก API ที่เพื่อนในทีมเปิดไว้ที่ \u003Ccode>http:\u002F\u002Flocalhost:3000\u003C\u002Fcode> ลองยิงผ่าน curl แล้วได้ข้อมูลครบ แต่พอเรียกจากหน้าเว็บ หน้าจอกลับว่างเปล่า เปิด Console ขึ้นมาเจอข้อความสีแดงยาวหนึ่งบรรทัด มีคำว่า CORS policy อยู่ตรงกลาง\u003C\u002Fp>\u003Cp>ไม่ต้องตกใจ และไม่ได้แปลกเลย เกือบทุกคนที่เริ่มต่อหน้าเว็บเข้ากับ API จะติด CORS ครั้งแรกในจังหวะแบบนี้พอดี ที่ทำให้งงคือโค้ดดูไม่มีอะไรผิด และเซิร์ฟเวอร์ก็ทำงานปกติ วันนี้ Superdev Academy จะพาไปดูว่า CORS error คืออะไร ทำไมเบราว์เซอร์ถึงบล็อก คำขอไปถึงเซิร์ฟเวอร์หรือยัง และที่สำคัญที่สุดคือต้องแก้ที่ไหน เพราะคำตอบไม่ได้อยู่ในโค้ดหน้าเว็บอย่างที่หลายคนคิด\u003C\u002Fp>\u003Ch2>ข้อความสีแดงนี้บอกอะไรเราบ้าง\u003C\u002Fh2>\u003Cp>ทีมเราจำลองสถานการณ์ข้างบนขึ้นมาจริงบนเครื่อง ด้วยเซิร์ฟเวอร์ Node.js สองตัว ตัวหนึ่งเสิร์ฟหน้าเว็บที่พอร์ต 5173 อีกตัวเป็น API ที่พอร์ต 3000 ซึ่งยังไม่ได้ตั้งค่าอะไรเรื่อง CORS เลย แล้วเปิดหน้าเว็บด้วย Google Chrome ข้อความที่ได้ใน Console คือบรรทัดนี้\u003C\u002Fp>\u003Cpre>\u003Ccode>Access to fetch at 'http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fusers' from origin 'http:\u002F\u002Flocalhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>อ่านทีละท่อนจะเห็นว่ามันบอกครบทุกอย่างแล้ว ท่อนแรกบอกว่าคำขอวิ่งไปที่ไหน ท่อนที่สองบอกว่าหน้าเว็บของเรามาจาก origin ไหน และท่อนสุดท้ายบอกเหตุผลว่า คำตอบจากเซิร์ฟเวอร์ไม่มี Header ชื่อ \u003Ccode>Access-Control-Allow-Origin\u003C\u002Fcode> ติดมาด้วย\u003C\u002Fp>\u003Cp>ส่วนในโค้ด JavaScript ของเรา สิ่งที่ \u003Ccode>catch\u003C\u002Fcode> จับได้มีแค่ \u003Ccode>TypeError: Failed to fetch\u003C\u002Fcode> ไม่มีรายละเอียดอื่นเลย เรื่องนี้ไม่ใช่บั๊ก \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FGuides\u002FCORS\">MDN อธิบายไว้ในคู่มือ CORS\u003C\u002Fa> ว่ารายละเอียดของความผิดพลาดแบบนี้ถูกซ่อนจาก JavaScript โดยตั้งใจเพื่อความปลอดภัย โค้ดรู้แค่ว่ามี error เกิดขึ้น ทางเดียวที่จะรู้ว่าพังเพราะอะไรคือเปิดดูใน Console ของเบราว์เซอร์ นี่คือเหตุผลที่เวลาเจอ \u003Ccode>Failed to fetch\u003C\u002Fcode> เราควรเปิด Console ก่อนเสมอ ก่อนจะไปไล่แก้โค้ด\u003C\u002Fp>\u003Ch2>Origin คืออะไร ทำไม localhost กับ localhost ถึงนับเป็นคนละที่\u003C\u002Fh2>\u003Cp>คำที่โผล่ในข้อความ error บ่อยที่สุดคือ origin ซึ่ง \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FGlossary\u002FOrigin\">MDN นิยามไว้\u003C\u002Fa> ว่าประกอบด้วยสามส่วนของ URL คือ Scheme (โปรโตคอล เช่น \u003Ccode>http\u003C\u002Fcode> หรือ \u003Ccode>https\u003C\u002Fcode>), Host (ชื่อโดเมน) และ Port สองที่จะนับเป็น origin เดียวกันก็ต่อเมื่อตรงกันครบทั้งสามส่วน ส่วน path ด้านหลังไม่มีผล\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Ccode>http:\u002F\u002Flocalhost:5173\u003C\u002Fcode> กับ \u003Ccode>http:\u002F\u002Flocalhost:3000\u003C\u002Fcode>: คนละ origin เพราะพอร์ตต่างกัน\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>http:\u002F\u002Fexample.com\u003C\u002Fcode> กับ \u003Ccode>https:\u002F\u002Fexample.com\u003C\u002Fcode>: คนละ origin เพราะ Scheme ต่างกัน\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>http:\u002F\u002Fexample.com\u003C\u002Fcode> กับ \u003Ccode>http:\u002F\u002Fwww.example.com\u003C\u002Fcode>: คนละ origin เพราะ Host ต่างกัน\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>http:\u002F\u002Fexample.com\u002Fapp1\u003C\u002Fcode> กับ \u003Ccode>http:\u002F\u002Fexample.com\u002Fapp2\u003C\u002Fcode>: origin เดียวกัน เพราะต่างกันแค่ path\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Cp>พูดง่ายๆ ถึงหน้าเว็บกับ API จะรันอยู่บนเครื่องเดียวกัน ใช้ชื่อ localhost เหมือนกัน แต่ถ้าพอร์ตไม่ตรงกัน เบราว์เซอร์ก็มองว่าเป็นคนละบ้านแล้ว ซึ่งตอนพัฒนาเว็บแทบจะเป็นแบบนี้เสมอ เพราะ Vite ใช้พอร์ต 5173 เป็นค่าเริ่มต้น ส่วน API มักจะรันอีกพอร์ตหนึ่ง\u003C\u002Fp>\u003Ch2>Same-Origin Policy: เบราว์เซอร์กำลังปกป้องใครอยู่\u003C\u002Fh2>\u003Cp>กฎที่อยู่เบื้องหลังข้อความนี้ชื่อ Same-Origin Policy ตาม\u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FSecurity\u002FSame-origin_policy\">คำอธิบายของ MDN\u003C\u002Fa> มันคือกลไกความปลอดภัยที่จำกัดว่าเอกสารหรือสคริปต์จาก origin หนึ่ง จะยุ่งกับข้อมูลของอีก origin ได้แค่ไหน โดยทั่วไปการอ่านข้อมูลข้าม origin จะถูกห้ามไว้ก่อน\u003C\u002Fp>\u003Cp>ตัวอย่างที่ MDN ยกไว้ช่วยให้เห็นภาพชัดมาก ลองนึกว่าคุณล็อกอินเว็บเมลค้างไว้ในเบราว์เซอร์ แล้วบังเอิญเปิดเว็บแปลกๆ อีกแท็บ ถ้าไม่มีกฎนี้ สคริปต์ในเว็บแปลกๆ นั้นก็สั่ง fetch ไปที่เว็บเมล แล้วอ่านอีเมลของคุณกลับไปได้ทันที เพราะเบราว์เซอร์แนบ Cookie ของคุณไปด้วย\u003C\u002Fp>\u003Cblockquote>\u003Cp>CORS error ไม่ได้มีไว้กันเรา มันมีไว้กันคนอื่นไม่ให้อ่านข้อมูลของผู้ใช้ผ่านเบราว์เซอร์ของผู้ใช้เอง\u003C\u002Fp>\u003C\u002Fblockquote>\u003Cp>ส่วน CORS ที่ย่อมาจาก Cross-Origin Resource Sharing คือช่องทางให้เซิร์ฟเวอร์ประกาศผ่าน HTTP Header ว่ายอมให้ origin ไหนบ้างเข้ามาอ่านข้อมูลได้ สังเกตว่าคนที่ประกาศคือเซิร์ฟเวอร์ ไม่ใช่หน้าเว็บ ประโยคนี้คือหัวใจของการแก้ปัญหาทั้งหมดในบทความนี้\u003C\u002Fp>\u003Ch2>คำขอไปถึงเซิร์ฟเวอร์แล้ว แต่เบราว์เซอร์ไม่ยอมส่งคำตอบให้โค้ด\u003C\u002Fh2>\u003Cp>จุดที่มือใหม่เข้าใจผิดบ่อยที่สุดคือคิดว่าเบราว์เซอร์บล็อกคำขอตั้งแต่ยังไม่ออกจากเครื่อง ในการทดลองของเรา Log ฝั่ง API บันทึกไว้แบบนี้\u003C\u002Fp>\u003Cpre>\u003Ccode>[api] GET \u002Fapi\u002Fusers Origin=http:\u002F\u002Flocalhost:5173\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>แปลว่าคำขอ GET วิ่งไปถึงเซิร์ฟเวอร์จริง เซิร์ฟเวอร์ก็ตอบกลับข้อมูลครบ แต่เบราว์เซอร์ตรวจแล้วไม่เจอ Header ที่อนุญาต origin ของเรา จึงไม่ยอมส่งคำตอบนั้นให้โค้ด JavaScript อ่าน ปัญหาจึงอยู่ที่ขั้นตอน \"อ่านคำตอบ\" ไม่ใช่ขั้นตอน \"ส่งคำขอ\"\u003C\u002Fp>\u003Cp>ข้อนี้ยังอธิบายอีกเรื่องที่ทำให้หลายคนงง คือทำไมยิงด้วย curl หรือ Postman แล้วได้ข้อมูลปกติ เราลองยิงไปที่ API ตัวเดิมด้วยคำสั่งนี้\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">curl -i -H \"Origin: http:\u002F\u002Flocalhost:5173\" http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fusers\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>ผลคือได้ \u003Ccode>HTTP\u002F1.1 200 OK\u003C\u002Fcode> พร้อมข้อมูล JSON ครบ ทั้งที่ไม่มี Header ของ CORS สักตัว เพราะ CORS เป็นกฎที่เบราว์เซอร์บังคับใช้ curl กับ Postman ไม่ได้บังคับ ดังนั้นการที่ curl ผ่านไม่ได้พิสูจน์ว่าเบราว์เซอร์จะผ่าน ถ้าอยากทวนเรื่อง Request, Response และ Status Code ก่อนไปต่อ ลองอ่าน \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.superdevacademy.com\u002Fblogs\u002Fhttp-status-codes-explained\">HTTP Status Code มีอะไรบ้าง\u003C\u002Fa> ประกอบได้\u003C\u002Fp>\u003Ch2>Preflight คืออะไร ทำไมบางครั้งเจอคำขอ OPTIONS โผล่มาก่อน\u003C\u002Fh2>\u003Cp>ถ้าเปิดแท็บ Network ดูแล้วเห็นคำขอ method \u003Ccode>OPTIONS\u003C\u002Fcode> วิ่งไปก่อนคำขอจริง นั่นคือ Preflight Request หรือคำขอที่เบราว์เซอร์ส่งไปถามเซิร์ฟเวอร์ก่อนว่า \"ถ้าจะส่งแบบนี้ รับไหม\"\u003C\u002Fp>\u003Cp>ตามคู่มือ CORS ของ MDN คำขอที่ไม่ต้องมี Preflight เรียกว่า Simple Request ซึ่งต้องเข้าเงื่อนไขครบ เช่น method เป็น \u003Ccode>GET\u003C\u002Fcode>, \u003Ccode>HEAD\u003C\u002Fcode> หรือ \u003Ccode>POST\u003C\u002Fcode> เท่านั้น และถ้ามี \u003Ccode>Content-Type\u003C\u002Fcode> ต้องเป็นหนึ่งในสามค่านี้ \u003Ccode>application\u002Fx-www-form-urlencoded\u003C\u002Fcode>, \u003Ccode>multipart\u002Fform-data\u003C\u002Fcode> หรือ \u003Ccode>text\u002Fplain\u003C\u002Fcode> เมื่อไหร่ที่หลุดเงื่อนไข เช่น ส่ง \u003Ccode>POST\u003C\u002Fcode> พร้อม \u003Ccode>Content-Type: application\u002Fjson\u003C\u002Fcode> หรือแนบ Header \u003Ccode>Authorization\u003C\u002Fcode> เบราว์เซอร์จะส่ง Preflight ก่อน โดยแนบ \u003Ccode>Access-Control-Request-Method\u003C\u002Fcode> และ \u003Ccode>Access-Control-Request-Headers\u003C\u002Fcode> ไปบอกว่าคำขอจริงจะใช้อะไร\u003C\u002Fp>\u003Cp>ในการทดลองเดียวกัน เราลองส่ง POST แบบ JSON ไปที่ API ที่ยังไม่ตั้งค่า CORS Log ฝั่งเซิร์ฟเวอร์เห็นแค่บรรทัด \u003Ccode>OPTIONS \u002Fapi\u002Fusers\u003C\u002Fcode> ไม่มี POST ตามมาเลย และ Console ขึ้นข้อความว่า\u003C\u002Fp>\u003Cpre>\u003Ccode>... 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.\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>สรุปคือ ถ้าเป็นคำขอที่ต้องมี Preflight แล้ว Preflight ไม่ผ่าน คำขอจริงจะไม่ถูกส่งออกไปเลย ต่างจากกรณี GET ธรรมดาที่คำขอถึงเซิร์ฟเวอร์แล้วแต่อ่านคำตอบไม่ได้ ถ้าเจอคำว่า preflight ในข้อความ error ให้ไปดูว่าเซิร์ฟเวอร์ตอบคำขอ OPTIONS ถูกหรือยัง\u003C\u002Fp>\u003Ch2>แก้ให้ถูกที่: ตั้งค่า Header ฝั่งเซิร์ฟเวอร์\u003C\u002Fh2>\u003Cp>เพราะคนที่มีสิทธิ์อนุญาตคือเซิร์ฟเวอร์ การแก้ที่ถาวรจึงอยู่ที่ API ไม่ใช่หน้าเว็บ ตัวอย่างนี้คือ API ตัวเดิมที่เราเพิ่มการตั้งค่า CORS เข้าไป เขียนด้วยโมดูล \u003Ccode>http\u003C\u002Fcode> ของ Node.js ล้วนๆ เพื่อให้เห็นว่าจริงๆ แล้วมันคือการใส่ Header ไม่กี่ตัว\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-js\">import http from 'node:http';\n\nhttp.createServer((req, res) =&gt; {\n  res.setHeader('Access-Control-Allow-Origin', 'http:\u002F\u002Flocalhost:5173');\n  res.setHeader('Vary', 'Origin');\n\n  if (req.method === 'OPTIONS') {\n    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');\n    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\n    res.setHeader('Access-Control-Max-Age', '600');\n    res.writeHead(204);\n    return res.end();\n  }\n\n  res.setHeader('Content-Type', 'application\u002Fjson');\n  res.end(JSON.stringify({ users: ['Mint', 'Beam'] }));\n}).listen(3000);\u003C\u002Fcode>\u003C\u002Fpre>\u003Cul>\u003Cli>\u003Cp>\u003Ccode>Access-Control-Allow-Origin\u003C\u002Fcode>: origin ที่อนุญาตให้อ่านคำตอบ ระบุให้ตรงกับหน้าเว็บของเรา\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>Vary: Origin\u003C\u002Fcode>: MDN แนะนำให้ใส่เมื่อระบุ origin แบบเจาะจง เพื่อบอก cache ว่าคำตอบเปลี่ยนตาม origin ที่ขอมา\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>Access-Control-Allow-Methods\u003C\u002Fcode> และ \u003Ccode>Access-Control-Allow-Headers\u003C\u002Fcode>: ตอบ Preflight ว่ารับ method และ Header อะไรบ้าง\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Ccode>Access-Control-Max-Age\u003C\u002Fcode>: ให้เบราว์เซอร์จำผล Preflight ไว้กี่วินาที ในตัวอย่างนี้ตั้งไว้ 600\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Cp>วิธีตรวจว่าตั้งค่าถูกแล้ว ใช้ curl จำลอง Preflight ได้เลย\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">curl -i -X OPTIONS \\\n  -H \"Origin: http:\u002F\u002Flocalhost:5173\" \\\n  -H \"Access-Control-Request-Method: POST\" \\\n  -H \"Access-Control-Request-Headers: content-type\" \\\n  http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fusers\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>ผลที่เราได้คือ \u003Ccode>HTTP\u002F1.1 204 No Content\u003C\u002Fcode> พร้อม \u003Ccode>Access-Control-Allow-Origin: http:\u002F\u002Flocalhost:5173\u003C\u002Fcode> และ Header ที่เหลือครบ พอเปิดหน้าเว็บอีกครั้ง ทั้ง GET และ POST แบบ JSON ได้ข้อมูลกลับมาปกติ โดยไม่ต้องแก้โค้ดหน้าเว็บแม้แต่บรรทัดเดียว\u003C\u002Fp>\u003Cp>ในงานจริง แทบไม่มีใครเขียน Header เองทีละตัวแบบนี้ เฟรมเวิร์กส่วนใหญ่มี Middleware สำหรับ CORS ให้ใช้ ถ้าอยากเห็นว่า Middleware ทำงานแทรกกลางคำขอยังไง ลองอ่าน \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.superdevacademy.com\u002Fblogs\u002Fgo-http-middleware-control-request\">Go กับ HTTP Middleware\u003C\u002Fa> ซึ่งยกการจัดการ CORS เป็นหนึ่งในงานที่ Middleware ทำบ่อย หรือถ้ายังไม่เคยเขียน API เองเลย เริ่มจาก \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.superdevacademy.com\u002Fblogs\u002Fcreating-api-javascript-nodejs-vs-go\">การสร้าง API ด้วย Node.js และ Go\u003C\u002Fa> ก่อนก็ได้\u003C\u002Fp>\u003Ch2>ตอนพัฒนาบนเครื่อง ใช้ Proxy ของ Vite แทนได้\u003C\u002Fh2>\u003Cp>บางครั้งเราไม่ได้เป็นเจ้าของ API หรือยังไม่อยากแตะโค้ดเซิร์ฟเวอร์ระหว่างพัฒนา อีกทางที่ถูกต้องคือทำให้เบราว์เซอร์ไม่ต้องข้าม origin เลย \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fvite.dev\u002Fconfig\u002Fserver-options\">เอกสารของ Vite\u003C\u002Fa> มีตัวเลือก \u003Ccode>server.proxy\u003C\u002Fcode> สำหรับให้ Dev Server ส่งต่อคำขอที่ขึ้นต้นด้วย path ที่กำหนดไปยังเซิร์ฟเวอร์ปลายทาง\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-js\">\u002F\u002F vite.config.js\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  server: {\n    proxy: {\n      '\u002Fapi': 'http:\u002F\u002Flocalhost:3000',\n    },\n  },\n})\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>จากนั้นในโค้ดหน้าเว็บให้เรียกแค่ \u003Ccode>fetch('\u002Fapi\u002Fusers')\u003C\u002Fcode> โดยไม่ต้องใส่โดเมน เบราว์เซอร์จะมองว่าเป็นคำขอไปที่ \u003Ccode>localhost:5173\u003C\u002Fcode> ซึ่งเป็น origin เดียวกับหน้าเว็บ แล้ว Vite จะส่งต่อไปที่พอร์ต 3000 ให้เองแบบเซิร์ฟเวอร์คุยกับเซิร์ฟเวอร์ ซึ่งไม่มีกฎ CORS มาเกี่ยว ทีมเราลองกับ Vite 8.3.1 โดยใช้ API ตัวที่ยังไม่ได้ตั้งค่า CORS เลย ผลคือหน้าเว็บได้ข้อมูลครบ ไม่มีข้อความสีแดงใน Console\u003C\u002Fp>\u003Cp>ข้อควรจำคือ \u003Ccode>server.proxy\u003C\u002Fcode> ทำงานเฉพาะตอนรัน Dev Server เท่านั้น พอ build ขึ้น Production ต้องกลับไปตอบให้ได้ว่าหน้าเว็บกับ API อยู่ origin เดียวกันไหม ถ้าไม่ ก็ต้องตั้ง Header ฝั่งเซิร์ฟเวอร์ตามหัวข้อก่อนหน้าอยู่ดี\u003C\u002Fp>\u003Ch2>ทางลัดที่ดูเหมือนแก้ได้ แต่ไม่ควรใช้\u003C\u002Fh2>\u003Cp>\u003Cstrong>ปิด Security ของเบราว์เซอร์ หรือลง Extension ที่ปิด CORS ได้ไหม?\u003C\u002Fstrong>\u003C\u002Fp>\u003Cp>ทำได้จริง และข้อความสีแดงจะหายไปจริงบนเครื่องของคุณ แต่มันหายแค่บนเครื่องของคุณเครื่องเดียว ผู้ใช้จริงที่เปิดเว็บด้วยเบราว์เซอร์ปกติจะยังเจอปัญหาเดิมทุกคน แถมเบราว์เซอร์ที่ปิดกฎนี้ไว้ก็ไม่มีอะไรกันเว็บอื่นมาอ่านข้อมูลของคุณตามตัวอย่างเว็บเมลข้างบน ทางลัดนี้จึงแค่เลื่อนปัญหาไปเจอตอนขึ้น Production ไม่ได้แก้อะไร\u003C\u002Fp>\u003Cp>\u003Cstrong>ใส่ \u003Ccode>Access-Control-Allow-Origin: *\u003C\u002Fcode> ไปเลยจะได้จบ?\u003C\u002Fstrong>\u003C\u002Fp>\u003Cp>เครื่องหมาย \u003Ccode>*\u003C\u002Fcode> แปลว่าอนุญาตทุก origin ใช้ได้กับ API สาธารณะที่ไม่ต้องล็อกอิน แต่พอคำขอต้องแนบ Cookie หรือข้อมูลยืนยันตัวตน (Credentials) คู่มือ CORS ของ MDN เขียนไว้ชัดว่าเซิร์ฟเวอร์ต้องไม่ใช้ \u003Ccode>*\u003C\u002Fcode> และต้องระบุ origin แบบเจาะจงแทน เราลองแล้วจริงด้วยการเรียก \u003Ccode>fetch\u003C\u002Fcode> พร้อม \u003Ccode>credentials: 'include'\u003C\u002Fcode> ไปที่ API ที่ตอบ \u003Ccode>*\u003C\u002Fcode> ผลคือ Chrome บล็อกพร้อมข้อความ\u003C\u002Fp>\u003Cpre>\u003Ccode>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'.\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>ส่วนคำขอแบบเดียวกันที่ไม่แนบ Credentials ผ่านปกติ ข้อสรุปคือ \u003Ccode>*\u003C\u002Fcode> ไม่ใช่ทางลัดสำหรับระบบที่มีล็อกอิน ถ้าต้องรองรับหลาย origin ให้เซิร์ฟเวอร์เก็บรายชื่อ origin ที่อนุญาตไว้ แล้วตอบกลับ origin ที่ตรงกับคำขอทีละตัว พร้อม \u003Ccode>Vary: Origin\u003C\u002Fcode>\u003C\u002Fp>\u003Cdiv data-type=\"horizontalRule\">\u003Chr>\u003C\u002Fdiv>\u003Ch2>สรุป: CORS error คืออะไร และต้องแก้ที่ไหน\u003C\u002Fh2>\u003Cp>CORS error คือเบราว์เซอร์บอกว่าหน้าเว็บจาก origin หนึ่งพยายามอ่านคำตอบจากอีก origin โดยที่เซิร์ฟเวอร์ปลายทางไม่ได้อนุญาตไว้ origin นับจาก Scheme, Host และ Port ครบทั้งสามส่วน ต่างกันแค่พอร์ตก็นับเป็นคนละที่ กฎนี้มีไว้ปกป้องผู้ใช้ และคำขอแบบธรรมดามักไปถึงเซิร์ฟเวอร์แล้วด้วยซ้ำ แค่โค้ดของเราไม่ได้รับคำตอบ\u003C\u002Fp>\u003Cp>ทางแก้จึงมีสองทางที่ถูกต้อง คือตั้ง \u003Ccode>Access-Control-Allow-Origin\u003C\u002Fcode> และ Header ที่เกี่ยวข้องที่ฝั่งเซิร์ฟเวอร์ หรือใช้ \u003Ccode>server.proxy\u003C\u002Fcode> ของ Vite ระหว่างพัฒนา ครั้งหน้าที่เจอข้อความสีแดงนี้ ลองเปิด Console อ่านเหตุผลท้ายประโยค แล้วยิง curl พร้อม Header \u003Ccode>Origin\u003C\u002Fcode> ดูว่าเซิร์ฟเวอร์ตอบ Header อะไรกลับมา แค่สองขั้นนี้ก็รู้แล้วว่าต้องไปแก้ตรงไหน ถ้าอยากทวนภาพใหญ่ว่าหน้าเว็บคุยกับเซิร์ฟเวอร์ยังไง ลองอ่าน \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.superdevacademy.com\u002Fblogs\u002Fep-30-http-requests-responses-js-go\">การทำงานกับ HTTP Requests และ Responses\u003C\u002Fa> ต่อได้\u003C\u002Fp>\u003Cp>ติดตามบทความและเทคนิคสำหรับคนเริ่มต้นสายโปรแกรมเมอร์ได้ที่ช่องทางของเรา\u003C\u002Fp>\u003Cp>Facebook: \u003Cstrong>Superdev Academy Thailand\u003C\u002Fstrong> — \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.facebook.com\u002Fsuperdev.academy.th\">https:\u002F\u002Fwww.facebook.com\u002Fsuperdev.academy.th\u003C\u002Fa>\u003C\u002Fp>\u003Cp>YouTube: \u003Cstrong>Superdev Academy Channel\u003C\u002Fstrong> — \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.youtube.com\u002F@SuperdevAcademy\">https:\u002F\u002Fwww.youtube.com\u002F@SuperdevAcademy\u003C\u002Fa>\u003C\u002Fp>\u003Cp>Instagram: \u003Cstrong>@superdevacademy\u003C\u002Fstrong> — \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.instagram.com\u002Fsuperdevacademy\u002F\">https:\u002F\u002Fwww.instagram.com\u002Fsuperdevacademy\u002F\u003C\u002Fa>\u003C\u002Fp>\u003Cp>TikTok: \u003Cstrong>@superdevacademy\u003C\u002Fstrong> — \u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\u002F\u002Fwww.tiktok.com\u002F@superdevacademy?lang=th-TH\">https:\u002F\u002Fwww.tiktok.com\u002F@superdevacademy?lang=th-TH\u003C\u002Fa>\u003C\u002Fp>","cover_cover_ouk7dbsk47.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Feuio3br71wws646\u002Fl\u002Fcover_cover_ouk7dbsk47.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Feuio3br71wws646\u002Fm\u002Fcover_cover_ouk7dbsk47.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Feuio3br71wws646\u002Fcover_cover_ouk7dbsk47.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Feuio3br71wws646\u002Fs\u002Fcover_cover_ouk7dbsk47.webp","2026-09-29 04:52:24.564Z","wfztm6bp9384v3u",{"keywords":18,"locale":49,"school_blog":59},[19,25,29,33,37,41,45],{"collectionId":20,"collectionName":21,"created":22,"created_by":16,"id":23,"name":24,"updated":22,"updated_by":16},"sclkey987654321","school_keywords","2026-09-29 04:52:24.555Z","gha7bar3aonp82k","CORS",{"collectionId":20,"collectionName":21,"created":26,"created_by":16,"id":27,"name":28,"updated":26,"updated_by":16},"2026-09-29 04:52:24.556Z","31yaem7bysicpho","CORS error",{"collectionId":20,"collectionName":21,"created":30,"created_by":16,"id":31,"name":32,"updated":30,"updated_by":16},"2026-09-29 04:52:24.557Z","j2usxffa8kr7nzy","CORS policy",{"collectionId":20,"collectionName":21,"created":34,"created_by":16,"id":35,"name":36,"updated":34,"updated_by":16},"2026-09-29 04:52:24.558Z","4z860r6k4u3vpk8","Access-Control-Allow-Origin",{"collectionId":20,"collectionName":21,"created":38,"created_by":16,"id":39,"name":40,"updated":38,"updated_by":16},"2026-09-29 04:52:24.559Z","su0ip12vpfcijrj","Same-Origin Policy",{"collectionId":20,"collectionName":21,"created":42,"created_by":16,"id":43,"name":44,"updated":42,"updated_by":16},"2026-09-29 04:52:24.560Z","wkn1bhumdnfgkor","Preflight Request",{"collectionId":20,"collectionName":21,"created":46,"created_by":16,"id":47,"name":48,"updated":46,"updated_by":16},"2026-10-01 01:48:48.617Z","gk8g827bghqnpky","ติด CORS",{"code":50,"collectionId":51,"collectionName":52,"created":53,"flag":54,"id":55,"is_default":56,"label":57,"updated":58},"th","pbc_1989393366","locales","2026-01-22 10:59:55.832Z","twemoji:flag-thailand","s8wri3bt4vgg2ji",true,"Thai","2026-04-10 15:42:46.614Z",{"category":60,"collectionId":61,"collectionName":62,"created":63,"expand":64,"id":81,"slug":82,"updated":83,"views":84},"rfxf19ot4iq992c","pbc_2105096300","school_blogs","2026-09-29 04:52:24.561Z",{"category":65},{"blogIds":66,"collectionId":67,"collectionName":68,"created":69,"created_by":70,"id":60,"image":71,"image_alt":72,"image_path":73,"image_s_url":74,"label":75,"name":76,"priority":77,"publish_at":78,"scheduled_at":72,"status":79,"updated":80,"updated_by":70},[],"sclcatblg987654321","school_category_blogs","2026-03-04 08:32:03.969Z","76qprkevbgfdps8","7acfigk1qkd_lv1k6bkji3_7h89h9h63a.webp","","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclcatblg987654321\u002Frfxf19ot4iq992c\u002F7acfigk1qkd_lv1k6bkji3_7h89h9h63a.webp","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclcatblg987654321\u002Frfxf19ot4iq992c\u002Fs\u002F7acfigk1qkd_lv1k6bkji3_7h89h9h63a.webp",{"en":76,"th":76},"Web Development",0,"2025-01-21 03:43:00.282Z","published","2026-08-18 14:48:42.218Z","kf88oxcm9qxulws","cors-error-explained-how-to-fix","2026-10-22 18:38:45.045Z",124,"euio3br71wws646",[23,27,31,35,39,43,47],"2026-10-22 05:00:00.000Z","CORS error คืออะไร ทำไมเบราว์เซอร์บล็อกเมื่อหน้าเว็บเรียก API ต่าง origin พร้อมอธิบาย Preflight และวิธีแก้ที่ถูกที่ด้วย Header ฝั่งเซิร์ฟเวอร์หรือ Vite Proxy","CORS error คืออะไร ทำไมเบราว์เซอร์บล็อก และแก้ยังไงให้ถูกที่","2026-10-22 05:00:00.152Z","ebkf8ra4nd55ual",1,[94,98,102,106,110,114],{"answer":95,"id":96,"question":97},"CORS error คือข้อความที่เบราว์เซอร์แจ้งเมื่อหน้าเว็บจาก origin หนึ่งพยายามอ่านคำตอบจาก API อีก origin แต่เซิร์ฟเวอร์ปลายทางไม่ได้ส่ง Header Access-Control-Allow-Origin มาอนุญาต ใน Chrome จะขึ้นว่า has been blocked by CORS policy ส่วนโค้ด JavaScript จะเห็นแค่ TypeError: Failed to fetch รายละเอียดจริงต้องเปิดดูใน Console","fbj9bhkjq4bvpwm","CORS error คืออะไร",{"answer":99,"id":100,"question":101},"CORS error ไม่มี HTTP Status Code ของตัวเอง เพราะเป็นการบล็อกที่เกิดในเบราว์เซอร์ ไม่ใช่คำตอบจากเซิร์ฟเวอร์ หลายครั้งเซิร์ฟเวอร์ตอบกลับปกติ เช่น 200 OK หรือ 204 No Content แต่เบราว์เซอร์ไม่ยอมให้ JavaScript อ่านคำตอบนั้น MDN ระบุว่ารายละเอียดของ error ถูกซ่อนจาก JavaScript เพื่อความปลอดภัย โค้ดจึงเห็นแค่ TypeError: Failed to fetch ส่วนสาเหตุจริงต้องเปิดดูใน Console ของเบราว์เซอร์","zuwhga1g0n9ufyp","CORS error มี Status Code ไหม",{"answer":103,"id":104,"question":105},"แก้ที่ฝั่งเซิร์ฟเวอร์เป็นหลัก ถ้า API เขียนด้วย Express ให้ติดตั้ง Middleware cors จาก npm แล้วใช้ app.use(cors({ origin: 'http:\u002F\u002Flocalhost:5173' })) โดยระบุ origin ให้ชัด เพราะถ้าไม่ระบุ ค่าเริ่มต้นคือ * ซึ่งใช้กับคำขอที่แนบ Cookie ไม่ได้ ถ้าเปิด credentials: true ต้องคู่กับ origin ที่ระบุชื่อเสมอ ส่วนแอป React ที่รันด้วย Vite ระหว่างพัฒนา ใช้ server.proxy ใน vite.config.js แทนได้ การใส่ Header CORS ในโค้ด React เองไม่ได้ผล","cim6nltz7ls6pf9","แก้ CORS error ใน React หรือ Express ยังไง",{"answer":107,"id":108,"question":109},"Preflight Request คือคำขอ method OPTIONS ที่เบราว์เซอร์ส่งไปถามเซิร์ฟเวอร์ก่อน เมื่อคำขอจริงไม่ใช่ Simple Request เช่น POST ที่มี Content-Type: application\u002Fjson หรือมี Header Authorization เบราว์เซอร์จะแนบ Access-Control-Request-Method และ Access-Control-Request-Headers ไปด้วย ถ้าเซิร์ฟเวอร์ตอบ Preflight ไม่ผ่าน คำขอจริงจะไม่ถูกส่งออกไปเลย","3cgdoq1dosiot6c","Preflight Request ใน CORS คืออะไร",{"answer":111,"id":112,"question":113},"ตั้ง Access-Control-Allow-Origin: * ได้กับ API สาธารณะที่ไม่ต้องล็อกอิน แต่ใช้ไม่ได้กับคำขอที่แนบ Cookie หรือ Credentials MDN ระบุว่าเซิร์ฟเวอร์ต้องระบุ origin แบบเจาะจงแทน เมื่อทดสอบ fetch พร้อม credentials: 'include' ไปที่ API ที่ตอบ * Chrome จะบล็อกทันที ทางที่ถูกคือเก็บรายชื่อ origin ที่อนุญาตไว้ แล้วตอบกลับทีละ origin พร้อม Vary: Origin","0zih9tqpaqkwto0","ตั้ง Access-Control-Allow-Origin เป็น * ได้ไหม",{"answer":115,"id":116,"question":117},"ใช้ตัวเลือก server.proxy ใน vite.config.js เช่นตั้ง '\u002Fapi': 'http:\u002F\u002Flocalhost:3000' แล้วให้หน้าเว็บเรียก fetch('\u002Fapi\u002Fusers') โดยไม่ใส่โดเมน เบราว์เซอร์จะมองว่าเป็นคำขอไปที่ origin เดียวกับหน้าเว็บ แล้ว Vite Dev Server ส่งต่อไปที่พอร์ต 3000 ให้เอง วิธีนี้ใช้ได้เฉพาะตอนรัน Dev Server ส่วน Production ยังต้องตั้ง Header CORS ที่เซิร์ฟเวอร์","vc2gmmzco3zkicf","แก้ CORS error ตอนพัฒนาบน localhost ด้วย Vite ยังไง",{"th":82,"en":82}]