DEV Community

Pooria A
Pooria A

Posted on

Why is my browser sending an OPTIONS HTTP request instead of POST?

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!

successful HTTP call

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'
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Still no practical results. I asked for a screenshot and this is how it was looking like in the browser:

browser screenshot, it's an OPTIONS call not a POST

Okay let's take a closer look, there are two things to consider here:

browser screenshot, OPTIONS call and content-type is text/html

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

OPTIONS method returns a text/html content type

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

I originally published this article in my blog!

Top comments (7)

Collapse
 
manangouhari profile image
Manan Gouhari

but how do we solve this?

Collapse
 
bellola profile image
Luis Bello

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

Collapse
 
atikaakmal profile image
atikaakmal

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.

Collapse
 
raghuhck profile image
Raghu

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.

Collapse
 
sangelxyz profile image
sAngelxyz

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.

Collapse
 
wajika profile image
wajika

I noticed that the "http --headers" operation did not return the "Access-Control-Allow-*" field. Why?

Collapse
 
gindev77 profile image
gindev77

OMG! thank you! i really struggled on this issue since yesterday, I was literally tearing my hair out on this problem!