Table of Contents#
- Understanding the Basics: What is CORS?
- Simple Requests vs. Preflighted Requests
- Why Axios Triggers an OPTIONS Request
- The Flow: OPTIONS → POST Explained
- Is This Axios-Specific?
- Handling Preflight Requests on the Server
- Can We Avoid the OPTIONS Request?
- Troubleshooting Common Preflight Issues
- Conclusion
- 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.comandhttps://api.example.comare different origins (subdomain differs).http://localhost:3000(frontend) andhttp://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, orPOST. - Headers: Only includes "simple headers" (e.g.,
Accept,Accept-Language,Content-Typewith limited values). - Content-Type: If set, must be one of:
application/x-www-form-urlencoded(form data),multipart/form-data(file uploads), ortext/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/jsonfor POST requests with JSON data. application/jsonis 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 includeThis 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: 86400Step 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
corsin 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-Originto 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-Headersdoes not includeContent-Type. - Fix: Add
Content-Typeto 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:
- Handles OPTIONS requests.
- 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.