days since this article was written, please be aware of its timeliness
The Origin of This Article
While reading an online resource about CORS, I was confused by a statement: “Note that after setting withCredentials = true, the cookie carried is the target domain’s cookie.” I was puzzled: If the current domain is a.com sending an xhr to b.com, shouldn’t it be the source domain a.com’s cookie that’s sent to b.com for processing? How could it be the target domain’s (which I assumed to be b.com) cookie? Thus, I began my investigation (spoiler: the statement in the material I read was indeed correct—it does carry the target domain b.com’s cookie).
Mini-Goal 1: Make a.com Send an ajax Request to b.com Under Simple Requests
What constitutes a simple request can be found via Google/Stack Overflow. Here’s a key fact: cookies are tied to domains, not IP addresses. I set up a simple express service locally that can handle cookies, and another identical service on my VPS, then used hosts file modifications to simulate different domains:
1 | |
First, I launched an express service on the VPS server with no special configurations, simply returning the header, running on port 9091:
1 | |
The a.com server setup was largely the same, except I added a static page to send ajax requests, running on port 9090:
1 | |
Content of index.html:
1 | |
Since the server didn’t set Access-Control-Allow-Origin, an error occurred:

Next, I added permission for ajax requests from a.com on the b.com server (requiring precise port specification):
1 | |
Sent the request again:


The console showed no errors, and the status code 200 confirmed that b.com now allowed requests from a.com:9090.
Mini-Goal 2: Local Service Receives Frontend cookies
Next, I tested whether the a.com backend could receive cookies from the frontend locally:
The test method was simple—just add some arbitrary cookies in the js:
1 | |

The Application tab in the local console confirmed the presence of the cookie. Checking the backend output:

No issues here. Visiting www.a.com:9090 indeed included the cookie, as expected.
Mini-Goal 3: Sending a.com’s cookie to b.com
At this point, the a.com page had cookies, so I clicked the button again to see if the ajax request could pass the cookie to b.com:

Just like before adding the cookie, b.com didn’t receive a.com’s cookie. This was due to security restrictions and was expected.
Bonus Mini-Goal: Non-Simple Requests
Here’s a quick test about simple requests. I added a new header to the xhr and sent the request again:
1 | |
Since this was done after setting Access-Control-Allow-Origin, the browser reported a different error this time:

I noticed it was still due to the Access-Control-Allow-Origin error, but this time it was because the frontend had set a custom header, making it a non-simple request. For non-simple requests, a preflight request (prelight) is first sent with the request type OPTIONS. You can check out this article for more details. The purpose of the preflight request is to “check in” with the b.com server to see if it accepts this xiaodan header. The backend did not include the value xiaodan in the returned header Access-Control-Allow-Headers, hence the error.
Next, we added the corresponding header to the response from b.com:
1 | |
Then we made the request again:

Surprisingly, the same error persisted. Although the server responded with 200, it did not return the corresponding Access-Control-Allow-Headers, so the response was rejected by the browser (note: not by the server, as the server did return 200).
After some troubleshooting, I realized the issue lay with this non-simple request. I made a slight modification to the b.com function:
1 | |
Server-side:

Client-side:

The root cause (to be verified—I’ll revisit the HTTP authoritative guide later) seems to be that the prelight request for non-simple requests does not initiate the actual request. Instead, it first sends a preflight request to test whether the server supports a particular non-simple header field. In other words, requests with non-simple headers do not reach app.get('/'). This is confirmed by the fact that console.log(req.headers) is written inside app.get('/'), and the b.com server did not output anything for the recent request. This design ensures the server is aware of the CORS standard to protect legacy servers that do not support CORS.
Mini-Goal 4: Sending a.com Domain’s cookie to b.com
Alright, intermission over. Let’s test whether setting withCredentials = true in the client (i.e., the xhr request initiated from the a.com page—only the xhrSend part is shown here) can send a.com’s cookie to b.com (the result is the same for simple and non-simple requests; for clarity, I removed the header set in xhr):
1 | |

This time, the error message changed: the server did not set Access-Control-Allow-Credentials to true. This header is used to allow requests to carry cookies, so let’s set it:
1 | |

Still nothing. Let’s check Chrome’s cookie:

The cookie is indeed set—what’s going on? Unconvinced, I thought req.headers might be formatted by express, so I checked the raw headers rawHeaders:

Still nothing. Did the cookie get eaten by a dog?
None of it, she said I’d find it someday, time, time will give me the answer ——— My Skate Shoes
OK, when you can’t find the problem, just eat an ice cream. I went downstairs and bought a Nestlé ice cream with cilantro flavor (can’t afford Häagen-Dazs), and while going upstairs, a light bulb moment hit me—maybe ours is a third-party cookie issue, could it be caused by me disabling browser tracking? So after finishing the ice cream, I went into chrome settings and unchecked the box for Send a "Do Not Track" request with your browsing traffic:

By the way, this time I put req.headers inside app.use just in case (though there really shouldn’t be any “just in case”):
1 | |
Clicked the button again to send the request, then checked the chrome console and b.com server output:
Because of non-simple headers, it showed two requests as before:


The server didn’t receive it either, which means it’s unrelated to this Chrome setting. So to control variables, I rechecked the Do Not Track option to match the previous state. Server-side (only showing the GET request):

Still no Cookie field. Why?
Then I remembered the sentence at the beginning of the article: “Note: After setting withCredentials = true, the cookie carried is the target domain’s cookie.” Could it be that when I click the button on a.com to send a request to b.com, it’s sending b.com’s cookie?
So I first created an index.html on the b.com server and added some random cookie:
b.com server code:
1 | |
b.com’s index.html code:
1 | |
OK, let’s first visit b.com:

No issues, the page loads normally, and the cookie is set correctly. Next, on a.com, I clicked the button to send the request:

You can see that the ajax request initiated from a.com carried b.com’s cookie.
The statement at the beginning of the article was confirmed.
Mini Goal-5: a.com’s JavaScript fetching b.com’s cookie:
Since a.com can send b.com’s cookie, can the frontend also retrieve b.com’s cookie?
Checked the documentation—ajax has two interfaces: getAllResponseHeaders() and getResponseHeader(), and the server has Access-Control-Expose-Header. So I tested it (I just want to output them separately, so what?).
Starting with the simpler one, first calling xhr’s getAllResponseHeaders() interface:
1 | |

No Cookie field appeared in the header, as expected. Just in case getAllResponseHeaders() didn’t traverse the header because Cookie was set to enumerable: false, I tried getResponseHeader():
1 | |

Still as expected, because the server didn’t expose the header content. So I set Access-Control-Expose-Header on b.com:
1 | |
Then execute getResponseHeader('Cookie') and getAllResponseHeaders() again.

No errors now, but still unable to obtain the cookie from b.com, even though the b.com server has granted permission.
After researching, I learned that Access-Control-Expose-Header can only be set to custom headers for the frontend to access. But this seems pointless, because these custom headers are set by the frontend in the first place. The only use case is for the backend to modify/create custom headers and then have the frontend retrieve them. Below, I’ll set a header on the backend for the frontend to fetch:
b.com’s server:
1 | |
a.com’s index.html:
1 | |

That’s it.
So this small goal is unachievable, but the SO community offers some solutions, like using third-party services/backend forwarding. After all, rules are rigid, but people are flexible—just like jsonp, right?
Reflection
Some claim that zero-basis beginners can master a programming language in just a few months of training. I think that’s a fantasy. Without foundational computer knowledge—without understanding concepts like binary/compilation principles/computer fundamentals/operating system basics/network foundations/communication protocols—being able to write code only proves you’re good at copying examples. You know that writing X produces Y, but you don’t know why writing X results in Y.
Therefore, when working with computers, the broader your knowledge, the better, and the deeper your understanding, the better. For instance, while learning about CORS, I recalled my earlier exposure to AUTH.
There’s something called OAuth 2.0. What’s its relationship with CORS? None, really. But here’s my comparison:
CORS allows users who have visited Site B to carry B’s cookie when making requests from Site A. The steps are:
- The user visits Site
B. - The user visits Site
Aand initiates a request to SiteBfromA. - Site
Bverifies thecookiefromBin the request fromA. If valid, it returnsB’s data.
OAuth allows users to access resources on Site B from Site A, but requires user authorization. The steps are:
- Initiate a request from Site
A. - Step 1 redirects to Site
Bfor authorization confirmation, then redirects back to SiteA. - Now, Site
Aobtains atokenand can access the user’s resources on SiteB.
See the resemblance?
The first step of CORS corresponds to the second step of OAuth. The second step of OAuth is analogous to the first step of CORS—you could say it’s like adding Access-Control-Allow-Credentials on Site B’s server, effectively completing authorization.
You could consider CORS with cookie as a simplified version of OAuth.
Excerpt from RFC 6749:

Postscript
Why do third-party advertising cookies leak privacy? This is because when an ad is placed on website A, the advertiser knows the ad is displayed on A (identified and paid for through ad placement id/key or similar identity). The advertiser then sets a cookie on this ad. Since the ad originates from the domain of advertiser B, the cookie set naturally belongs to B. Every time website A loads this ad, it must execute a piece of js. In this js, it is configured to allow A to send cookies, while B also permits cookies from A to carry B’s cookies back. Thus, everything becomes known.
`Google AD Impl:


Notes
-
The above modifications involve server-side changes, all of which require a service restart. Since restarting services on a
VPSis inconvenient and connections may time out, the best approach is to place thea.comcontent from the article on theVPS(primarily for modifyingindex.html), while hosting theb.comcontent locally (mainly for modifyingindex.js). -
To keep remote connections from disconnecting, the simplest method is to run them in the background (provided the connection remains active; otherwise, if the connection drops, incoming requests performing
I/Ooperations will still be terminated). You can runnode index.js &, or ifnode index.jsis already running, pressctrl+zto suspend it to the background, then execute thebgcommand to resume the most recent background task. Thejobscommand lists current tasks. If you exit the currentsessionand reconnect later, the task will still be running, butjobswon’t display it. In this case, useps -Ato list all processes, thenkill IDto terminate thenodeprocess and restart it. -
During the completion of small goals, I suspected errors occurred because custom fields weren’t prefixed with
X-. Upon checking the standards, I found no requirement forX-prefixes—Wikipedia and Stack Overflow merely recommend customHeadersto start withX-. -
After setting
withCredentials = true, the server’sAccess-Control-Allow-Origincan no longer use thewildcard*.
I often wish that when facing some key decisions in life, someone could tell me the best course of action so that I would not waste my precious time. Putting myself in others' shoes, I therefore write blogs often, hoping to record in this tiny corner of the vast Internet the once-in-a-lifetime experiences that matter to me, and to help those who seek help.