coderain blog

Why Does Axios Send Two Requests (OPTIONS & POST) When Posting Data? Explained

If you’ve ever used Axios to send a POST request from a frontend application (e.g., React, Vue) to a backend API, you might have noticed something puzzling in your browser’s network tab: two requests are sent instead of one. The first is an OPTIONS request, followed by the actual POST request. This behavior often leaves developers scratching their heads: Why two requests? Is this an Axios bug?

Fear not—this is not a bug, but a deliberate security mechanism enforced by web browsers. In this blog, we’ll demystify why Axios (and other HTTP clients) trigger this "double request" pattern, break down the role of the OPTIONS request, and explain how to handle it effectively in your applications.

2026-01

Table of Contents#

  1. Understanding the Basics: What is CORS?
  2. Simple Requests vs. Preflighted Requests
  3. Why Axios Triggers an OPTIONS Request
  4. The Flow: OPTIONS → POST Explained
  5. Is This Axios-Specific?
  6. Handling Preflight Requests on the Server
  7. Can We Avoid the OPTIONS Request?
  8. Troubleshooting Common Preflight Issues
  9. Conclusion
  10. References

1. Understanding the Basics: What is CORS?#

To understand why Axios sends two requests, we first need to grasp CORS (Cross-Origin Resource Sharing). CORS is a security feature implemented by web browsers to restrict web pages from making requests to a different domain (origin) than the one that served the original page. This prevents malicious websites from accessing sensitive data on behalf of the user.

An "origin" is defined by the combination of protocol (http/https), domain, and port. For example:

  • https://example.com and https://api.example.com are different origins (subdomain differs).
  • http://localhost:3000 (frontend) and http://localhost:5000 (backend) are different origins (port differs).

2. Simple Requests vs. Preflighted Requests#

Browsers classify cross-origin requests into two categories: simple requests and preflighted requests. Only preflighted requests trigger the "double request" behavior (OPTIONS + POST).

What is a Simple Request?#

A request is "simple" if it meets all of the following conditions:

  • Method: One of GET, HEAD, or POST.
  • Headers: Only includes "simple headers" (e.g., Accept, Accept-Language, Content-Type with limited values).
  • Content-Type: If set, must be one of:
    • application/x-www-form-urlencoded (form data),
    • multipart/form-data (file uploads), or
    • text/plain (plain text).

Simple requests are sent directly to the server without any prior checks.

What is a Preflighted Request?#

A request is "preflighted" if it fails the simple request criteria. This includes:

  • Using non-simple methods (e.g., PUT, DELETE, PATCH).
  • Using non-simple headers (e.g., Authorization, X-Custom-Header).
  • Using Content-Type: application/json (the default for Axios POST requests!).

For preflighted requests, the browser first sends an OPTIONS request (called a "preflight request") to the server to check if the actual request (e.g., POST) is allowed. Only if the server approves the preflight does the browser send the actual request.

3. Why Axios Triggers an OPTIONS Request#

Axios is a popular HTTP client for JavaScript, but it’s not Axios itself that sends the OPTIONS request. The OPTIONS request is automatically triggered by the browser when Axios sends a preflighted request.

The key reason Axios often triggers preflight is its default configuration:

  • By default, Axios sets Content-Type: application/json for POST requests with JSON data.
  • application/json is not a simple content type (see Section 2), so the request is preflighted.

Example: Axios POST Request#

Let’s say you send a POST request with Axios like this:

import axios from 'axios';
 
const postData = async () => {
  try {
    const response = await axios.post('http://localhost:5000/api/data', {
      name: 'John',
      email: '[email protected]'
    });
    console.log(response.data);
  } catch (error) {
    console.error(error);
  }
};

Axios automatically sets the following headers for this request:

  • Content-Type: application/json (because we’re sending a JSON payload).
  • Accept: application/json, text/plain, */* (default accept header).

Since Content-Type: application/json is non-simple, the browser treats this as a preflighted request and sends an OPTIONS request first.

4. The Flow: OPTIONS → POST Explained#

Let’s walk through the step-by-step flow when Axios sends a preflighted POST request:

Step 1: Browser Sends OPTIONS (Preflight Request)#

The browser sends an OPTIONS request to the backend server with headers like:

OPTIONS /api/data HTTP/1.1
Origin: http://localhost:3000  # Frontend origin
Access-Control-Request-Method: POST  # The actual method to be used
Access-Control-Request-Headers: Content-Type  # Headers the POST will include

This request asks the server: "Can a POST request from http://localhost:3000 with Content-Type: application/json be sent to /api/data?"

Step 2: Server Responds to OPTIONS#

The server must respond to the OPTIONS request with headers that explicitly allow the preflighted request. Critical headers include:

  • Access-Control-Allow-Origin: http://localhost:3000 (or * for public APIs): Specifies which origins are allowed.
  • Access-Control-Allow-Methods: POST: Specifies allowed methods.
  • Access-Control-Allow-Headers: Content-Type: Specifies allowed headers.
  • Access-Control-Max-Age: 86400 (optional): Caches the preflight approval for 24 hours to avoid repeated OPTIONS requests.

Example successful OPTIONS response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

Step 3: Browser Sends the Actual POST Request#

If the server approves the preflight (via the OPTIONS response), the browser sends the actual POST request from Axios:

POST /api/data HTTP/1.1
Origin: http://localhost:3000
Content-Type: application/json
 
{ "name": "John", "email": "[email protected]" }

Step 4: Server Responds to POST#

Finally, the server processes the POST request and sends a response (e.g., 200 OK with data).

5. Is This Axios-Specific?#

No! The "OPTIONS + POST" pattern is not unique to Axios. It occurs with any HTTP client (e.g., fetch, superagent) when sending preflighted requests.

For example, using fetch with Content-Type: application/json would trigger the same behavior:

fetch('http://localhost:5000/api/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }, // Non-simple content type
  body: JSON.stringify({ name: 'John' })
});

Axios just makes it easy to send JSON payloads (by default), which often leads to preflight requests.

6. Handling Preflight Requests on the Server#

To resolve preflight issues, your backend server must explicitly handle OPTIONS requests and return the required CORS headers. Below are examples for common backend frameworks:

Example 1: Node.js/Express#

Use the cors middleware to automatically handle preflight requests:

const express = require('express');
const cors = require('cors');
const app = express();
 
// Allow cross-origin requests from frontend (http://localhost:3000)
app.use(cors({
  origin: 'http://localhost:3000', // Replace with your frontend origin
  methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allow specific methods
  allowedHeaders: ['Content-Type', 'Authorization'] // Allow specific headers
}));
 
// Parse JSON bodies (required for POST requests)
app.use(express.json());
 
// Your POST endpoint
app.post('/api/data', (req, res) => {
  res.json({ message: 'Data received!', data: req.body });
});
 
app.listen(5000, () => console.log('Server running on port 5000'));

The cors middleware automatically responds to OPTIONS requests with the correct headers.

Example 2: Python/Flask#

Use the flask-cors extension:

from flask import Flask, request, jsonify
from flask_cors import CORS
 
app = Flask(__name__)
# Allow cross-origin requests from frontend
CORS(app, origins="http://localhost:3000", methods=["POST"], allow_headers=["Content-Type"])
 
@app.route('/api/data', methods=['POST'])
def handle_data():
    data = request.get_json()
    return jsonify({"message": "Data received!", "data": data})
 
if __name__ == '__main__':
    app.run(port=5000)

7. Can We Avoid the OPTIONS Request?#

In most cases, you should not avoid preflight requests—they exist to protect your API from unauthorized cross-origin access. However, if you must send a simple request (e.g., for performance reasons), you can modify your Axios request to meet simple request criteria:

Option 1: Use Content-Type: text/plain#

Send data as plain text instead of JSON:

axios.post('http://localhost:5000/api/data', 
  JSON.stringify({ name: 'John' }), // Stringify JSON to plain text
  { headers: { 'Content-Type': 'text/plain' } } // Simple content type
);

Caveat: Your backend must parse plain text into JSON manually.

Option 2: Use application/x-www-form-urlencoded#

Send data as form-encoded key-value pairs:

import qs from 'qs'; // Axios has a built-in qs library for encoding
 
axios.post('http://localhost:5000/api/data', 
  qs.stringify({ name: 'John', email: '[email protected]' }), // Encode as form data
  { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);

8. Troubleshooting Common Preflight Issues#

If your Axios POST request fails, the issue often lies with the preflight (OPTIONS) request. Here are common problems and fixes:

Issue 1: OPTIONS Request Returns 404/403#

  • Why: The server does not handle OPTIONS requests for the endpoint.
  • Fix: Ensure your server is configured to accept OPTIONS requests (use CORS middleware like cors in Express).

Issue 2: Missing Access-Control-Allow-Origin Header#

  • Why: The server’s OPTIONS response does not include Access-Control-Allow-Origin.
  • Fix: Explicitly set Access-Control-Allow-Origin to your frontend origin (e.g., http://localhost:3000) or * (for public APIs).

Issue 3: Content-Type Not Allowed#

  • Why: The server’s Access-Control-Allow-Headers does not include Content-Type.
  • Fix: Add Content-Type to the list of allowed headers (e.g., Access-Control-Allow-Headers: Content-Type).

9. Conclusion#

Axios does not send two requests by itself—the OPTIONS request is a browser-initiated preflight check for cross-origin requests that fail the "simple request" criteria. This behavior is triggered when Axios uses Content-Type: application/json (its default for POST requests), which is a non-simple content type.

To resolve preflight issues, ensure your backend server:

  1. Handles OPTIONS requests.
  2. Returns the required CORS headers (e.g., Access-Control-Allow-Origin, Access-Control-Allow-Methods).

Preflight requests are a critical security feature, so avoid disabling them unless absolutely necessary.

10. References#