FSociety

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

November 10, 2019

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'
});

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:

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

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.

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 🤦

Learn more


Pooria Atarzadeh

Personal blog by Pooria Atarzadeh
Unattended curiosity on web, graphql, blockchain and more

About Me