Bypassing CORS with Proxy
Why I Encountered This Problem
While developing the frontend and calling an external API, I suddenly got a CORS error:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
The API definitely worked, but the browser blocked it. Requests via Postman or curl worked fine. I didn't understand why.
Searching showed many "add CORS headers on the server" answers, but the problem was I couldn't modify the server. It was either an external API or the backend team was too busy for immediate changes.
So I wondered "is there a way to bypass CORS at least in development?" and discovered proxy as a solution.
What Confused Me Initially
I roughly knew what CORS was. "Browser blocks requests to different domains" was my understanding. But confusing parts:
- Why does Postman work but browser doesn't?
- Why CORS error even on same localhost with different ports?
- How does proxy bypass CORS?
Especially "doesn't using proxy create security issues?" CORS is for security, so is bypassing it okay?
The 'Aha!' Moment
The turning point was accepting "CORS is a browser policy, not a server policy."
The Nature of CORS
I understood CORS through the "apartment security guard" analogy:
- Browser (Guard): Checks ID (CORS headers) when outsiders (different domains) try to enter. Blocks if missing.
- Server (Homeowner): Issues permit (Access-Control-Allow-Origin) to guard saying "let this person in."
- Postman/curl (Delivery): Goes directly to homeowner without guard. No CORS check.
sequenceDiagram
participant Browser
participant Proxy as Proxy Server
participant API as External API
Note over Browser,API: CORS Error Scenario
Browser->>API: GET https://api.example.com/data
API->>Browser: 200 OK (no CORS headers)
Browser->>Browser: CORS check fails
Browser->>Browser: Error!
Note over Browser,API: Using Proxy Scenario
Browser->>Proxy: GET /api/data (same domain)
Proxy->>API: GET https://api.example.com/data
API->>Proxy: 200 OK
Proxy->>Browser: 200 OK (no CORS check)
Note right of Browser: Same domain, no CORS check!
The key is CORS is only checked by browsers. Server-to-server communication has no CORS. So with a proxy server in between:
- Browser → Proxy: Same domain, no CORS check
- Proxy → External API: Server-to-server, no CORS check
Framework-Specific Proxy Configuration
1. Vite
Simplest. Add to vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
Usage:
// ❌ CORS error
fetch('https://api.example.com/users');
// ✅ Using proxy
fetch('/api/users'); // → https://api.example.com/users
Options:
target: Actual API server addresschangeOrigin: ChangeHostheader to target (required!)rewrite: Path transformation (/api/users→/users)
2. Create React App
Add to package.json (simple case):
{
"proxy": "https://api.example.com"
}
Or src/setupProxy.js (complex case):
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': '', // remove /api
},
})
);
};
Note: setupProxy.js applies automatically without restart.
3. Next.js
next.config.js:
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
Or use API Routes (recommended):
// pages/api/users.ts
export default async function handler(req, res) {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
res.status(200).json(data);
}
Usage:
// Client
fetch('/api/users'); // Call Next.js API Route
4. Webpack Dev Server
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
5. Express (Node.js)
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
})
);
app.listen(3000);
Practical Tips
1. Multiple API Proxies
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api/v1': {
target: 'https://api1.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1/, ''),
},
'/api/v2': {
target: 'https://api2.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v2/, ''),
},
},
},
});
2. Add Auth Headers
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
// Add API key to all requests
proxyReq.setHeader('Authorization', 'Bearer YOUR_API_KEY');
});
},
},
},
},
});
3. HTTPS API Proxy
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
secure: false, // Allow self-signed certificates
},
},
},
});
4. WebSocket Proxy
export default defineConfig({
server: {
proxy: {
'/socket': {
target: 'ws://localhost:5000',
ws: true, // Enable WebSocket
},
},
},
});
5. Conditional Proxy
export default defineConfig({
server: {
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'https://api.example.com',
changeOrigin: true,
},
},
},
});
.env.local:
VITE_API_URL=https://dev-api.example.com
Production Environment Considerations
⚠️ Proxy is Development-Only
Proxy configuration only works on dev server. Doesn't work after build.
// ❌ Doesn't work in production
fetch('/api/users'); // 404 error!
Solution 1: Use environment variables
const API_URL = import.meta.env.VITE_API_URL || '/api';
// Development: /api/users (proxy)
// Production: https://api.example.com/users
fetch(`${API_URL}/users`);
# .env.development
VITE_API_URL=/api
# .env.production
VITE_API_URL=https://api.example.com
Solution 2: Configure proxy on server
Nginx:
location /api {
proxy_pass https://api.example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Vercel:
// vercel.json
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/:path*"
}
]
}
Security Considerations
1. Prevent API Key Exposure
// ❌ Expose API key to client
fetch('/api/users', {
headers: {
'Authorization': 'Bearer SECRET_KEY', // Visible in browser!
},
});
// ✅ Add API key on server (proxy config)
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Authorization', `Bearer ${process.env.API_KEY}`);
});
2. Rate Limiting
Limit proxy requests too:
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests
});
app.use('/api', apiLimiter);
3. Allowed Domains Only
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
if (!allowedOrigins.includes(origin)) {
res.status(403).send('Forbidden');
}
},
},
}
Mistakes I Made
1. changeOrigin: false
Initially forgot changeOrigin and proxy didn't work.
// ❌ Problem code
proxy: {
'/api': {
target: 'https://api.example.com',
// no changeOrigin!
},
}
// ✅ Correct code
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // Required!
},
}
Lesson: changeOrigin: true is almost always needed.
2. Path Rewrite Mistake
// ❌ Problem code
proxy: {
'/api': {
target: 'https://api.example.com/api', // /api duplicated!
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// Result: /api/users → https://api.example.com/api/users (wrong)
// ✅ Correct code
proxy: {
'/api': {
target: 'https://api.example.com',
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// Result: /api/users → https://api.example.com/users (correct)
3. Using Proxy in Production
Requested /api/users after build and got 404.
Lesson: In production, use full URL with environment variables or configure proxy on server.
4. HTTPS Certificate Error
Got errors proxying to API with self-signed certificate.
// ✅ Solution
proxy: {
'/api': {
target: 'https://self-signed.example.com',
changeOrigin: true,
secure: false, // Allow self-signed certificates
},
}
Note: Use secure: true in production.
Debugging Tips
1. Check Proxy Logs
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('proxy error', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Sending Request:', req.method, req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('Received Response:', proxyRes.statusCode, req.url);
});
},
},
},
},
});
2. Check Network Tab
In browser DevTools → Network tab:
- Verify request URL is
/api/users - Check if response headers have CORS-related headers
3. Test Proxy with curl
# Direct request without proxy
curl https://api.example.com/users
# Request through proxy
curl http://localhost:3000/api/users
One-Line Summary
CORS errors can be bypassed with proxy configuration in development, but production requires adding CORS headers on server or using server-side proxy.