The curse of the CORS Preflight, and how to defeat it
Here’s another post for the search engines because I think answering my own questions on StackOverflow is cheating. TIL how to make cross-site requests from an AngularJS client to a ASP.NET WebAPI service without the dreaded OPTIONS preflight. This might apply to other server/client frameworks, but that is for you to figure out All By Yourself™.
Now you are in the magical forest of CORS. As the MDN article “HTTP access control (CORS)" explains:
Well, it doesn’t say exactly that because I’m paraphrasing. But basically CORS means that
- Before the browser submits a non-simple cross-site request it sends a OPTIONS method and some headers
- The server replies with a response with Access-Control-* headers
- The browser sends the real GET/POST/PUT/DELETE request
- The server sends the real response, again with the Access-Control header
- The actual data is available to the application.
This is very chatty and makes everything slow and stupid, and also there are many things that can break. A hilarious example is that even if everything worked up until #4 but the server forgot to include the Access-Control header, then even if the data is actually sent the browser will refuse to load it. This was designed by the same person who invented unskippable DVD previews. Let’s start with #2, what a server must do to handle these accursed preflight requests, then we’ll dive into preventing #1 altogether.
Your API must be made to understand requests with the OPTIONS method. With WebAPI this can be done with the NuGet package “Microsoft.AspNet.WebApi.Cors”, which allows OPTIONS requests to arrive without breaking and is nicely described in the article “Enabling Cross-Origin Requests”. This will allow your app to work again because it will respond with the proper response headers and your boss will get off your back, if that’s all you care about. You should probably enable this if you have 3rd-party API clients, such as if you want other developers to embed your api.smurf-collector.com data in some mashup hackathon craziness. You can see an example of this server/client conversation at the actual MDN article linked above.
"But wait", you say, "now my app is half as snappy because of all these rootin’ tootin’ preflights." You are correct, and I applaud your use of cowboy slang. Put another way, why would you go ask your mom’s permission to eat one cookie at a time if you could just eat the entire box by yourself in the kitchen and hope she never notices? That extra 150 milliseconds is an eternity.
The answer is that you can avoid a preflight by doing a “simple request”, that is, the sort of request your browser could always do before this CORS abomination was brought into this world. So much for your fancy “Authorize” header, boo. Cookies are out (and they should be), so how do you do this? First, you can always send junk to your API with the token in the URL query string, like api.smurf-collector.com/smurf/brainy-smurf?token=gargamel1234. “No, that is bogus because anyone can see the token in the URL” you say before you facepalm yourself and realize that (1) nobody but you will see your API calls and (C) you always use HTTPS so nobody will intercept the token anyway except the NSA and script-kiddies with the heartbleed toolkit. That handles GET requests, but POSTs are still preflighting, now what? The problem is the POST content type. Simple POSTS are done with only a few content types, but frameworks like AngularJS set the content type to “application/json”. So, you set the content type to “text/plain” and now WebAPI has no idea what is going on. This blog post right here will explain how you can trick WebAPI into understanding what “text/plain” means. You could also put the token inside every message itself and read it somehow but that smells weird to me. I recommend that you enable both authentication methods, both the “Authorize” header and also the URL query string thing.
If everything is lined up, then your app should be able to shuffle around data to another server without preflights. I hope you have enjoyed our journey together. I know I have.