This issue caught my attention a few days ago that my colleagues were facing difficulty in using a new API developed in-house using Flask. The problem was that no matter what, the front-end developer couldn't make a call with correct content-type
. Even though that Axios uses JSON as the default content type, the call was always going with a text/html
format and everyone were getting frustrated ๐คจ.
In the other hand, the back-end developer was showing her the result from Postman (an application for developers to send HTTP calls) and everything was working fine there!
I first tried to test if the end point is working fine or not. Me being a CLI guy, used my favorite HTTP client HTTPie to do the basic call. It's something like CURL but looks better for the eyes!
Nothing is wrong here if we test the API standalone with a HTTP client, but the axios request below would result in nothing.
axios.post('https://ENDPOITN_URL', {
field1: 'something',
field2: 'something'
});
My colleague moved forward and tried to enforce a application/json
content-type to axios. It's a bit weird but maybe somewhere else in the code the default for the axios is changed?
const customHeaders = {
'content-type': 'application/json',
};
axios.post('https://ENDPOITN_URL', {
field1: 'something',
field2: 'something'
}, customHeaders);
Still no practical results. I asked for a screenshot and this is how it was looking like in the browser:
Okay let's take a closer look, there are two things to consider here:
As you can see, the POST method is never sent and only a method called OPTIONS is sent to the endpoint. The response headers from this call has a content-type
of 'text/html' which is the reason for all this evil here. So... what's going on?
What is a preflight request?
A preflight request, is a mechanism in CORS by the browser to check if the resource destination is willing to accept the real request or not. Afterall, why would a request be sent when the target host is not willing to receive it anyway?
This mechanism works by sending an OPTIONS
HTTP method with Access-Control-Request-Method
and Access-Control-Request-Headers
in the header to notify the server about the type of request it wants to send. The response it retrieves determine if the actual request is allowed to be sent or not. This is a sample of a preflight request:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
I highlighted the last three lines, because they are important fields in this call. Most developers are familiar with the Origin method because if it's not allowed from the backend API, you are not able to make AJAX calls to fetch the data. The other two parameters are overlooked ๐ง because most frameworks and libraries would take care of them anyway. For example any backend developer using express can simply add a middleware called CORS and make sure all the calls in his express app are providing those parameters for the OPTIONS method to the browsers.
var cors = require('cors')
app.use(cors()) // cool now everything is handled!
Whenever the server received that request, it should responds with Access-Control-Allow-Methods
and some other meta data to identify if the original request is acceptable or not! A sample response would look something like this (but it varies):
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
It's important to mention that, not all requests would preflight. As far as I know, only requests that are meant to be sent to a different origin and are not a form content-type are preflighted (excluding GET and HEADER methods).
So what was the problem?
I tried to send a normal OPTIONS request to the endpoint to check the rules. I used the --headers
in HTTPie to only receive the header of the request.
Turned out that the value of the content-type
here is text/html
and that's why browser wouldn't push through with the actual POST method, however with a normal client it's acceptable.
But we originally mentioned that most of the frameworks would handle this out of the box, so why here Flask is giving us wrong content-type? It's sort of a tricky situation... I figured if I send a normal POST request to the API without the required body
parameters, the endpoint will throw an error which is not properly handled!
Well it's an obvious bug on the backend but probably they didn't care because it was an internal API and it was working fine with correct parameters. However, the OPTIONS method contains no body
parameters within and since the original API without params is returning a text/html
content (the web server error page) the OPTIONS method was also returning the same, mistakenly thinking that this API does not accept a JSON request ๐คฆ
I really enjoyed learning about this mechanism better through this article. If you like to learn more about this HTTP method and the process of preflight feel free to scavenge these links further:
Learn more
- OPTIONS - MDN web docs
- Express CORS middleware source code in github
- Access-Control-Allow-Methods HTTP header
I originally published this article in my blog!
Top comments (7)
but how do we solve this?
I solved it by doing what he said.
npm install cors
var cors = require('cors')
app.use(cors())
Afterwards I was able to make POST requests
Hi, I am doing the same things but still did not resolve this issue. could you please tell what i can do to resolve this errro.
You need to send a few headers.
"Content-Type": "application/json", // content type
"Access-Control-Allow-Origin" : 'your request url', // your request url
"Access-Control-Allow-Methods" : "POST, GET, OPTIONS" // supported methods
You then simply need to return a 200 ok. Then the browser will send the POST, GET request straight after with the content body.
Great post.This post really helped me to solve the same issue what I was facing from 3-4 days. I handled OPTIONS request at backend. If request is of OPTIONS type then only 3 response headers are sent back with required values.
I noticed that the "http --headers" operation did not return the "Access-Control-Allow-*" field. Why?
OMG! thank you! i really struggled on this issue since yesterday, I was literally tearing my hair out on this problem!