Cache your CORS
httptoolkit.tech
httptoolkit.tech
GET /v1/document/{document-id}/comments/{comment-id}
For every new document-id or comment-id, there will be a new pre-flight request.Alternative hacks: Offer a variant of your API format that either
1. Moves the resource path to the request body (or to a header that is included in "Vary"). Though the rest of your stack (load balancing, observability, redaction) might not be ok with this, e.g. do your WAF rules support matching on the request body? You also will no longer get automatic path-based caching for GET requests.
2. Conforms to the rules of a CORS "simple" request [1], which won't trigger a pre-flight request. This is what we did on the Dropbox API [2]. You'll need to move the auth information from the Authorization header to a query parameter or the body, which can be dangerous wrt redaction, e.g. many tools automatically redact the Authorization header but not query parameters.
[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simpl...
[2] https://www.dropbox.com/developers/documentation/http/docume... (see "Browser-based JavaScript and CORS pre-flight requests")
I'm careful that I don't demand anyone go into depth on any particular subject. CORS is just one opportunity that seems to need a fix with every new project. It also has a variety of solutions, which is again opportunity to show what they know.
There is little point in looking for what a client doesn't know. I've got that covered.
I'd like you to back up your claim that a server-side proxy or using the same domain is the easiest solution, and in particular, easier than the solutions suggested by cakoose.
As for whether that's easier - I've been doing it like this since forever, so I'm biased and it's easy for me. It's easy because I don't have to worry about problems related to cross origin resource sharing and because I know how to write the necessary configs. If you don't have to walk through mine field, you never worry about mines. That's the easy part I refer to. I don't even need to test whether CORS is set up properly, worry about preflight and what not etc. - it works, forever.
Whether it will be as easy for you, I can't tell that, you can have completely different opinion and be correct about it, but the fact remains that proxy between 2 resources removes the problem. I consider problem removed as something easy, you might not.
If you're dealing with a bunch of separate black boxes (as our firebase poster is) then maybe you do have to wrangle CORS but if you're developing your own applications then there is no good reason to introduce these issues into your pipeline.
CORS ain't that hard.
This confirms my initial comment - people making up reasons.
The solution is always the same, it's always as easy and there's never the need to introduce something new since you can use what you already have.
Does that mean that I'd have to introduce a web server in front of Firebase Hosting that I'd have to maintain and scale, not to mention that it negates all advantages of using FB hosting in the first place?
I'll stick with CORS, thank you.
There's a comment below mine that tells you you can achieve this kind of proxy with the actual Firebase itself. And your comment, again, proves my hypothesis - people just make reasons up with improper arguments.
This doesn't require handling an OPTIONS request for every endpoint, which I agree could be fairly simple, but Firebase Hosting configuration could very well be less invasive than this change in the rest of the codebase.
It's a very common case.
Basically that won't work besides making your CDN somehow proxy requests for which not static file exists.
(In case it’s not clear, Fastly is also caching and serving these requests as a CDN.)
In a lot of sensitive businesses that wouldn't be allowed.
You can't guarantee the integrity and privacy of the full request and response chain.
This goes against various security standards and ISOs.
You might be able to get away with it and trust the CDN but that's an awful lot of trust.
A lot of people seem to think that there is a big difference between paying a lessor (e.g. Hetzner) for a server on which you terminate TLS, paying a cloud host (e.g. Amazon) to terminate TLS, and paying a CDN (e.g. Fastly) to terminate TLS. Legally there is no difference aside from the specific language of the contracts, which you can review and negotiate in advance.
The difference security-wise is entirely down to the operations of each company, which again you can review and discuss in advance. Strictly speaking a CDN should have lower risk than a host since they are not persisting sensitive data (if you set your cache headers correctly). And as discussed above, using one domain helps avoids cross-domain security concerns.
Given the fact I have control over domain(s), I'd have http://domain.tld/api for API and serve static js from http://domain.tld/static/*.js
Given the fact I have the need for CDN, it means I've got enough traffic that justifies the bill incurred
What is the benefit or purpose of doing this?
I think we may be talking past each other and one of us is misunderstanding OP.
Say you stand up a website with an API at https://example.com. You host your static assets on the CDN, at https://example.myfastcdn.com/. So example.com's home page looks like:
<head>
<script src="https://example.myfastcdn.com/app.js"></script>
</head>
<body>
<div id="site-root"></div><!-- app.js renders some application here -->
</body>
The origin when you load your site is https://example.com, so the scripts hosted on the CDN can still make API requests to https://example.com.What am I missing here?
It's like saying that a house built without a lock is a made up issue and that no lock pr door is needed.
Public CDN should never be trusted. If you use a CDN in the first place and have strict security requirements, then you create your own private CDN. And if you can control that private CDN, you have all the ingredients to avoid CORS.
It's really that simple. No one is saying you are wrong, but you're refusing to look at the entire picture and you focus only on a subset, in which - of course - your argument works.
I'm sorry but that's simply not an acceptable constraint.
No one is telling you not to deal with CORS your way. Fact of the matter is that you can avoid it, but you're making up reasons why you can't. The only reason you can't is because you won't. You're free to use whatever approach you like, there's no police here, just don't state that I or anyone else wrote what we didn't. It'd be grown up thing to do. Thanks and best of success with your projects.
Example: you have http://ui.localhost and you have http://api.localhost
UI speaking to API = CORS
But, instead of doing fetch('http://api.localhost/resource'), you do fetch('http://ui.localhost/api/resource')
In the nginx config for ui.localhost domain, you create a rule that says "everything that starts with /api, intercept it, remove /api at the start of the path and send the rest to http://api.localhost, ending up with http://api.localhost/resource"
I do frontend and backend development and I have this setup with docker-compose, the config for nginx is really trivial and widely available in many tutorials.
That's why we have various controls with proxies, such as including the original requester's IP etc.
It's irrelevant who actually asks for data if you pass the HTTP request info unaltered (except path parameter), the WAF can do its job. That's the beauty of HTTP and its stateless nature. You can scale infinitely and do various actions such as this one and get the expected result.
That works for first-party JS. Doesn't work for a public API used by others.
Edit: Specifically purely client-side apps. For someone hosting a static HTML+JS app, it's annoying to have to set up and run a server-side route just to circumvent CORS.
(Maybe not so bad with something like Next.js, where it's easy to add a backend route to your primarily static website.)
And it adds an extra hop of latency to every request.
GraphQL
ducks
And now you have two problems
GQL is a bit ugly, but works well, kind of standardized, etc.
Is there something similar for providing a batch endpoint for OpenAPI requests?
GET /blog/1?with=comments,author&only=title,body,created_at,comments.body,author.nameJMAP is very well suited to CORS due to this: https://www.rfc-editor.org/rfc/rfc8620.html
I wish they'd standardize HTTP QUERY soon: https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-met...
I was about to ask if OPTIONS would be sufficient, but it looks like some of the MDN URLs suggest just that.
But it might be easy to add? https://github.com/dropbox/dropbox-sdk-js/blob/main/src/drop...
Make sure to always set the URL parameter "reject_cors_preflight=true", which will make sure you're not inadvertently triggering pre-flight requests.
So I started thinking about how we did things pre-CORS and I've been wanting to build this frame-proxy in our API. Pretty helpful!
I've also built a tiny "just tell me what to do" SPA for debugging your CORS configurations that everybody reading this might find useful too: https://httptoolkit.tech/will-it-cors/
At Clerk, we took the opposite approach, and restructured our API so it fits within the narrow window that does not require a preflight
I wrote a little about it here: https://clerk.dev/blog/skip-cors-options-preflight
In practice for our app we rolled out with 600 to make sure things were working. And now we run at 28800 (6h) (but 2h on Chrome)
Your approach definitely sounds sane, would encourage others to do the same rather than following the article from the get go.
> not everyone even respects TTL
This used to be a problem with at least one common DNS cache, where it would see a very small value as an error and apply its own default (24 hours IIRC) instead. 10 minutes was fine, but 9m59s and it would not update until next day (the threshold may not have been 10 mins, it could have been 500s (8m20s), but it was something of that order).
I'm pretty sure that is not longer a common DNS daemon, or if it is that behaviour has been fixed, so these days I'm not really concerned for my projects. For work things I might be a bit more restrained with short TTLs, just in case (for personal projects I can take the “it is not my fault your DNS setup is broken” line, but that sort of attitude doesn't always fly in a commercial environment!).
However, once I comform that everything is working alright, I crank up TTL to one day to reap some performance gains. For sites with low visits, and likely to not have the DNS query cached, the query latency can add up, and sometimes goes as high as 100ms if the recursive resolver has to take several round-trips to resolve the whole domain chain at EDNS level.
These days chrome hides the preflight requests by default and you miss to notice the latency added for each of those CORS calls.
Also we don't deal with CORS unless it's an external plugin that we include in our site. Earlier we had subdomains like api..com, static..com to parallelize network requests which required CORS to be setup. With H2 we got rid of all of them and load everything from a single domain. This reduced the CORS surface area.
For example, to allow abc.com AND bcd.com, you could check Origin and if correct return Access-Control-Allow-Origin: *. In this case, setting Access-Control-Max-Age will mean this applies to any site* after a single successful request. If you return the site's name, this will break CORS* for that amount of time if an attacker makes a single request to it.
What if the API is shared between multiple domains? Do you need to reverse proxy it everywhere?
What if it is a public API for third-party sites?
> What if the API is shared between multiple domains? Do you need to reverse proxy it everywhere?
It would depend on your specific setup. We use a single nginx server entry to proxy many domains.
> What if it is a public API for third-party sites?
Then this approach would not be viable.
> cross-origin API requests will require these preflight requests, notably including: … Any request including credentials
No, setting XMLHttpRequest’s withCredentials:true and fetch’s credentials:"include" to send the user’s Cookie with the request does not imply that a preflight request must be made, since <script> and <form> sent the Cookie with cross-site requests (back in the CSRF days when the default Cookie SameSite flag was effectively SameSite=None). Maybe he was referring to a custom header such as Authorization, which does trigger prefetch.
There, that wasn't so hard, was it? I assume this is written for web developers who find this the most familiar acronym in the world, but ... still, it would not kill anybody to include the definition of the acronym, perhaps even with a friendly link [1] to make it Even More Accessible.
I'll be off looking at the lawn mowing robot, now.
I do appreciate your point of view, but inclusiveness shouldn't come at the cost of brevity when those who would be included wouldn't benefit from it.
Me: web developer since 1996. I had to look it up. Came here to make the same comment, and see you being lambasted for it.
Don't let these haterz get you down. It's standard practice across ALL domains to define acronyms, and those who give this article a pass because ReASonS!! aren't people I'd willingly choose to work with: anglo-saxons who believe the whole world shares their lived experience, and their mental model.
Hacker news is just horrible for that attitude. Accessibility is a thing.
I guess I need to work on my tone, less snark more helpful. It's a bit comforting at least that people like you exist, who are web developers but still didn't know this one. :)
If instead this said something like: "For the uninitiated, CORS stands for ...", I don't think it would've attracted any downvotes.