[{"data":1,"prerenderedAt":1463},["ShallowReactive",2],{"en-post-\u002Fen\u002Fcors-guide":3},{"id":4,"title":5,"body":6,"description":1449,"extension":1450,"meta":1451,"navigation":225,"path":1458,"seo":1459,"sitemap":1460,"stem":1461,"__hash__":1462},"blogEn\u002Fen\u002Fcors-guide.md","CORS Errors Explained: Why Requests Get Blocked and How to Fix Preflight and Credentials Correctly",{"type":7,"value":8,"toc":1429},"minimark",[9,14,27,37,48,54,64,71,74,78,89,165,171,173,180,183,339,346,386,388,392,399,405,408,496,502,505,511,520,526,708,755,757,761,764,793,842,849,855,879,995,997,1008,1021,1049,1052,1079,1081,1085,1093,1096,1162,1169,1176,1185,1189,1192,1196,1199,1259,1266,1268,1272,1279,1295,1311,1313,1317,1418,1425],[10,11,13],"h2",{"id":12},"the-server-is-fine-so-why-the-error","The server is fine, so why the error?",[15,16,17,18,22,23,26],"p",{},"Your frontend (",[19,20,21],"code",{},"http:\u002F\u002Flocalhost:5000",") sends a request to your API server (",[19,24,25],{},"http:\u002F\u002Flocalhost:5001","), and the console shows:",[28,29,34],"pre",{"className":30,"code":32,"language":33},[31],"language-text","Access to fetch at 'http:\u002F\u002Flocalhost:5001\u002Fusers' from origin 'http:\u002F\u002Flocalhost:5000'\nhas been blocked by CORS policy: No 'Access-Control-Allow-Origin' header\nis present on the requested resource.\n","text",[19,35,32],{"__ignoreMap":36},"",[15,38,39,40,43,44],{},"It works in Postman and ",[19,41,42],{},"curl",", just not in the browser. And surprisingly, ",[45,46,47],"strong",{},"the API server's log shows the request arrived and was fully handled.",[28,49,52],{"className":50,"code":51,"language":33},[31],"[API] GET \u002Fusers origin=http:\u002F\u002Flocalhost:5000   ← the server got the request and sent a response\n",[19,53,51],{"__ignoreMap":36},[55,56,57],"blockquote",{},[15,58,59,60,63],{},"A CORS error doesn't mean ",[45,61,62],{},"the server rejected the request; it means the browser refused to hand the response to your JavaScript",", because the server didn't send a header saying \"this origin may see the response.\"",[15,65,66,67,70],{},"That's why you can't fix CORS in frontend code. You fix it with the ",[45,68,69],{},"server's response headers",".",[72,73],"hr",{},[10,75,77],{"id":76},"origins-and-the-same-origin-policy","Origins and the same-origin policy",[15,79,80,81,84,85,88],{},"By default, browsers stop ",[45,82,83],{},"JavaScript from reading responses from a different origin",". This is the same-origin policy. Two URLs share an origin only when ",[45,86,87],{},"protocol + host + port"," all match.",[90,91,92,111],"table",{},[93,94,95],"thead",{},[96,97,98,105,108],"tr",{},[99,100,101,102],"th",{},"Compared with ",[19,103,104],{},"https:\u002F\u002Fblog.com",[99,106,107],{},"Same origin?",[99,109,110],{},"Why",[112,113,114,128,141,153],"tbody",{},[96,115,116,122,125],{},[117,118,119],"td",{},[19,120,121],{},"https:\u002F\u002Fblog.com\u002Fposts",[117,123,124],{},"✅",[117,126,127],{},"Path doesn't matter",[96,129,130,135,138],{},[117,131,132],{},[19,133,134],{},"http:\u002F\u002Fblog.com",[117,136,137],{},"❌",[117,139,140],{},"Different protocol",[96,142,143,148,150],{},[117,144,145],{},[19,146,147],{},"https:\u002F\u002Fapi.blog.com",[117,149,137],{},[117,151,152],{},"Different host (subdomains are different origins)",[96,154,155,160,162],{},[117,156,157],{},[19,158,159],{},"https:\u002F\u002Fblog.com:8080",[117,161,137],{},[117,163,164],{},"Different port",[15,166,167,168,70],{},"Without the same-origin policy, JavaScript on a malicious site could call the API of the bank you're logged into and read the responses (your balance, your transactions). CORS (Cross-Origin Resource Sharing) is the set of rules that ",[45,169,170],{},"opens an exception only when the server allows it",[72,172],{},[10,174,176,177],{"id":175},"the-basic-fix-access-control-allow-origin","The basic fix: ",[19,178,179],{},"Access-Control-Allow-Origin",[15,181,182],{},"When the server includes this header, the browser hands over the response.",[28,184,188],{"className":185,"code":186,"language":187,"meta":36,"style":36},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F Node.js built-in http module\nimport http from \"node:http\";\n\nhttp.createServer((req, res) => {\n  res.setHeader(\"Access-Control-Allow-Origin\", \"http:\u002F\u002Flocalhost:5000\");\n  res.setHeader(\"Content-Type\", \"application\u002Fjson\");\n  res.end(JSON.stringify({ users: [] }));\n}).listen(5001);\n","js",[19,189,190,199,220,227,259,282,301,323],{"__ignoreMap":36},[191,192,195],"span",{"class":193,"line":194},"line",1,[191,196,198],{"class":197},"sJ8bj","\u002F\u002F Node.js built-in http module\n",[191,200,202,206,210,213,217],{"class":193,"line":201},2,[191,203,205],{"class":204},"szBVR","import",[191,207,209],{"class":208},"sVt8B"," http ",[191,211,212],{"class":204},"from",[191,214,216],{"class":215},"sZZnC"," \"node:http\"",[191,218,219],{"class":208},";\n",[191,221,223],{"class":193,"line":222},3,[191,224,226],{"emptyLinePlaceholder":225},true,"\n",[191,228,230,233,237,240,244,247,250,253,256],{"class":193,"line":229},4,[191,231,232],{"class":208},"http.",[191,234,236],{"class":235},"sScJk","createServer",[191,238,239],{"class":208},"((",[191,241,243],{"class":242},"s4XuR","req",[191,245,246],{"class":208},", ",[191,248,249],{"class":242},"res",[191,251,252],{"class":208},") ",[191,254,255],{"class":204},"=>",[191,257,258],{"class":208}," {\n",[191,260,262,265,268,271,274,276,279],{"class":193,"line":261},5,[191,263,264],{"class":208},"  res.",[191,266,267],{"class":235},"setHeader",[191,269,270],{"class":208},"(",[191,272,273],{"class":215},"\"Access-Control-Allow-Origin\"",[191,275,246],{"class":208},[191,277,278],{"class":215},"\"http:\u002F\u002Flocalhost:5000\"",[191,280,281],{"class":208},");\n",[191,283,285,287,289,291,294,296,299],{"class":193,"line":284},6,[191,286,264],{"class":208},[191,288,267],{"class":235},[191,290,270],{"class":208},[191,292,293],{"class":215},"\"Content-Type\"",[191,295,246],{"class":208},[191,297,298],{"class":215},"\"application\u002Fjson\"",[191,300,281],{"class":208},[191,302,304,306,309,311,315,317,320],{"class":193,"line":303},7,[191,305,264],{"class":208},[191,307,308],{"class":235},"end",[191,310,270],{"class":208},[191,312,314],{"class":313},"sj4cs","JSON",[191,316,70],{"class":208},[191,318,319],{"class":235},"stringify",[191,321,322],{"class":208},"({ users: [] }));\n",[191,324,326,329,332,334,337],{"class":193,"line":325},8,[191,327,328],{"class":208},"}).",[191,330,331],{"class":235},"listen",[191,333,270],{"class":208},[191,335,336],{"class":313},"5001",[191,338,281],{"class":208},[15,340,341,342,345],{},"With Express, the ",[19,343,344],{},"cors"," middleware does the same thing.",[28,347,349],{"className":185,"code":348,"language":187,"meta":36,"style":36},"import cors from \"cors\";\napp.use(cors({ origin: \"https:\u002F\u002Fmy-frontend.com\" }));\n",[19,350,351,365],{"__ignoreMap":36},[191,352,353,355,358,360,363],{"class":193,"line":194},[191,354,205],{"class":204},[191,356,357],{"class":208}," cors ",[191,359,212],{"class":204},[191,361,362],{"class":215}," \"cors\"",[191,364,219],{"class":208},[191,366,367,370,373,375,377,380,383],{"class":193,"line":201},[191,368,369],{"class":208},"app.",[191,371,372],{"class":235},"use",[191,374,270],{"class":208},[191,376,344],{"class":235},[191,378,379],{"class":208},"({ origin: ",[191,381,382],{"class":215},"\"https:\u002F\u002Fmy-frontend.com\"",[191,384,385],{"class":208}," }));\n",[72,387],{},[10,389,391],{"id":390},"simple-requests-vs-preflight","Simple requests vs preflight",[15,393,394,395,398],{},"With that in place, send JSON with ",[19,396,397],{},"POST"," and you're blocked again:",[28,400,403],{"className":401,"code":402,"language":33},[31],"Request header field content-type is not allowed by Access-Control-Allow-Headers\nin preflight response.\n",[19,404,402],{"__ignoreMap":36},[15,406,407],{},"Browsers treat requests in two ways:",[90,409,410,423],{},[93,411,412],{},[96,413,414,417,420],{},[99,415,416],{},"Kind",[99,418,419],{},"Conditions",[99,421,422],{},"Browser behavior",[112,424,425,462],{},[96,426,427,430,456],{},[117,428,429],{},"Simple request",[117,431,432,435,436,435,439,441,442,445,446,246,449,452,453],{},[19,433,434],{},"GET","\u002F",[19,437,438],{},"HEAD",[19,440,397],{}," + only basic headers + ",[19,443,444],{},"Content-Type"," of ",[19,447,448],{},"text\u002Fplain",[19,450,451],{},"multipart\u002Fform-data",", or ",[19,454,455],{},"application\u002Fx-www-form-urlencoded",[117,457,458,459],{},"Sends it right away, then checks the response's ",[19,460,461],{},"Allow-Origin",[96,463,464,467,486],{},[117,465,466],{},"Everything else (preflighted)",[117,468,469,246,472,246,475,478,479,482,483],{},[19,470,471],{},"PUT",[19,473,474],{},"DELETE",[19,476,477],{},"PATCH"," \u002F ",[19,480,481],{},"Content-Type: application\u002Fjson"," \u002F custom headers like ",[19,484,485],{},"Authorization",[117,487,488,495],{},[45,489,490,491,494],{},"Asks for permission first with an ",[19,492,493],{},"OPTIONS"," request",", and sends the real request only if allowed",[15,497,498,501],{},[19,499,500],{},"application\u002Fjson"," isn't one of the simple-request types, so most API requests get a preflight.",[15,503,504],{},"When you check, a server that doesn't answer preflight properly logs this:",[28,506,509],{"className":507,"code":508,"language":33},[31],"[API] OPTIONS \u002Fposts origin=http:\u002F\u002Flocalhost:5000   ← only the permission check arrives\n                                                     ← the real POST is never sent\n",[19,510,508],{"__ignoreMap":36},[55,512,513],{},[15,514,515,516,519],{},"When preflight fails, ",[45,517,518],{},"the real request is never sent to the server."," A simple request reaches the server and only its response is blocked; a preflighted request is stopped before it leaves.",[15,521,522,523,525],{},"The server has to answer ",[19,524,493],{}," with the methods and headers it allows.",[28,527,529],{"className":185,"code":528,"language":187,"meta":36,"style":36},"http.createServer((req, res) => {\n  res.setHeader(\"Access-Control-Allow-Origin\", \"http:\u002F\u002Flocalhost:5000\");\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\"); \u002F\u002F cache the preflight result for 10 minutes\n    res.statusCode = 204;\n    res.end();\n    return;\n  }\n\n  \u002F\u002F ...handle the real request\n}).listen(5001);\n",[19,530,531,551,567,571,588,607,625,647,660,670,678,684,689,695],{"__ignoreMap":36},[191,532,533,535,537,539,541,543,545,547,549],{"class":193,"line":194},[191,534,232],{"class":208},[191,536,236],{"class":235},[191,538,239],{"class":208},[191,540,243],{"class":242},[191,542,246],{"class":208},[191,544,249],{"class":242},[191,546,252],{"class":208},[191,548,255],{"class":204},[191,550,258],{"class":208},[191,552,553,555,557,559,561,563,565],{"class":193,"line":201},[191,554,264],{"class":208},[191,556,267],{"class":235},[191,558,270],{"class":208},[191,560,273],{"class":215},[191,562,246],{"class":208},[191,564,278],{"class":215},[191,566,281],{"class":208},[191,568,569],{"class":193,"line":222},[191,570,226],{"emptyLinePlaceholder":225},[191,572,573,576,579,582,585],{"class":193,"line":229},[191,574,575],{"class":204},"  if",[191,577,578],{"class":208}," (req.method ",[191,580,581],{"class":204},"===",[191,583,584],{"class":215}," \"OPTIONS\"",[191,586,587],{"class":208},") {\n",[191,589,590,593,595,597,600,602,605],{"class":193,"line":261},[191,591,592],{"class":208},"    res.",[191,594,267],{"class":235},[191,596,270],{"class":208},[191,598,599],{"class":215},"\"Access-Control-Allow-Methods\"",[191,601,246],{"class":208},[191,603,604],{"class":215},"\"GET, POST, PUT, DELETE\"",[191,606,281],{"class":208},[191,608,609,611,613,615,618,620,623],{"class":193,"line":284},[191,610,592],{"class":208},[191,612,267],{"class":235},[191,614,270],{"class":208},[191,616,617],{"class":215},"\"Access-Control-Allow-Headers\"",[191,619,246],{"class":208},[191,621,622],{"class":215},"\"Content-Type, Authorization\"",[191,624,281],{"class":208},[191,626,627,629,631,633,636,638,641,644],{"class":193,"line":303},[191,628,592],{"class":208},[191,630,267],{"class":235},[191,632,270],{"class":208},[191,634,635],{"class":215},"\"Access-Control-Max-Age\"",[191,637,246],{"class":208},[191,639,640],{"class":215},"\"600\"",[191,642,643],{"class":208},"); ",[191,645,646],{"class":197},"\u002F\u002F cache the preflight result for 10 minutes\n",[191,648,649,652,655,658],{"class":193,"line":325},[191,650,651],{"class":208},"    res.statusCode ",[191,653,654],{"class":204},"=",[191,656,657],{"class":313}," 204",[191,659,219],{"class":208},[191,661,663,665,667],{"class":193,"line":662},9,[191,664,592],{"class":208},[191,666,308],{"class":235},[191,668,669],{"class":208},"();\n",[191,671,673,676],{"class":193,"line":672},10,[191,674,675],{"class":204},"    return",[191,677,219],{"class":208},[191,679,681],{"class":193,"line":680},11,[191,682,683],{"class":208},"  }\n",[191,685,687],{"class":193,"line":686},12,[191,688,226],{"emptyLinePlaceholder":225},[191,690,692],{"class":193,"line":691},13,[191,693,694],{"class":197},"  \u002F\u002F ...handle the real request\n",[191,696,698,700,702,704,706],{"class":193,"line":697},14,[191,699,328],{"class":208},[191,701,331],{"class":235},[191,703,270],{"class":208},[191,705,336],{"class":313},[191,707,281],{"class":208},[90,709,710,720],{},[93,711,712],{},[96,713,714,717],{},[99,715,716],{},"Preflight response header",[99,718,719],{},"Meaning",[112,721,722,732,742],{},[96,723,724,729],{},[117,725,726],{},[19,727,728],{},"Access-Control-Allow-Methods",[117,730,731],{},"HTTP methods that are allowed",[96,733,734,739],{},[117,735,736],{},[19,737,738],{},"Access-Control-Allow-Headers",[117,740,741],{},"Headers the request may include",[96,743,744,749],{},[117,745,746],{},[19,747,748],{},"Access-Control-Max-Age",[117,750,751,752,754],{},"How many seconds to cache the preflight result (so ",[19,753,493],{}," isn't sent every time)",[72,756],{},[10,758,760],{"id":759},"sending-cookies-credentials","Sending cookies: credentials",[15,762,763],{},"To send a login session cookie to an API on a different origin, both the frontend and the server need settings.",[28,765,767],{"className":185,"code":766,"language":187,"meta":36,"style":36},"\u002F\u002F Frontend\nfetch(\"https:\u002F\u002Fapi.my-service.com\u002Fme\", { credentials: \"include\" });\n",[19,768,769,774],{"__ignoreMap":36},[191,770,771],{"class":193,"line":194},[191,772,773],{"class":197},"\u002F\u002F Frontend\n",[191,775,776,779,781,784,787,790],{"class":193,"line":201},[191,777,778],{"class":235},"fetch",[191,780,270],{"class":208},[191,782,783],{"class":215},"\"https:\u002F\u002Fapi.my-service.com\u002Fme\"",[191,785,786],{"class":208},", { credentials: ",[191,788,789],{"class":215},"\"include\"",[191,791,792],{"class":208}," });\n",[28,794,796],{"className":185,"code":795,"language":187,"meta":36,"style":36},"\u002F\u002F Server\nres.setHeader(\"Access-Control-Allow-Origin\", \"https:\u002F\u002Fmy-service.com\"); \u002F\u002F * not allowed\nres.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n",[19,797,798,803,824],{"__ignoreMap":36},[191,799,800],{"class":193,"line":194},[191,801,802],{"class":197},"\u002F\u002F Server\n",[191,804,805,808,810,812,814,816,819,821],{"class":193,"line":201},[191,806,807],{"class":208},"res.",[191,809,267],{"class":235},[191,811,270],{"class":208},[191,813,273],{"class":215},[191,815,246],{"class":208},[191,817,818],{"class":215},"\"https:\u002F\u002Fmy-service.com\"",[191,820,643],{"class":208},[191,822,823],{"class":197},"\u002F\u002F * not allowed\n",[191,825,826,828,830,832,835,837,840],{"class":193,"line":222},[191,827,807],{"class":208},[191,829,267],{"class":235},[191,831,270],{"class":208},[191,833,834],{"class":215},"\"Access-Control-Allow-Credentials\"",[191,836,246],{"class":208},[191,838,839],{"class":215},"\"true\"",[191,841,281],{"class":208},[15,843,844,845,848],{},"The most common mistake here is combining ",[19,846,847],{},"Allow-Origin: *"," with cookies. Browsers reject that combination:",[28,850,853],{"className":851,"code":852,"language":33},[31],"The value of the 'Access-Control-Allow-Origin' header in the response must not be\nthe wildcard '*' when the request's credentials mode is 'include'.\n",[19,854,852],{"__ignoreMap":36},[856,857,858],"warning",{},[55,859,860],{},[15,861,862,863,866,867,870,871,874,875,878],{},"Requests that send cookies can't use ",[19,864,865],{},"*","; you must name ",[45,868,869],{},"one exact origin",". If you allow several origins, compare the request's ",[19,872,873],{},"Origin"," header against an allowlist and echo it back only when it matches. Send ",[19,876,877],{},"Vary: Origin"," as well, so a CDN doesn't mix up cached responses meant for different origins.",[28,880,882],{"className":185,"code":881,"language":187,"meta":36,"style":36},"const ALLOWED = [\"https:\u002F\u002Fmy-service.com\", \"https:\u002F\u002Fadmin.my-service.com\"];\n\nconst origin = req.headers.origin;\nif (ALLOWED.includes(origin)) {\n  res.setHeader(\"Access-Control-Allow-Origin\", origin);\n  res.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n}\nres.setHeader(\"Vary\", \"Origin\");\n",[19,883,884,908,912,924,943,956,972,977],{"__ignoreMap":36},[191,885,886,889,892,895,898,900,902,905],{"class":193,"line":194},[191,887,888],{"class":204},"const",[191,890,891],{"class":313}," ALLOWED",[191,893,894],{"class":204}," =",[191,896,897],{"class":208}," [",[191,899,818],{"class":215},[191,901,246],{"class":208},[191,903,904],{"class":215},"\"https:\u002F\u002Fadmin.my-service.com\"",[191,906,907],{"class":208},"];\n",[191,909,910],{"class":193,"line":201},[191,911,226],{"emptyLinePlaceholder":225},[191,913,914,916,919,921],{"class":193,"line":222},[191,915,888],{"class":204},[191,917,918],{"class":313}," origin",[191,920,894],{"class":204},[191,922,923],{"class":208}," req.headers.origin;\n",[191,925,926,929,932,935,937,940],{"class":193,"line":229},[191,927,928],{"class":204},"if",[191,930,931],{"class":208}," (",[191,933,934],{"class":313},"ALLOWED",[191,936,70],{"class":208},[191,938,939],{"class":235},"includes",[191,941,942],{"class":208},"(origin)) {\n",[191,944,945,947,949,951,953],{"class":193,"line":261},[191,946,264],{"class":208},[191,948,267],{"class":235},[191,950,270],{"class":208},[191,952,273],{"class":215},[191,954,955],{"class":208},", origin);\n",[191,957,958,960,962,964,966,968,970],{"class":193,"line":284},[191,959,264],{"class":208},[191,961,267],{"class":235},[191,963,270],{"class":208},[191,965,834],{"class":215},[191,967,246],{"class":208},[191,969,839],{"class":215},[191,971,281],{"class":208},[191,973,974],{"class":193,"line":303},[191,975,976],{"class":208},"}\n",[191,978,979,981,983,985,988,990,993],{"class":193,"line":325},[191,980,807],{"class":208},[191,982,267],{"class":235},[191,984,270],{"class":208},[191,986,987],{"class":215},"\"Vary\"",[191,989,246],{"class":208},[191,991,992],{"class":215},"\"Origin\"",[191,994,281],{"class":208},[72,996],{},[10,998,1000,1001,1004,1005],{"id":999},"when-a-response-header-reads-as-null-expose-headers","When a response header reads as ",[19,1002,1003],{},"null",": ",[19,1006,1007],{},"Expose-Headers",[15,1009,1010,1011,1013,1014,1017,1018,1020],{},"In CORS requests, only a few response headers (like ",[19,1012,444],{},") are readable by default. If you send something like ",[19,1015,1016],{},"X-Total-Count"," for pagination and the frontend gets ",[19,1019,1003],{},", this is why.",[28,1022,1024],{"className":185,"code":1023,"language":187,"meta":36,"style":36},"\u002F\u002F The server sent X-Total-Count: 42, but...\nres.headers.get(\"X-Total-Count\"); \u002F\u002F null\n",[19,1025,1026,1031],{"__ignoreMap":36},[191,1027,1028],{"class":193,"line":194},[191,1029,1030],{"class":197},"\u002F\u002F The server sent X-Total-Count: 42, but...\n",[191,1032,1033,1036,1039,1041,1044,1046],{"class":193,"line":201},[191,1034,1035],{"class":208},"res.headers.",[191,1037,1038],{"class":235},"get",[191,1040,270],{"class":208},[191,1042,1043],{"class":215},"\"X-Total-Count\"",[191,1045,643],{"class":208},[191,1047,1048],{"class":197},"\u002F\u002F null\n",[15,1050,1051],{},"The server has to list the headers it exposes.",[28,1053,1055],{"className":185,"code":1054,"language":187,"meta":36,"style":36},"res.setHeader(\"Access-Control-Expose-Headers\", \"X-Total-Count\");\n\u002F\u002F now res.headers.get(\"X-Total-Count\") → \"42\"\n",[19,1056,1057,1074],{"__ignoreMap":36},[191,1058,1059,1061,1063,1065,1068,1070,1072],{"class":193,"line":194},[191,1060,807],{"class":208},[191,1062,267],{"class":235},[191,1064,270],{"class":208},[191,1066,1067],{"class":215},"\"Access-Control-Expose-Headers\"",[191,1069,246],{"class":208},[191,1071,1043],{"class":215},[191,1073,281],{"class":208},[191,1075,1076],{"class":193,"line":201},[191,1077,1078],{"class":197},"\u002F\u002F now res.headers.get(\"X-Total-Count\") → \"42\"\n",[72,1080],{},[10,1082,1084],{"id":1083},"fixes-that-dont-actually-fix-anything","Fixes that don't actually fix anything",[1086,1087,1089,1090],"h3",{"id":1088},"mode-no-cors","❌ ",[19,1091,1092],{},"mode: \"no-cors\"",[15,1094,1095],{},"People often see the error and change the code like this:",[28,1097,1099],{"className":185,"code":1098,"language":187,"meta":36,"style":36},"const res = await fetch(\"http:\u002F\u002Flocalhost:5001\u002Fusers\", { mode: \"no-cors\" });\nres.type; \u002F\u002F \"opaque\"\nres.status; \u002F\u002F 0\nawait res.json(); \u002F\u002F fails: you can't read the body\n",[19,1100,1101,1129,1137,1145],{"__ignoreMap":36},[191,1102,1103,1105,1108,1110,1113,1116,1118,1121,1124,1127],{"class":193,"line":194},[191,1104,888],{"class":204},[191,1106,1107],{"class":313}," res",[191,1109,894],{"class":204},[191,1111,1112],{"class":204}," await",[191,1114,1115],{"class":235}," fetch",[191,1117,270],{"class":208},[191,1119,1120],{"class":215},"\"http:\u002F\u002Flocalhost:5001\u002Fusers\"",[191,1122,1123],{"class":208},", { mode: ",[191,1125,1126],{"class":215},"\"no-cors\"",[191,1128,792],{"class":208},[191,1130,1131,1134],{"class":193,"line":201},[191,1132,1133],{"class":208},"res.type; ",[191,1135,1136],{"class":197},"\u002F\u002F \"opaque\"\n",[191,1138,1139,1142],{"class":193,"line":222},[191,1140,1141],{"class":208},"res.status; ",[191,1143,1144],{"class":197},"\u002F\u002F 0\n",[191,1146,1147,1150,1153,1156,1159],{"class":193,"line":229},[191,1148,1149],{"class":204},"await",[191,1151,1152],{"class":208}," res.",[191,1154,1155],{"class":235},"json",[191,1157,1158],{"class":208},"(); ",[191,1160,1161],{"class":197},"\u002F\u002F fails: you can't read the body\n",[15,1163,1164,1165,1168],{},"The error goes away, but ",[45,1166,1167],{},"the response becomes \"opaque\": you can't read the status or the body."," You've hidden the error, not fixed it.",[1086,1170,1089,1172,1175],{"id":1171},"access-control-allow-origin-everywhere",[19,1173,1174],{},"Access-Control-Allow-Origin: *"," everywhere",[15,1177,1178,1179,1181,1182,1184],{},"For a public API (a weather API anyone can use), ",[19,1180,865],{}," is correct. But if you put ",[19,1183,865],{}," on authenticated APIs out of habit, it breaks the moment you add cookie auth, and you skip thinking about who should actually be allowed. Get in the habit of naming allowed origins.",[1086,1186,1188],{"id":1187},"disabling-cors-with-a-browser-extension-or-launch-flag","❌ Disabling CORS with a browser extension or launch flag",[15,1190,1191],{},"It only works in your browser; your users' browsers still block it. Even for local testing, it tends to hide the real problem, so it's not recommended.",[1086,1193,1195],{"id":1194},"in-development-a-proxy-is-a-valid-option","✅ In development, a proxy is a valid option",[15,1197,1198],{},"If the frontend dev server forwards API requests for you, the browser sees a same-origin request and CORS never comes up.",[28,1200,1202],{"className":185,"code":1201,"language":187,"meta":36,"style":36},"\u002F\u002F vite.config.js\nexport default {\n  server: {\n    proxy: {\n      \"\u002Fapi\": \"http:\u002F\u002Flocalhost:5001\", \u002F\u002F forward \u002Fapi\u002F* requests to the API server\n    },\n  },\n};\n",[19,1203,1204,1209,1219,1224,1229,1244,1249,1254],{"__ignoreMap":36},[191,1205,1206],{"class":193,"line":194},[191,1207,1208],{"class":197},"\u002F\u002F vite.config.js\n",[191,1210,1211,1214,1217],{"class":193,"line":201},[191,1212,1213],{"class":204},"export",[191,1215,1216],{"class":204}," default",[191,1218,258],{"class":208},[191,1220,1221],{"class":193,"line":222},[191,1222,1223],{"class":208},"  server: {\n",[191,1225,1226],{"class":193,"line":229},[191,1227,1228],{"class":208},"    proxy: {\n",[191,1230,1231,1234,1236,1239,1241],{"class":193,"line":261},[191,1232,1233],{"class":215},"      \"\u002Fapi\"",[191,1235,1004],{"class":208},[191,1237,1238],{"class":215},"\"http:\u002F\u002Flocalhost:5001\"",[191,1240,246],{"class":208},[191,1242,1243],{"class":197},"\u002F\u002F forward \u002Fapi\u002F* requests to the API server\n",[191,1245,1246],{"class":193,"line":284},[191,1247,1248],{"class":208},"    },\n",[191,1250,1251],{"class":193,"line":303},[191,1252,1253],{"class":208},"  },\n",[191,1255,1256],{"class":193,"line":325},[191,1257,1258],{"class":208},"};\n",[15,1260,1261,1262,1265],{},"In production, if the frontend and API live under the same domain (for example ",[19,1263,1264],{},"my-service.com\u002Fapi","), you don't need CORS settings at all.",[72,1267],{},[10,1269,1271],{"id":1270},"cors-does-not-protect-your-server","CORS does not protect your server",[15,1273,1274,1275,1278],{},"One last thing you need to know. As the first example showed, ",[45,1276,1277],{},"a simple request blocked by CORS still reaches the server and runs."," CORS stops the browser from reading responses from other origins; it is not a firewall that blocks requests to your server.",[1280,1281,1282,1289],"ul",{},[1283,1284,1285,1286,1288],"li",{},"CORS doesn't apply to ",[19,1287,42],{},", Postman, or server-to-server requests at all.",[1283,1290,1291,1292,70],{},"Blocking requests that change data is the job of ",[45,1293,1294],{},"authentication, authorization checks, and CSRF protection",[1296,1297,1298],"note",{},[55,1299,1300],{},[15,1301,1302,1303,1306,1307,1310],{},"CORS settings decide \"who can ",[45,1304,1305],{},"read my API's responses in a browser",".\" \"Who can ",[45,1308,1309],{},"call my API","\" has to be protected separately with authentication and authorization.",[72,1312],{},[10,1314,1316],{"id":1315},"summary-fixes-by-error-message","Summary: fixes by error message",[90,1318,1319,1332],{},[93,1320,1321],{},[96,1322,1323,1326,1329],{},[99,1324,1325],{},"Phrase in the error",[99,1327,1328],{},"Cause",[99,1330,1331],{},"What to do on the server",[112,1333,1334,1349,1368,1385,1403],{},[96,1335,1336,1341,1344],{},[117,1337,1338],{},[19,1339,1340],{},"No 'Access-Control-Allow-Origin' header",[117,1342,1343],{},"Missing allow header",[117,1345,1346,1347],{},"Add ",[19,1348,179],{},[96,1350,1351,1356,1359],{},[117,1352,1353],{},[19,1354,1355],{},"not allowed by Access-Control-Allow-Headers in preflight",[117,1357,1358],{},"Header not allowed in preflight",[117,1360,1346,1361,1364,1365,1367],{},[19,1362,1363],{},"Allow-Headers"," to the ",[19,1366,493],{}," response",[96,1369,1370,1375,1378],{},[117,1371,1372],{},[19,1373,1374],{},"Method PUT is not allowed",[117,1376,1377],{},"Method not allowed in preflight",[117,1379,1346,1380,1364,1383,1367],{},[19,1381,1382],{},"Allow-Methods",[19,1384,493],{},[96,1386,1387,1392,1397],{},[117,1388,1389],{},[19,1390,1391],{},"must not be the wildcard '*' when ... credentials",[117,1393,1394,1395],{},"Cookies + ",[19,1396,865],{},[117,1398,1399,1400],{},"Exact origin + ",[19,1401,1402],{},"Allow-Credentials: true",[96,1404,1405,1410,1413],{},[117,1406,1407,1408],{},"(no error) response header is ",[19,1409,1003],{},[117,1411,1412],{},"Header not exposed",[117,1414,1346,1415],{},[19,1416,1417],{},"Access-Control-Expose-Headers",[15,1419,1420,1421,1424],{},"When you hit a CORS error, before touching frontend code, read the error message for ",[45,1422,1423],{},"\"what didn't the browser get permission for from the server?\""," The answer is almost always one response header on the server.",[1426,1427,1428],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":201,"depth":201,"links":1430},[1431,1432,1433,1435,1436,1437,1439,1447,1448],{"id":12,"depth":201,"text":13},{"id":76,"depth":201,"text":77},{"id":175,"depth":201,"text":1434},"The basic fix: Access-Control-Allow-Origin",{"id":390,"depth":201,"text":391},{"id":759,"depth":201,"text":760},{"id":999,"depth":201,"text":1438},"When a response header reads as null: Expose-Headers",{"id":1083,"depth":201,"text":1084,"children":1440},[1441,1443,1445,1446],{"id":1088,"depth":222,"text":1442},"❌ mode: \"no-cors\"",{"id":1171,"depth":222,"text":1444},"❌ Access-Control-Allow-Origin: * everywhere",{"id":1187,"depth":222,"text":1188},{"id":1194,"depth":222,"text":1195},{"id":1270,"depth":201,"text":1271},{"id":1315,"depth":201,"text":1316},"A 'has been blocked by CORS policy' error comes from the browser, not the server. The same-origin policy, simple vs preflighted requests, requests with cookies, and exposing response headers, verified in a real browser, plus why fixes like mode: 'no-cors' don't work.","md",{"date":1452,"field":1453,"tags":1454},"2026.10.08","backend",[1455,1456,1457],"http","web-security","nodejs","\u002Fen\u002Fcors-guide",{"title":5,"description":1449},{"loc":1458},"en\u002Fcors-guide","49561ezred08vodDb9tsjVBXypAk8ZE5PzIqTxqsck4",1791482015983]