Why is my browser sending an OPTIONS HTTP request instead of POST?
Tags: http, browser, webdev, javascript
Published: 2019-11-09 20:49:08

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](https://www.getpostman.com/) (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](https://httpie.org) to do the basic call. It's something like CURL but looks better for the eyes!

![successful HTTP call](https://fsociety.at/static/c03069c1af479f1dd2dab7e05383f8b4/0c224/1.png)

Nothing is wrong here if we test the API standalone with a HTTP client, but the axios request below would result in nothing.

```javascript
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?

```javascript{2}
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](https://fsociety.at/static/3ef7f1ecb72da66b7d7f3466f9c525f1/aa6a0/2.png)

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

![browser screenshot, OPTIONS call and content-type is text/html](https://fsociety.at/static/8e7ac9558d628bc469a8052662301154/c0626/3.png)

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:

```{8-10}
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.

```javascript{3}
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](https://fsociety.at/static/754b5a32e144fac76855b8aed8949dd8/639eb/4.png)

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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS)
- [Express CORS middleware source code in github](https://github.com/expressjs/cors)
- [Access-Control-Allow-Methods HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)


*I originally published this article in [my blog!](https://fsociety.at/why-my-browser-is-sending-an-OPTIONS-request-instead-of-POST/)*