Hacker News new | past | comments | ask | show | jobs | submit login
You don't need that CORS request (nickolinger.com)
382 points by olingern 4 months ago | hide | past | favorite | 243 comments



You don't even need an SPA. Having the backend + frontend share the same state makes development so much faster and easier it's shockingly refreshing. When building a SaaS project last year I decided to just ditch React/Angular/Vue and did everything server-side like it was 2005. I was extremely pleased at the result -- everything feels snappy, secure, and native for the browser. I was shocked at how much time and headache I saved: not having to design/deploy an API and all the trimmings (ACLs, CORS, JWTs etc.), not having to spend X hours a day wrestling with Webpack/hot reloading/npm/tree-shaking/state management, having direct access to backend secrets/state/APIs and just spitting out HTML, it's just so....lovely. It simplifies performance tuning as well, the only thing I need to optimize is the server, which is easy to do because I have 100% control and visibility, compared to debugging/optimizing JavaScript for a million different clients.


And as a bonus, your site works perfectly in degraded environments (poor network conditions, no JS runtime). It's really the best way to do things: SPAs are great for applications, but for 99% of websites server-side rendering only has advantages.


> And as a bonus, your site works perfectly in degraded environments (poor network conditions, no JS runtime).

What? No. Not at all. If you're behind a subpar network, your dynamic HTML web app does not load/refresh/update at all, and your users start to get frustrated because your crappy webpage is broken and fails to even do the most basic things.

This is not the case with SPAs, and some of the most pressing problems they solve: perceived performance, resilience to faulty network connections, and overall improved UX.

Let's put it this way: with SPAs you can design your app to work even without a working network connection. That's how resilient SPAs are to networking issues. How do you pull that off with dynamic HTML?

> but for 99% of websites server-side rendering only has advantages.

No, not really. Unless you cherry-pick what goes into the 99%, even basic CRUD, form-driven pages the dynamic HTML way suffers from a multitude of drawbacks that are no longer issues in SPAs, both technical and organizational.


> This is not the case with SPAs,

LOL. I'm yet to see a single SPA which can tolerate a bad network. You certainly have better tools to do it, but it still takes a ton of effort and nobody does it. So it ends up failing in worse ways because now the application just stalls and you don't even get the standard browser errors/timeouts.

Unless SPAs are built as "offline first" I haven't seen a single one which works better than a traditional MVC app in such conditions.


Yeah, have fun building an "offline first" app if your scope is anywhere in excess of Postman.


The advantages you listed are potential advantages but the reality is often different. Those features don't come for free, and given the number of SPA's out in the wild it's rare that functionality like offline mode or gracefully degrading when running into faulty network connections are actually built, let alone maintained.

Most of the time SPA's degrade very poorly and instead of breaking they often appear as if they're somewhat working but the site is actually in a broken state and will need a complete refresh to become usable again.


> If you're behind a subpar network

There is a (high) probability users will close the tab after waiting for over a minute for MBs of JS to download and parse.


> There is a (high) probability users will close the tab after waiting for over a minute for MBs of JS to download and parse.

I'm not sure what leads you to believe that a SPA requires "MBs of JS" to work. I've worked on a popular SPA deployed to multiple regions and with tons of localization data, and the total payload stayed well below 1MB. Plenty of dynamic HTML content, specially images, require far more than that.

In fact, this very discussion on HN, a very spartan dynamic HTML page, currently requires over 250kB as it dumps all the threads regardless of whether you read them or not. As a contrast, Reddit's frontpage takes 750kB.


Wow, the fact that you compare Reddit to HN, and you consider the Reddit SPA to be "better" means you have a fundamentally different notion of better. For me, Old Reddit and Hacker News load fast, feel snappy and native, and work better in every way, while the new Reddit SPA is sluggish, full of random network errors and bugs that I encounter regularly just clicking around, and takes up a ton of memory/CPU just to browse a discussion site.

And Reddit's frontpage size is 1.3 MB according to one check I did which checks the size of all network calls after loading, etc. and that's WITHOUT all the lazy-loaded content below the fold.


> And Reddit's frontpage size is 1.3 MB

Ugh, the two chat scripts are 944KB + 1.30 *MB* alone [0]

Full page load (without touching anything at all) is 12.87MB (6.58 on wire) with 273 requests.

[0] https://imgur.com/a/bHf9CQb


> I'm not sure what leads you to believe that a SPA requires "MBs of JS" to work.

> In fact, this very discussion on HN, a very spartan dynamic HTML page, currently requires over 250kB as it dumps all the threads regardless of whether you read them or not. As a contrast, Reddit's frontpage takes 750kB.

You can use tools like your browser’s developer tools or webpage test to accurately measure this. It seems like that would be a useful skill to develop for accurately reasoning about SPAs and understanding why your beliefs don’t match real users’ experiences.

For example, Reddit.com is actually 10MB of transfer, not 750KB, and it takes 5 seconds to process 6.7MB of JavaScript (47 requests to transfer 1.8MB of compressed JavaScript) which delays the start of rendering until 2 seconds in and requires almost 10 seconds to render the top of the page.

https://webpagetest.org/result/220104_AiDcHT_cf05f20e55718cf...

In contrast, the HN page requires a TOTAL of 62KB to render in less than a second:

https://webpagetest.org/result/220104_AiDcAH_1c302fd66a183aa...

That’s an enormous difference which is extremely noticeable if you don’t have a fast computer and network connection, and that’s before you get to the serious bugs Reddit has because their SPA doesn’t handle errors or session timeouts well (I see this on a near-daily basis).

I have a recent iPhone and the new Reddit SPA lags notably behind the old version, which renders twice as fast and is more reliable because it uses a tenth of the code:

https://webpagetest.org/result/220104_BiDcDZ_9bdbf4482d84584...


> Plenty of dynamic HTML content, specially images, require far more than that. (...) this very discussion on HN, a very spartan dynamic HTML page, currently requires over 250kB

There's the difference: HTML-first approach will be able to draw everything using very little resources by the time i've loaded a few hundred kilobytes. With JS bundles i first have to wait for the scripts to load, then the dance of unreliable network-roundtrips starts and can fail in mysterious ways unknown the the browser UI.

Images will not prevent the browser from drawing the window, and they can be lazy-loaded if you consider that's better UX (of course the browser can decide to override that setting to respect user preferences). Time to full render is orders of magnitude better on simple HTML/CSS (what browsers were designed to render) than with a crap bundle that's going to trigger network connections and DOM changes (so full page redraws) in mysterious ways.

Also, i love when my refresh and back button in the browser UI do what they're supposed to do. I'm not saying it's impossible with an SPA, that's just something most SPAs i stumble upon are completely incapable to respect.

I encourage you to run the experiment. Take out an old Pentium 4 with 2G RAM, setup Firefox and in the developer bar (f12) turn on network's throttling. It will not be a realistic simulation as you would need packet loss (which other tools can simulate) but it will certainly help you measure performance and make informed decisions.


in an SPA, that's cached, so not the second time


Have you measured this carefully? All of yours users will notice it on the first visit, so you need to think about that first impression but it’s also rarely the case that people load your page so frequently that everything stays in the cache forever. Mobile devices have small caches which purge more frequently than most developers think and that’s also true of CDN nodes, and continuous deployment acts as a ceiling for long-term caching.

When I’ve measured this for real users on moderately busy sites (6 figure daily users, not Google-scale) the reported first page load times tracked the cold cache times for most users (70+%) even for people who’d visited before. If they were geographically clustered (news story, etc.) the cache hits on CDN nodes would be high but you couldn’t count on that.


What exactly are the "multitude of drawbacks", that basic CRUD pages suffer from?


Im not the OP and not sure about multitude but one problem for a successful app is bandwidth usage from transferring redundant chunks of HTML, and potentially JS and CSS


Assets like JS/CSS can be cached, and unless you're doing something very convoluted HTML overhead should be pretty low. I mean HTML was standardized in the 90s when we had <100Kb/s (not KB/s) bandwidth so i understand if you have a "successful app" you're always going to need to deal with scaling, but it's not exactly a problem for most of us and caching reverse proxies are very easy to operate and stable nowadays.


So you’ve said that you don’t have this problem because your not a successful app? Or are you denying that there’s duplicate HTML, and sometimes JS and CSS being sent? 100k for 100 users is 10mb and bandwidth is one of the easier ways to save costs.


> What? No. Not at all. If you're behind a subpar network, your dynamic HTML web app does not load/refresh/update at all, and your users start to get frustrated because your crappy webpage is broken and fails to even do the most basic things. > > This is not the case with SPAs, and some of the most pressing problems they solve: perceived performance, resilience to faulty network connections, and overall improved UX.

That's the sales pitch but here's what actually happens for the vast majority of time: the user gets frustrated because after 5 minutes the SPA finishes failing to load and all they have a blank page or, if it's a site they visit so frequently that all of its dependencies tree are still in the browser's cache, they get the UI shell but nothing works. As a bonus, the assumption that the megabytes of JavaScript would manage state often means that core web functionality like the back button or reloading do not work so while a CRUD user would be able to hit reload and get the expected result as soon as their network connection improves, the SPA user will have to start over from the beginning.

It is technically possible to build things which work offline but in the real world that doesn't work for most applications for a number of reasons:

1. The app depends on things which need to make network requests — you can give a nicer error page but that's not going to make your users happy.

2. The developers ship updates often enough that cached versions aren't current — e.g. you might have all 50MB of dependencies in the cache but if the user got the HTML referencing different URLs, that doesn't matter.

3. The developers forgot to test that they allow long-term caching or serve-stale on their assets.

4. There's a big difference between being completely offline where the interface status shows down and high latency / packet loss. The most frustrating experience for the users are the latter because things act like they're going to work and if they were doing an activity which changes state it might not be clear whether something succeeded or not. The offline API doesn't work for that and it's almost certain that your SPA doesn't really handle it because:

5. Statistically nobody tests in those degraded conditions so they end up with the Twitter-style SPA where it loads the core of the UI structure but then nothing renders. This is better than a server timeout page only in that it allows you to try to convince the user to be less frustrated.

The reason why this is almost always better for server-side rendering comes down to the increased client footprint of an SPA. With an SPA you're depending on an enormous amount of code to load and run before the user sees anything and there's a lot more that can go wrong in ways you didn't handle, which is why it's so common to learn something is an SPA when you get a “successful” blank page. Servers certainly can have problems but most of the moving parts are under your control so you have better visibility and control, and there's no better way to handle degraded conditions than to depend on them less. If the network is slow, trimming your transfer by 1-2 orders of magnitude is going to do more than almost anything else to improve the user experience.


Thanks! You've summarized my experience with SPAs pretty well. Do you have or know of a detailed blogpost on this topic with concrete examples?


Could you link to some of the stuff that you are mentioning, maybe an interesting article if you remember one offhand? I think what you are saying is controversial due to the other child comments, which is intriguing because I think what you are saying is interesting and I am hoping you might point me to a larger discussion on the topic.


Caching via service workers and the app manifest would be two places to start.


Wait what? Other than "the client blocks JS" which is niche beyond niche the SPA is the one that actually performs better with a spotty network. Fully offline or "occasionally connected" is half the reason to use an SPA. Just as an example once downloaded FB messenger is fully functional over an unreliable network.


Hum... I have never seen a SPA perform well with a spotty network.

Yeah, I get what you are trying to get, that an SPA can retry failures, prioritize resources better, reload less data... They just never do that competently, as it is a shitload of work, and the "once downloaded" part is irrelevant, because people only ever load a couple of pages on most sites visits.


Did you try it? Try with 256Kb/s and 20% packet loss (and if your networking tooling allows it, bursts of >1s downtime every 20s) and tell me which one performs better. Yes that's actual conditions in which many people access the Internet in 2021 whether it's on cellular or crappy xDSL.

I've yet to see a single SPA that can deal with such conditions, while native HTML/CSS will work like a breeze in any web browser. For bonus points, browser UI can tell you which requests take a long time and/or refresh any page while maintaining a cache, something most SPAs are really bad at doing, and you get SEO/interoperability for free due to your markup being a curl call away from any client.


And the thin/thick client/server wheel continues to turn.


Why does this always come up? Maybe the author does need the SPA. This comment does not seem particularly relevant to the contents of the article.


Because if you're re-examining your need of CORS and evaluating whether it makes sense to instead host everything from the same host, you can also re-examine your need of an SPA and evaluate whether it makes sense to instead just render everything server side; it'll solve even more problems if you can.


> it'll solve even more problems

Wow. Why doesn’t everyone do this?


Actually, a lot of people do. Ever heard of Laravel, Django or Ruby or Rails? Outside of the React/JS world, a lot of developers are happy solving real business problems instead of working with hacks that were built on top of browser DOMs.


I mean, to be a bit more charitable, when you have use cases that require server pushes, the traditional server rendered pages don't work so well, and that is where SPAs make sense. Mind you, a lot of the frameworks for rendering server side have started to include natural ways of handling those updates (Phoenix Liveview for instance being one I'm familiar with), that makes it so you likely don't need an SPA in those situations, but there are still use cases where an SPA may be simpler or may enable a UX that otherwise would not be possible.

But to the apparently snide response to my original comment, while there may be reasons for SPAs, there are also plenty of uses of them that could be more simply done via server side rendering, especially with the place such frameworks have moved towards, in enabling server pushed updates.


That’s great to hear. Hopefully I can grow up to be one of them some day.


Some people's careers are based on JS frameworks and managing (or perpetuating) frontend complexity. How many will voluntarily put themselves out of a job?


CORS is just one more problem from the class of performance problems that only SPAs have. Seems very relevant to me.


> only SPAs have

That’s a bold claim.


I prefer laravel as well but it’s definitely not as snappy as an spa since I have to load a full page of html at each click. What are you doing differently that I should be doing?

Currently, I’m doing a hybrid sometimes where I’ll have jquery do a call inside the page when I want one particular area to be fast.


Check out htmx. I use it for thing like table pagination.

Server side I check for htmx headers, and if so omit headers/footers are just return the body. htmx then handles the html swap and boom, you’ve just turned the page without reloading the whole page.

Simple and straightforward. I also use a plugin to preload the responses on mouseover in some cases for even more snapiness.


KISS. Keep the HTML minimal and the full page reload is often just as fast as an AJAX call. For the parts that need to be realtime/interactive I use Livewire. Livewire is annoying to work with sometimes, and the author got carried away with that whole AlpineJS nonsense (reinventing the whole problem), and Livewire is still immature, but the basics work well (just keep your Livewire components extremely simple and it works great).


Yeah, I cant count how many times I have seen SPAs have tons of calls to an endpoint that passes back huge amount of data when the client needed a single property from the response. This can get artificially bad in large companies that require devs to onboard each endpoint through an API exchange/gateway...because they avoid creating new endpoints.


Keep your markup lean, serve styles separately so they can be cached.


Take a look at the tall stack. It abstracts away the js that does partial page updates.

https://tallstack.dev/


How long does it take your server to render the page? That would directly translate to client-facing latency. You want that response time to be as low as possible, definitely under 100ms.


+1

I find Laravel + Unpoly a great combination.


This is exactly, what I wanted the frontend developers to do. Junior developers introduce new library/framework just because it looks "cool to work with", later it becomes a problem, which will then be replaced by another library (its a endless loop these days).


No contract between the front and back end is obviously an extremely tight coupling. I’ve seen this end really badly when you have to cleave them after the fact (particularly with embedded JS in server side templating). For a small product without much velocity, I’d be OK with the approach.


What did you use for your back-end and for spitting out the HTML?


Laravel. I have no particular love for PHP, but Laravel just "gets it right" and understands the needs of a web developer. After years of working with Python/NodeJS (Flask/Express), Laravel was like a key that fit a lock. The ecosystem of paid plugins and services around it are really well built as well.


I've always said that frameworks, tools, ecosystem, libraries, community, etc are far, far, far more important than the programming language.

Also, nowadays PHP is not 10 year's ago PHP, same as nowadays's JavaScript is not 10 year's ago JavaScript. They're both "ok" languages to me.

And yes, having used for many years Django, some playing with Rails on the side, and then everything under the sun for Node, I 100% agree Laravel is freaking awesome and I really wish I had discovered it earlier.


I love when devs blow off SPAs because they think they're geniuses or above JS. You spun up a shitty 1995 style site that every menu on the toolbar triggers a page reload and patted yourself on the back. Congrats on sending the same html and css with every page load, I'm sure your low network speed users love it.

Eureka exclaimed the "full-stack" developer, who needs javascript!?


> Congrats on sending the same html and css with every page load, I'm sure your low network speed users love it.

If only browsers could somehow retain the parts of a web page that did not change, like images and css and js, in some sort of local storage or cache.


You can probably express this without personal attacks


The HTML is probably smaller than the JavaScript blob.

If you can get the HTML within the same order of magnitude as JSON itself, then it's perhaps a compelling argument to ditch SPAs. The bandwidth delta would be minimal, and the development overhead would be much less substantial.

Note that this is how the web used to be. Small HTML markup.

We even started baking structured meaning into the HTML (Semantic Web, "Web 3.0") so that documents could be consumed like APIs. But then JSON and the platform giants put an end to p2p web payloads and rich document schemas. That was a mistake.


Probably you're not aware yet that there are also "modern ways" to do server side templates/html. Nowadays you have things such as Unpoly, Htmx, Hotwire, Livewire, etc which can be added incrementaly to any traditional MVC applications leading to an incredible reduction in the amount of work necessary to ship something robust if you're a single developer or in a small team.

SPAs can be great for large teams with different skills/profiles, and large companies with a lot of money though.


Unfortunately, this kind of discussion ends up turning into a knife fight these days here.

I tried using HTMX recently for mainly a CRUD app for personal use, with limited dynamic loading. Still felt the need to rely on JS(Jquery) for loading images and triggering refresh on list of items without full page reload[1].

Did a small A/B test from dev POV using Vue.js, as it is one of the more lighter frameworks. Ended up leaning more towards Vue.js, as had to do some non-conventional(from HTMX POV)[1] things for a simple CRUD app not relying on full page loads/refreshes. Hopefully there is another step in this evolution.


would you mind reaching out to let me know where htmx fell down for you?

htmx at bigsky dot software

or you can jump on the discord if that works:

https://htmx.org/discord

thanks for taking a look at htmx


You have Cache-Control and Expires headers for this. Old shitty 1995 style I know.


This is a harsh but correct take. Additionally separating your backend logic into an API makes it easier to write more clients for your service. This benefit can’t be overstated.


If your HTML-generating code interfaces with a clearly-defined internal API, then your JSON API can be just a thin layer that authenticates the user and provides an HTTP front-end to your internal API.

Essentially, your web "client" is now the HTML-generating server-side code instead of a JavaScript app. Other clients are unaffected and can work the same as they otherwise would.


I've never worked with a web application where the requirements of the "customer facing API" and the "drives the web application API" (or the "drives the mobile application API", "drives the internal admin app API", etc.) matched for very long, if ever. Customers will usually need a small subset of the API that your web app does, the authentication will likely be different, you will need to make optimizations in your web app fetching that will be highly tuned to your specific use case, and a whole bunch of other concerns that make the idea of your web application being driven off your public API unrealistic.


Sure it can. Such as implying "the only way you'd be able to separate your backend logic into an API is if your browser client uses it".


Even if I knew I needed an API and multiple clients with 100% certainty (and most products don't), I would still prefer to build my browser client deeply integrated without separating the backend logic. The benefits of having at least one platform with faster dev speed cannot be overstated.


Ya, it all really comes down to the problem you’re solving and what makes sense for the task at hand. The point I was trying to get across in my other reply to you is that sometimes the thing that makes the most sense is an SPA (even if it’s less performant, or takes a little longer to build).


CORS is such an ugly web standard. It's not only a pain in you butt, but it also makes your requests slow through these extra pre-flight round-trips.

I have hope that we can remove it, though, in new versions of HTTP.

CORS only exists because XMLHTTPRequest broke the assumptions of web 1.0 servers. Suddenly any web browser loading any page anywhere could make a request to your server without the user's explicit permission, and a ton of web servers had already been built and were running under the assumption that users would only hit their endpoints by explicitly typing in a URL or clicking on a link.

But each time we design a new version of HTTP, we get an opportunity to remove CORS restrictions for it, because there aren't any servers running the new version yet.

And with Braid (https://braid.org), we're making changes that are big enough that I think it really warrants taking a fresh look at all of this stuff. So I have hope that we can eliminate CORS and all this ugliness.


> CORS only exists because XMLHTTPRequest broke the assumptions of web 1.0 servers. Suddenly any web browser loading any page anywhere could make a request to your server without the user's explicit permission

This had always been true; making cross-origin requests with e.g. <img> tags is still very much a thing.


This is why we can make GET and POST requests without an OPTIONS preflight...

Apparently, since developers should already have been protecting against cross-origin requests on GET and POST via other means (CSRF tokens), there was no need for the additional preflight protection.

They only added preflights to requests that browsers couldn't make previously.

Source: https://stackoverflow.com/a/39736697/114855


I'd rather deal with stateless CORS than having to muck around with CSRF again, though.


This is also the reason you can still POST text/plain to random TCP servers on the user's local network, without pre-flight check.


That’ll be gone soon: https://web.dev/cors-rfc1918-feedback/


True. CORS exists because the web browser would send the user's cookies (ie auth) for your site to you, when the user doesn't think they are on your site, but it's really malicious code from another site the user was visiting, acting as the user on your site without the user's knowledge.

Thats the threat model CORS is meant to address. Not just in general that a web host might not want a request from "anywhere" happening, but that site B might not want an authenticated request on behalf of User X to site B being made by code from site A.


That still doesn't distinguish the XmlHttpRequest from the <img> tag. They both send cookies.

You're right that sending cookies is the problem CORS addresses, but XmlHttpRequest didn't break any assumptions that were valid before.


It is much more difficult (perhaps impossible on a properly-implemented site?) to interact with a site on behalf of a user in harmful ways through an img tag -- Like, say, getting their checking account number, or transfering money from their checking account to yours, or resetting their password or email address. It's difficult/impossible to do that kind of attack only through an <img> tag, because Javascript can't see the body response of the URL in an <img> tag even if you point it at a non-image, and additionally an img tag can only be used for GET requests.

I didn't say that XmlHttpRequests "broke any assumptions that were valid before", I don't know exactly what that means.

If an XmlHttpRequest were allowed to do arbitrary cross-site requests, Javascrpt could use them do some of the kind of attacks mentioned above. Which is why XmlHttpRequests can't do cross-site requests. CORS exists to let XmlHttpRequests do cross-site requests only in an opt-in way, so a site can opt-in to it in ways that it knows/believes are secure from attacks like above.

I'm not saying the overall web security story is perfect (nobody would ever say that!), but that's where CORS comes from, and why it's different than img tag.

The common misconception is that CORS exists as some kind of general access control to your website. It only exists to keep the user of a well-behaved browser safe (in certain ways) from malicious Javascript on a site they are visiting. CORS has nothing at all to do with any scenarios involving malicious browsers, which are of course free to ignore CORS.


> (perhaps impossible on a properly-implemented site?)

Well, "proper implementation" would prevent a lot of attacks. But there's nothing stopping you, as a web server, from making state changes in response to GET requests, and it's far from unheard of.


I don't understand what you're trying to argue about.


The thing XHR added was:

1) POST.

2) Of arbitrary content.

3) With an arbitrary Content-Type header.

You don't get an OPTIONS for the sort of XHR request that you could do with an <img> tag (always GET). You don't get an OPTIONS for the sort of XHR request that you could do with a <form> tag (GET or POST). You only get OPTIONS if one of the following is true:

* Your request method is not GET/HEAD/POST

* You set a header value for a header other than Accept, Accept-Language, Content-Language, or Content-Type.

* You set Content-Type to a value other than "application/x-www-form-urlencoded", "multipart/form-data", or "text/plain".

* You have upload listeners on the XHR upload.

* You use ReadableStream in the request.


True, though POST requests claiming to send form data are among the most threatening requests (and still would be if they weren't immune to CORS). It's a weird hole from a security perspective, though as you note it makes perfect sense from a historical-development perspective.


Also 4) ability to see the text results of your request.

No way for JS to do that with an IMG tag, I don't think.


If the XHR is same-origin then you control that text anyway, so know what it is (and there is no OPTIONS anything going on), and if it's cross-origin then the text results are not exposed by default (unless the server jumps through the other CORS hoops to expose them). The OPTIONS bits are not relevant to this part.


Instead of sending a request before every defined case in the CORS policy, it could be implemented requesting only one GET to a plain file (fully cacheable) with the domain policy definition.

Like robots.txt for the search engines.


Except CORS policies can be dynamic, depending on the request context.


It doesn't seem impossible that the static file could have an indicator for "send an OPTIONS in this case". 95% of CORS setups don't need to look at the request beyond the origin requesting it (and I'd wager that almost all CORS-enabled APIs just blindly allow everything).


This is how Macromedia Flash handled it a lifetime ago


Flash was 20 years ahead of its time (even though it already had its share of fame).

Apple killed it to pave way for the App Store. It was a shame because around that time Flash was at its peak of innovation, with stuff like Alchemy (whose modern day equivalent could be WebAssembly) and Flex (no-code thingy that was TRULY catching up, only 10 years ago). It was also starting to implement features like atomics, shared data buffers, app bundles, hardware-accelerated 3D, etc... They also went to great lenghts to open their VM, and the ecosystem that was being generated around that was awesome (anyone here used haXe back then?). It was also present on like 99% of devices already and it was the only thing at the time that truly felt like "write once, run anywhere" (HTML wasn't as feature-rich and still quite fragmented between browsers).

Flash was the single biggest threat to Apple's planned business model, and with it gone, Apple's road to becoming a trillion-dollar company has been a walk in the park.


Besides loading resources which was mentioned by sibling comment, some websites still rely on submiting forms cross origin, even with cookies! XMLHTTPRequest is not required. iframes exist, so the user doesn't even have to see their browser make those requests.


What is braid? I just get a blank page.


Try loading again, seems to be working now. Braid is an IETF draft proposal (and larger ecosystem) around bringing state synchronization to HTTP.


Cross-Origin Resource Sharing (CORS)


No, it's not ugly. It's necessary. And we don't need any additional tooling around it or yet another thing that "fixes" what doesn't need to be fixed.

If you control domain.com and api.domain.com, then you can create a proxy that glues the two to the same domain, getting rid of any CORS annoyances forever. And you use the tech that exists. The whole problem takes less than 1 minute to type, test, deploy and there's no need for yet another big thing invented to solve a small problem that occurs due to ignorance of self-proclaimed "developers".


Yes it is ugly. How many learners have been stumpped by CORS errors over the years?

But it's also necessary.


I think it has some ugliness to it, but the idea behind it is necessary. The restrictions it places on front-end scripts are currently very challenging to work around, particularly if you're trying to implement a strict CORS policy on an existing website.

It's difficult enough to implement that it probably won't be implemented on the majority of the web - and maybe that's okay.


> And we don't need any additional tooling around it or yet another thing that "fixes" what doesn't need to be fixed

This is what CORS is though.


The irony in this is that with CloudFront at least, it can't strip the "/api" prefix before sending it to your backend, so you have to have your backend server understand /api/xyz rather than just /xyz calls like when it was on a dedicated api subdomain.

But don't worry! AWS thought of this! They invented Another Cloud Thing, namely Lambda@Edge, to solve this. Now you can run a JS function for every single request that hits your CloudFront Distribution so that you can run logic in there to strip the prefix before it gets passed to your origin. You read that right! Execute code every time a request hits your proxy! But wait, doesn't executing code for every single request sound insane AND doesn't it also add latency which this was trying to remove? Yes! Isn't cloud just lovely?


> They invented Another Cloud Thing, namely Lambda@Edge, to solve this.

Yes, designed for usecases like those handled by Cloudflare Workers, which boil down to updating data cached in edge servers without requiring global redeployments or pinging a central server. We're talking about stuff like adding timestamps to images or adding headers to HTTP responses or pre-rendering some HTML or emit CDN-aware metrics.

> Now you can run a JS function for every single request that hits your CloudFront Distribution

Not exactly. Lambda@Edge are event handlers from CDN events. You use them when they suit your needs.

> so that you can run logic in there to strip the prefix before it gets passed to your origin.

Your strawman example doesn't even feature among the dozen examples provided by AWS regarding how to use Lambda@Edge.

Everyone is free to come up with silly ideas and absurd examples, but if you design systems around braindead ideas then that says a lot about you and nothing about the tools you chose to abuse

> You read that right! Execute code every time a request hits your proxy!

Yes, that's what web servers do. What exactly is your point?

> But wait, doesn't executing code for every single request sound insane

It doesn't. That's what a web server does. Moreso, Lambda@Edge (and Cloudflare Workers too) only do it if you explicitly decide to make them do it, to match precisely what you tell them to do.

What point are you trying to make, exactly?

> AND doesn't it also add latency which this was trying to remove?

It does. It adds tens of milliseconds when the alternatives can add hundreds of milliseconds. You're also expected to do basic engineering work and do basic performance work when seeking performance improvements, such as measuring things instead of mindlessly jumping on bandwagons without caring for the outcome.

> Isn't cloud just lovely?

I feel your comment manifests too much cinicism to cover too much ignorance on a topic you are not familiar nor understand the basic premise.


You shouldn't be so stern. The use case I provided is AWS's recommended way to solve this problem[0]. :)

> Yes, designed for usecases like those handled by Cloudflare Workers, which boil down to updating data cached in edge servers without requiring global redeployments or pinging a central server. We're talking about stuff like adding timestamps to images or adding headers to HTTP responses or pre-rendering some HTML or emit CDN-aware metrics.

> Not exactly. Lambda@Edge are event handlers from CDN events. You use them when they suit your needs

> Your strawman example doesn't even feature among the dozen examples provided by AWS regarding how to use Lambda@Edge.

For all of these counterpoints, see [0].

> Everyone is free to come up with silly ideas and absurd examples, but if you design systems around braindead ideas then that says a lot about you and nothing about the tools you chose to abuse

I take it you are directing this particular comment at AWS considering they are recommending this solution? [0]

> Yes, that's what web servers do. What exactly is your point?

Don't be daft. You know what I mean. Obviously web servers execute code. In this case the web server (CloudFront Distribution) is passing the request to yet another "thing" which happens to be a JS function that is invoked specifically to handle something web servers like nginx were built to do extremely efficiently on their own. CloudFront could easily support this without additional dependencies that add complexity to your system design and introduce additional failure modes. In this case, I am making your point for you: using Lambda@Edge for this IS ridiculous, but that's the AWS way.

> It doesn't. That's what a web server does. Moreso, Lambda@Edge (and Cloudflare Workers too) only do it if you explicitly decide to make them do it, to match precisely what you tell them to do.

> It does. It adds tens of milliseconds when the alternatives can add hundreds of milliseconds. You're also expected to do basic engineering work and do basic performance work when seeking performance improvements, such as measuring things instead of mindlessly jumping on bandwagons without caring for the outcome.

Sorry for not writing a detailed blog post to describe all of my findings. FYI, the latency added with Lambda@Edge caused my request time to double and added much more variance.

I'm surprised a lot of your counterpoints are just blaming me for doing the wrong thing, when this is actually what is recommended by AWS. Invoking a custom function to process the request. See [0]. Of course the right solution is to use nginx (but then you lose out on AWS scaling) or completely redesign your system to fit another solution like API Gateway.

From AWS's blog:

> In this scenario we can use Lambda@Edge to change the path pattern before forwarding a request to the origin and thus removing the context. For details on see this detailed re:Invent session. [0]

[0] https://aws.amazon.com/blogs/architecture/serving-content-us...


>But don't worry!

I don't, I can just teach my backend to process /api. The backend is running code under my control anyway. This seems like one of the least difficult "problems" to solve in any non-trivial codebase.


I don't want to argue that the feature should be built in (and most importantly free, because that's something you forgot to mention, those lamdba functions are paid for each run) but

> doesn't executing code for every single request sound insane

How would you strip some url path without executing code for every single request? It could be code you did not write, but some code is always needed to do something. If latency of lamdba@edge is an issue, you can try CloudFront functions, which should be better for small tasks like this (though I don't have any experience with them as we switched to a similar competitor)


CloudFront Distributions cannot pass the request to CloudFront Functions before sending to the origin. In other words, they cannot be used to modify origin request/responses. They can only modify the viewer request/responses. [0]

Only Lambda@Edge can help the scenario which I provided, which is also AWS's recommended solution. [1]

[0] https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

[1] https://aws.amazon.com/blogs/architecture/serving-content-us...


From your link [0] "URL redirects or rewrites" is a supported use case.

For stripping /api/ from the path, I don't know what would be the benefit of doing it at the origin request, rather than at the viewer request?

Cloudfront functions are pretty new, so some older docs might still reference lamdba@edge only.


The origin request is your backend. The viewer request is the one you're requesting from the client. You want the viewer to have /api. You want the origin to not have /api.

Therefore stripping must be performed on the origin request, which is not supported by CloudFront Functions.


Both the viewer request and origin request hook both happen early enough that you can update the path

See https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

I have used lamdba@edge and our primary use case was to rewrite url (to add languages information for example) so I'm sure it works in viewer request events.


No. Please see the many other responses I gave to other comments. It does not work because the viewer request is the request the viewer views. If you change the URL of the viewer request, CloudFront sends a 301 accordingly. In other words, changing /api/blah to /blah in the "Viewer Request" part results in CloudFront injecting a `301 location: /blah` and the client doesn't hit your backend because they got redirected. The client instead hits your frontend as a result of your URL rewrite. There is a very clear difference between viewer and origin. If you could rewrite origin requests at the viewer level there would not be any difference between the two. This is the difference. Viewer is what the client sees. Origin is what the backend sees.


My understanding is that you would need to rewrite the url and update the origin.

I could be wrong as I did this lambda@edge a while ago, but the main difference between the two request event is that viewer request is done in all cases, while origin request is done only in case of cache miss.

[EDIT] you are right, updating the origin is indeed something that cannot be done on the viewer side. My bad, I should have checked earlier.


Personally, I did it with templates in api gateway, as at least then I'm not paying for the cpu time to do these sorts of manipulation. But yes, stuff like this made me really shake my head at 'the cloud'


I don't know. This doesn't feel like something that should require edge logic. nginx handles this with a single rewrite directive, so I'd expect some part of the AWS routing layer to have a setting to rewrite the URL accordingly. OTOH, AWS can't and shouldn't account for every edge case, probably.


A rewrite directive is just writing code.


Rewrite directives handled by low-level routing logic of the web server are definitely different to a container image that gets spun up in a distributed network and queried for each request, don't you think?


Not really, no. In fact the only significant difference (that the Lambda@Edge function is run at the closest geolocation to the client, rather than on the origin server) is an advantage to Lambda@Edge.

What difference are you alluding to?


A HTTP URI path prefix mapping is done in nginx, in the same process, no context switching, no buffering, I think it is even done without any allocation.

Of course it doesn't matter, basically logging takes more resources than whatever routing is needed. (Especially because even if it requires running a Cloudflare worker it's just a NodeJS V8 isolate, very lightweight. As far as I know. But I have no idea of its costs.)

But AWS Lambdas are pretty pricey compared to a free nginx rewrite.


> But AWS Lambdas are pretty pricey

Kind of why CloudFront Functions we're invented for this use case, AWS Lambda@Edge is the more general purpose edge computing facility.


> (...) are definitely different to a container image that gets spun up (...)

I'm not sure you're making a informed observation. "Spinning up a container image" is just fancy jargon/handwaving to refer to launching a process, which is something nginx already does under the hood in a myriad usecases without warranting complains from users.

> and queried for each request, don't you think?

I'm not sure if either you framed your sentence poorly or you don't have a clear idea about what you're talking about. In AWS lambda, you don't "spin up containers" at each request. You launch your lambda once it's deployed or when it scales up, and it stays up as long as you see traffic.


Poor phrasing, then. Meant to say a container is started once, then queried on every subsequent request. And what I’m talking about is not the user-facing lambda, but what AWS does on the infrastructure layer below, which involves running a containerised process on firecracker VMs, communicating via HTTP. There was a great summary of how AWS works a while ago, going into way more detail, that I sadly can’t find anymore. But there’s definitely more work involved in calling a lambda function than forking a process in nginx.


Crafty alternative: just.. don't strip it? It seems like a perfectly reasonable and simple route that avoids complexity and cost to just keep /api.


It's just one of the many things unexpectedly out of your control when using a cloud provider.


Well, we've all read this thread so it's not unexpected anymore.


>doesn't executing code for every single request sound insane AND doesn't it also add latency which this was trying to remove? Yes!

but surely executing code at the source of your web app is more efficient than having the client and server attempt to perform it? faster than 40-90ms per request at least


> But don't worry! AWS thought of this! They invented Another Cloud Thing, namely Lambda@Edge, to solve this.

No, CloudFront Functions is the Another Cloud Thing AWS invented more specifically for this use case. Lambda@Edge is the older and more general purpose edge computing facility.

> You read that right! Execute code every time a request hits your proxy!

Any rewrite rule in any platform, and heck, any other functionality provided by the distribution, is executing code every time it gets a request (whether or not you are writing code for the purpose.)


CloudFront Distributions cannot pass the request to CloudFront Functions before sending to the origin. In other words, they cannot be used to modify origin request/responses. They can only modify the viewer request/responses. [0]

Only Lambda@Edge can help the scenario which I provided, which is also AWS's recommended solution. [1]

[0] https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

[1] https://aws.amazon.com/blogs/architecture/serving-content-us...


Not sure why you cannot strip in your backend, and it's a huge deal. Code needs to be run somewhere though. While Lambda@Edge sucks, Cloudfront runs functions now which is not Lambda@Edge, and faster because it executes on the PoP location. https://aws.amazon.com/blogs/aws/introducing-cloudfront-func...


CloudFront Distributions cannot pass the request to CloudFront Functions before sending to the origin. In other words, they cannot be used to modify origin request/responses. They can only modify the viewer request/responses. [0]

Only Lambda@Edge can help the scenario which I provided, which is also AWS's recommended solution. [1]

[0] https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

[1] https://aws.amazon.com/blogs/architecture/serving-content-us...


I've just tried it. The following function works exactly as-is when the function is configured as Viewer request. I changed it to append some text to URL.

See: https://github.com/aws-samples/amazon-cloudfront-functions/b...


That MODIFIES a viewer request, resulting in a viewer redirect. We want to keep the viewer request the same, i.e. with /api, and strip it only from the backend origin request.

The viewer request is the one you make from the client, with /api. The origin request is the one CloudFront sends to your backend, which, without using Lambda@Edge via the origin request, will get sent as-is with the /api prefix. You have to strip it from the origin request.

If you strip /api from the viewer request, your `domain.com/api/users` request becomes a redirect to `domain.com/users` which results in a call to your frontend instead of your backend.

The example you referenced solves a completely different issue, not the one we are talking about.


CloudFront is a CDN, it should cache static resources, not so much manipulate incoming requests - that's more a job for a load balancer / proxy like Elastic Load Balancer.


Fully agree here: I don’t expect anything else but reliable and performant HTTP caching from a CDN like CloudFront.

Request manipulation is not the duty of a cache - even though other CDN providers mix request manipulation functionality with caching. In my opinion, they don’t need to be in the same product.

If you still need request manipulation, because you don’t control the origin or you don’t want to introduce another service between CloudFront and the origin, you would use CloudFront Functions, which is cheaper than Lambda@Edge and easy to set up.


CloudFront Distributions cannot pass the request to CloudFront Functions before sending to the origin. In other words, they cannot be used to modify origin request/responses. They can only modify the viewer request/responses. [0]

Only Lambda@Edge can help the scenario which I provided, which is also AWS's recommended solution. [1]

[0] https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

[1] https://aws.amazon.com/blogs/architecture/serving-content-us...


CloudFront is CDN with limited features, other CDNs (Akamai, Fastly etc) are more than capable of manipulating incoming requests


Which is why CloudFront and Lambda are separate products. Lambda@Edge simply combines them.


We ran into the same issue and we had to use API gateway to rewrite urls. but it had the added benefit of handling CORS there and the preflight requests never hits the application servers. Additionally preflight requests are cached at cloudfront and at browser with "Access-Control-Max-Age" and for the first request we do pre connect during the main domain load so the users never notice the small latency it may have.

I agree that AWS products are all over the place.


"AWS products are all over the place."

It's the customers. They run into issues using something against all advice in the docs and then request features and services or AWS sees all the clamor around a perceived issue and they create a "solution."

Developers are really averse to reading and understanding documentation. It's why they all jumped on the cloud. But it turns out it isn't all just magical infinite performance.

Is your lambda awake?


In asp.net we just use:

app.UsePathBase("/api");

And we have 0 issues.


Similar to Lambda@Edge, Cloudflare also offers Workers (https://workers.cloudflare.com/) which is the same thing, but only with a JavaScript runtime (no nodejs, they use V8), so I believe that it's significantly faster.


CloudFront has CloudFront Functions now which is similar; a very stripped-down JS environment that runs at the edge locations.


CloudFront Distributions cannot pass the request to CloudFront Functions before sending to the origin. In other words, they cannot be used to modify origin request/responses. They can only modify the viewer request/responses. [0]

Only Lambda@Edge can help the scenario which I provided, which is also AWS's recommended solution. [1]

[0] https://docs.aws.amazon.com/AmazonCloudFront/latest/Develope...

[1] https://aws.amazon.com/blogs/architecture/serving-content-us...


A viewer request function can, of course, be used to modify the request before it is sent to the origin. The difference is only related to caching — viewer request happens before the cache lookup, origin request happens after the cache lookup when CF has decided to make an origin request.

For the stated purpose, either function is fine:

With Lambda@Edge you'd use origin request if you were caching these paths, because your function would be called less often so your costs would be lower.

With CF Functions you can only do the pre-cache-lookup modification, so it will be called for every request, but the cost is much lower than Lambda@Edge so it may not matter, and maybe you were not caching these paths anyway in which case it's virtually identical.


Modifying the URL in a viewer request results in the viewer request changing accordingly. If you look at my other responses to other comments, I explained that in detail.

Basically, using the Viewer Request trigger and modifying the URI there causes CloudFront to force a 301 to the user to the new URI, because it's for the viewer. Therefore, in the example of /api/users, if you modify the viewer to remove /api, CloudFront literally removes /api from your request URI, meaning the client accesses /api/users but the server instead sends you to /users (read: server returns 301 location: /users when you hit /api/users) because of your viewer rewrite. You end up hitting your frontend instead of your backend because in order for it to hit your backend, the viewer request has to have /api in it. Therefore you cannot strip it in the viewer request. You must do it in the origin request, which is not supported by CloudFront functions.


Recently chrome required COOP and COEP for shared memory access, which essentially restricts any access to anything besides the same origin. This basically killed my project/motivation of 2~ years.

Using Emscripten and threads, you require COOP & COEP headers to be sent via the main document. This is not common practice for static html hosting sites, thus requiring that you have access to a config file or .htaccess, and requires ALL assets to be hosted exclusively on that same server.

Killing my project is a bit extreme, but killed my motivation.

It's a web-based game with multiplayer that I was hoping people would modify and expand on, hosting themselves, uploading to places like Github.

I've recently started modifying Emscripten's runtime library to try and treat each webworker as a separate instance that just communicates between each other. This has major overhead as each thread is a new memory instance. I've tried getting it to extract each function into it's own module for a webworker to load but that's a major task.

Web apps want to act as desktop applications but they're so held back by security it's nearly impossible. We don't even have a proper local storage system.

To quote Dilbert, "Security is more important than usability. In a perfect world, no one would be able to use anything."


I’m sorry your project was stalled by this.

What static file hosting were you using? If it doesn’t support COOP/COEP headers yet, you can force it by using a service worker with this neat hack: https://stefnotch.github.io/web/COOP%20and%20COEP%20Service%...

Firebase hosting would allow you to set headers with a .htaccess file. Cloudflare Workers can also set the headers.

If your project was going to entail loading resources from other origins not under your control, well, that’s exactly the code injection scenario these headers are designed to prevent.


Sorry late reply, ended up going to bed. This article is actually very interesting, I'll have to dig deeper into this. Thanks.


Here's the Dilbert strip: https://dilbert.com/strip/2007-11-16


I may have misunderstood your requirements, but why do you say that most static hosts don't allow custom headers to be set?

I know that Netlify does[0], and use it myself, so I would expect the other big names to support something as well.

0: https://docs.netlify.com/routing/headers/


I really wanted to give developers more control over their asset hosting. If they wanted to host their assets on a remote server for better performance, that's an option I'd want them to have. Relying on only X Y Z service will certainly work, but certainly not optimal.

It didn't out right kill the project, It'll work under these specific conditions but reducing usability is not helpful.


I'm quite saddened the web has had to rescind so many features (high-res timers, shared memory, memory profiling). It's worth noting that most of these high-end capabilities had just gotten outright disabled for a time, when Spectre/Meltdown hit. That they got turned on at all is a notable improvement. https://web.dev/why-coop-coep/

I don't like some of the candor of the article. Treating cross-origin activity as a mis-feature is a great mis-service to the web: it's not really much of a web, imo, if sites are restricted to talking to themselves. That the web standards crew is happy to shit on this range of capabilities, to write new standards that walk the web back: it's a harsh regression, by afraid leaders. There is a lot a lot a lot of trouble here, in these domains, but this desire to just cancel the feature & walk away, to make the web one where sites have to work & play only with themselves: it's a death knell, against the spirit of the thing, deeply. That it's so inconvenient & troublesome to standards authors, so difficult for browser engineers, that it (used to be) a hazard for sitemasters: I'm sorry for your suffering but good things & great powers have a price. You're wrong to want to shut this possibility out. Easier path that it may be.


Have you looked at Electron? It brings a learning curve and other problems but you have a node.js backend that you can pass messages to and would have full access to the machine.


What is the author trying to say, it is very hard to recognize the answer of the article.

Is it to point requests to sub path of the main domain to reduce OPTIONS requests? eg: api.example.com ---> example.com/api

In that case why not use proper "Access-Control-Max-Age" headers?

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Ac...


Caching is one solution and it work fine, not perfect.

Another thing to consider is avoid hitting your api application with those requests.

You quite probably don't need authorization or any other business logic in that preflight. You can just catch any OPTION or OPTION+preflight headers in your proxy, webserver or balancer and handle it there.

You certainly don't want to handle them in Rails/Rack, nodejs, lambda, django, spring or such.

This makes them so much faster for users, and so much lighter for servers that the once per 2 hours cached request hardly is measurable, even.


Yes I believe the article is suggesting exactly that.

Even with that header users will still hit that latency on the first request. If the goal is to lower latency on first page load then that won’t help. Although it should definitely help after that.


Sometimes it's very difficult to use same domain if e.g. API is on different tech stack. IIRC you could then do something like

<link rel=preconnect crossorigin=anonymous href="https://api.example.com">

or

<link rel=preload crossorigin=anonymous href="https://api.example.com/emptyendpoint">

just to initiate the connection/CORS dance early one before your JS kicks in. In my prev job we'd do a request to empty endpoint from inlined JS. Poor hack but it worked.


Note that if you send any non standard request headers, for the CORS response to be cached and reused, you need to make an initial request with exactly same set of headers, which means it is only possible via JS (preload request can't have extra headers). Hence inlined JS to send the "warmup" request. You can wrap it in a promise which resolves on response, and only then do the actual app request.


I'm having trouble thinking of a web stack where you can't set up a reverse proxy either via Nginx colocated with the frontend web server, through any of the usual suspects for termination (e.g. CDNs or load balancers), or worst case scenario, through the frontend web server itself.

Can you share any examples?


I'm not an expert because I only work on front-end but scale ops folks in my prev job had their reasons.

We had legacy www http/1 monolith and api http/2 geodistributed in the cloud. Making the two go through the common proxy wouldn't probably make sense and/or would be quite expensive.


Just because they are on different tech stacks doesn't mean you can't do this.


Is it still that much important with http2 for most webapps?

You can answer the options request at the webserver level or even Varnish which should be quick enough and the http connection reused.


> Is it still that much important with http2 for most webapps?

Yes? The overhead of a full request/response cycle is still overhead, even if it's lowered by not having to duplicate establishing the connection itself.


Chrome imposes a limit of 2h on Access-Control-Max-Age (other browsers may differ).

Anyway, today we have the tools to expose different backends through the same domain. Reverse proxies, k8s ingress, … a separate domain may not add much value in most situations.


Subdomains absolutely have value! It doesn't matter that we have those tools "today" (it's not like we didn't have reverse proxies many years ago), different domains are still the only way to truly split up traffic. A reverse proxy by definition doubles your total bandwidth requirement, which is expensive and inefficient if you aren't running everything in one datacenter. You'd need to not only get servers with enough bandwidth to cover their traffic, but another server with the sum of all individual server bandwidths to handle proxying. And if you want to do geographic sharing, you either need to shard all services, or be fine with an increase in latency for non-sharded services.


Even then, most often your backend API service runs off one domain, so it needs only one OPTION request ever two hours for the user session.

If your users make only one request per two hours, that still doubles the requests, so in that case caching'll hardly help. But in more typical cases, where one session does 20+ requests, it's a meagre 5% of the requests. In which case 'resolving that n+1 request' or 'speeding up that 1200ms response' is far better low hanging fruit than removing the single OPTIONS request.

It depends on your case, though.


I've struggled with this exact problem. A client developed a SPA and an API to go with it. Because "reasons" they wanted the API to live on api.customersite.com. Fair enough, that's their problem. Except it's not, because the developers have no idea how CORS work, only that it's a thing. So their API can't send CORS headers back, they never implemented that and apparently can't figure out how to make it work.

Instead, we now have a reverse proxy (haproxy) that "fixes" the missing CORS headers, by intercepting the OPTIONS call and return a dummy response with the correct headers included. The developer basically understand NOTHING in regards to CORS, so whenever the silly SPA breaks, the logic is always the same: "CORS is broken, fix it". At not point has it been an option to fix the API service to include the correct headers.

We could just have moved the API to /api and saved days of debugging and writing work-arounds, but no, api.customersite.com looks more professional.


How you can be a web developer today and not knowing how to add two headers to all responses a API does, boggles my mind. Literally took me about 30 minutes to understand how CORS works when it first started being shipped with browsers.


You probably understand the rest of what you need to understand CORS. I've noticed that web developers today don't even understand things like HTTP in general particularly well. They're not trying to understand, they're trying to ship the next feature, and CORS is "blocking them".


Sadly true, I love to ask webdevs in intervews if they can give definitions of http, or idk, user agent, and it's impressive how much ignorance there is out there.


In my experience with CORS, my problem is it takes 30 minutes nearly every time you need to bugfix it.

It’s not that complicated but when something breaks CORS adjacent, you’re stuck reviewing the gotchas.


I've had similar experience.

I inherited both front and back-end. Their solution was "allow: *" (which is like saying, "I don't like carrying my house keys, so I removed the lock")

The system did require a "api.*" for other things, so my quick solution was a proxy_pass for /api.


I'm pretty sure this is the most upvoted answer on SO

...tap ...tap ...tap

No. Second top answer. https://stackoverflow.com/a/27280939/1507124

With the obvious WTF top comment and the natural follow-up. It solved my problem, therefore it's great.


Interestingly, your hacky workaround to client non-cooperation sounds very much like what others are suggesting as their first-choice solution anyway!

See for instance this comment: https://news.ycombinator.com/item?id=29778528 Which suggests implementing the CORS response in a reverse proxy or some other covering layer.

And of course the OP also involves a reverse-proxy, although one mapping to avoid the need for CORS headers.


From my experience, when you want to have 12 factor apps and just build your Frontend/backend once, and don’t want to leak absolute urls in the Frontend code, you are better of with relative URL’s anyways, i.e. /api for your api, and let a reverse proxy do the proper mapping to your services.

Then of course you don’t need CORS


That's what I tend to stick to, CORS is a headache and very easy to do wrong and you shouldn't do it unless you make an open API that can be directly integrated into other websites - which is probably a bad idea.

For most if not all use cases, you can set up a proxy in your front-end's web server so that it doesn't need CORS.


This!


That's one strategy. Having something like `API_URL=https://api.foo.bar/v1` works equally well for 12 factor apps, and gives you the added benefit of transparently pointing to test hosts or staging versions running elsewhere.


Yeah. I would have this env var inside my reverse proxy to have it 12-factor app compatible. And I guess your solution as well? And you have some templating mechanism to replace the absolute variable on the fly?

I would use a mix of your solution, i.e. allow the Java script client to be configured with either absolute or relative urls in case one needs to point the client elsewhere + reverse proxy


Before you start rearchitecting your app, be sure to check if the only reason you're seeing preflights on GET is that someone added a dumb X-Requested-By header.

(You can even do POSTs without preflight if you use a whitelisted content-type.)



Are you conflating CORS with some specific product?


No, referring to this: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simpl...

And the case where preflight isn’t needed despite using CORS.


This will result in fewer requests, but the author didn't provide any test results demonstrating that the server load was appreciably reduced. Since the preflight OPTIONS request will be cached, it will only occur for some fraction of the total volume of requests. Since those requests are also very low impact, I'm skeptical that this pattern will have a consequential impact for most APIs.


The author mentions Cloudflare as having the ability to rewrite requests from `api.website.com/*` to `website.com/api/*`. Is that functionality available as a page rule or would we have to use workers and pay for every request rewritten?

I remember taking a look at this a while ago and only found ways to do this through workers, so was turned off by the potential cost scaling. Ended up just setting a high `Access-Control-Max-Age` and calling it a day. But maybe I've missed a more cost effective way to accomplish this?


I also can't find a way to do this with page or transform rules. Trying to create a rule which modifies the Host header is met with an error message. Apparently only Enterprise customers can do this [1] [2].

[1] https://support.cloudflare.com/hc/en-us/articles/206652947-U...

[2] https://community.cloudflare.com/t/rewrite-host-header-in-pa...


Ah yes that has been the story of my life on Cloudflare. Anything remotely interesting is gated behind what I can only assume is a thousands of dollars per month Enterprise plan.


Or you can have the browser cache the CORS header for up to 2 hours (cross-browser), for the same performance effect - but it will also work for third party website consumers of your API that you can't just put on your domain.


The author did not actually solve the problem at hand.

Try the API playground[1] on the authors site. Its takes more than 1 secs for me to get a preflight response back.

The preflight request hits fastly and aws apigateway and maybe the application well. There are lot of options to solve the problem at fastly and apigateway as I mention in another comment[2].

I really hope author reads the comments here.

[1] https://www.meetup.com/api/playground/

[2] https://news.ycombinator.com/item?id=29778973


Here is an extensive step-by-step tutorial which describes in detail how to create and deploy a React-based web app frontend using TypeScript and Redux Toolkit on top of a Node.js based AWS Lambda backend with a DynamoDB database, connect and integrate them through API Gateway and CloudFront, and explains how to codify and automate the required cloud infrastructure and deployment process using Terraform.

The resulting architecture does not require any CORS requests, too:

https://manuel.kiessling.net/2021/05/02/tutorial-react-singl...


> Here is an extensive step-by-step tutorial which describes in detail how to create and deploy a React-based web app frontend using TypeScript and Redux Toolkit on top of a Node.js based AWS Lambda backend with a DynamoDB database, connect and integrate them through API Gateway and CloudFront, and explains how to codify and automate the required cloud infrastructure and deployment process using Terraform.

I think it's time we take a step back and address this insane complexity required to send HTML documents to users...


Im not sure if its to deploy a blog or a satellite.


That's a mouthful.


I've read that sentence three times and still were not sure if it's meant as a joke or not.


It’s not – I like to summarize my tutorials comprehensively. I am not a native speaker though; I apologize if the sentence isn’t structured correctly or sounds inelegant. Corrections welcome.


Oh, your language is perfectly fine. It's just that your article description reveals a sad truth about modern web technologies: it's a tangled mess.


Highly complicated jacked up messes that you can't explain to your customers...and they don't care.


I guess it's just the mind-bogglingly huge number of components involved to display HTML to someone :)


That is absolutely true in some sense - then again, it is one solution where you start with a decent architecture that can grow and is scalable at linear, and very low, costs.

If you were to really only deliver some HTML, it's overkill. For this reason, my blog is hosted with an old-school 90s-style FTP-backed "webspace" provider.

But the architecture described is used to develop-build-deploy a full-flegded web-based application, with a persistence layer for user data and a potentially complex user-interface.

And for this, having a sound architecture and tech-stack where you do not need to care about low-level server issues is, while certainly not a silver bullet, quite a productive experience.


That’s frontend for you.


I don't think I'm a fan of CV development...


I hope it's clear that the reference to CVDD is tongue-in-cheek.


imho CORS is poorly designed mechanism, you should be able to flag api.domain.com as a safe place, making a single cors request at start of user interaction with your app. Also some sort of caching should be more than welcome.

On a side note at previous company I've worked at, speed was critical, so we were forced to do same tactic, use /api instead of api.domain.com which resulted in huge improvements :)


> Also some sort of caching should be more than welcome.

I have good news: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Ac...


My favorite way to avoid preflights is to have Next.js handle the reverse proxy. It fits my mental model better to have it code-side instead of putting Cloudflare/Fastly in front.


That does avoid preflight, but it just adds additional requests for all static assets. It seems like it's just trading one type of overhead for another.


I do this enough that I really had Next had some config-level handling for API reverse proxy stuff instead of writing a /api wrapper for each.



Does this work if you want to allow an iframe to execute code in a parent browser window from a different domain? (Assuming you control both domains and can inject a different Sec-fetch-mode header into the parent page)?

Context here: I'm maintaining a PWA that has to run on local iphones/android devices and maintain contact with a server on local networks in the 127.x block. But the point of download for the app itself is under an https domain on the open internet. It uses an iframe to check lots of local 127.x.x addresses until it finds one with a local server, and bootstraps itself to the code on the local server that way; unfortunately, it can't run as a true PWA because the iframe at the center of it violates CORS (due to the ban on mixing clear and SSL requests in the more recent versions of Chrome and Safari). Would be nice if the local servers could simply serve up their content and control the window without a whole domain-specific postMessage protocol.


This approach relies on the backend proxying `/api` to the API. In other words, the client requests http://www.example.com/api/foo, then the backend goes off and fetches http://api.example.com/foo, then sends it back to the client.

You can’t do that because in your case only the client can connect to the local networks; your backend has no access.


Ah. ok. I didn't realize this was strictly a backend call.


Is avoiding the OPTIONS request as simple as keeping a fetch to the same origin? I thought there were all kinds of rules for something to be considered a "simple request", like it can't be JSON, can't be PUT or DELETE, etc. I haven't looked much into this but does someone have a clear answer?


Those rules[1] specify whether or not a cross-origin request is considered "simple". If the request is not cross-origin, you can basically do whatever you want.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simpl...


Hint for those who didn't fall for cloud behemoths vendor lock. Deploying your app to plain VPS you still need a reverse proxy like nginx to handle raw requests and terminate TLS. So it is natural to let it do the routing part too.


Aren’t `OPTION` requests useful when you want to build an actual REST API and not what most devs call everything having an HTTP interface? The clients can use the requests to understand which actions they can apply to the resources.


I was interested to note that the Dropbox API offers a hack to avoid the extra CORS request - among other details, their server accepts this:

- Set the Content-Type to "text/plain; charset=dropbox-cors-hack" instead of "application/json" or "application/octet-stream". [0]

...which is allowed in a "Simple" request that doesn't require a separate round trip for OPTIONS.

[0] - https://www.dropbox.com/developers/documentation/http/docume...


Is there any way to read the OPTIONS response from JavaScript?

I'm guessing not, but if there was, theoretically could you just include your API response in the CORS rejection response?


Nope, because OPTIONS requests have no body [1]

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OP...


So can we place the whole body in a response header called X-Body:?


While it’s definitely not recommended to do so, you could probably do it. You need to test it out though, not sure every browser will support this.


CORS exists to protect the backed from developers that ask these types of questions.


Let's not be mean to someone asking a question. It is always good to ask, even stupid ones.


I'm not mean to anyone with less karma than me.


Please use Access-Control-Max-Age instead. CORS has some nice security properties.


What is the security risk of moving from an API subdomain to a single host?


CORS tries to prevent CSRF issues by preventing cross-origin cookie authentication.


Sure, but moving your whole app under a single origin to avoid dealing with CORS doesn't weaken any protection here.


if handled correctly, sure. but it also means that implicit cookie authentication ('ambient authority') is possible and can easily happen by accident


nothing

CORS is just a verification that the cross domain request is to a trusted domain. Your own domain is assumed to be trusted, by default


That is also my understanding, but I'm trying to find out what my parent thinks the security advantage is


I don’t understand what the article is actually recommending. To avoid CORS requests, I usually proxy www.foobar.app/api to api.foobar.app, but it seems like the article is suggesting the opposite.


The author is recommending that the client see only requests to foobar.app, and if you have a separate host for the API, you use a reverse proxy. Which I think is also what you are saying you do?


Yes, that’s what I’m usually doing.


You are not the only one being confused. I think the author mixed it up?


I’m under the same impression :)


Looking at some of my HTTP requests, I noticed that

* `Sec-Fetch-Site: cross-site` can be `Sec-Fetch-Mode: no-cors`

* `Sec-Fetch-Site: same-origin` can be `Sec-Fetch-Mode: cors`

It looks like all four combinations of these two headers are possible.


This is quite timely, was getting frustrated with some CORS issues last night because I am using the domain & api.domain schema. Will have to give this a try.


Try doing this with Heroku and you’ll decide that you want CORS after a few hours.


You can have 2 Heroku apps, one for frontend with little resources since it's all static and one for API with more resources. Then create a custom Nginx config on the static app to redirect API requests and it should work fine.


Doesn’t that create a single point of failure for both the services?


For most services, if the static server is down, the service is down as far as the user is concerned.

Therefore the added dependency may not reduce end-user-percieved reliability.


I reckon that most software services in the world are B2B rather than B2C (guessing from the fact that most users spend most of their time on a handful of services).


Why?


If you don't have an edge proxy (additional compute) you then accept all requests, or accept that a simple header is telling the truth.


Going to implement this to improve Denigma.app load time!


Didn't we do this back in the day for IE?


I'm not 100% on this, but doesn't disabling CORS essentially re-introduce CSRF?


They are bot disabling CORS, they are just proxying the request throigh the same origin so that for their web app they won't have CORS requests.


I think you misunderstand what CORS is. Are you under the impression CORS is a way of blocking requests? It’s the other way around.

Browsers prevent most types of requests that go from one hostname/port/protocol to another by default. CORS is a way for a server to tell browsers to relax these restrictions in some way.

If you disable CORS, all that means is that the default browser behaviour applies, which means that more types of requests are prevented.


Thanks for helping to clarify!


Theory is that an attacker can bypass CSRF protections when CORS is disabled by making an extra GET request to parse the CSRF token which is then provided in the next request.


Then again, I suppose a dedicated attacker can just bypass CORS.


Or just declare JSON as text, simple cors supports authentication, no need to mess with routes.


> You don’t need that CORS request

…if you’re using a proxy like Cloudflare.

The short version of this post is that if your api is hidden behind Cloudflare, you can have it proxy requests to you api that lives on a subdomain.


A reverse proxy using Nginx would work just as well.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: