Fix: Vite Proxy Not Working — API Requests Not Forwarded or 404/502 Errors
Part of: React & Frontend Errors
Quick Answer
How to fix Vite dev server proxy issues — proxy configuration in vite.config.ts, path rewriting, WebSocket proxying, HTTPS targets, and common misconfigurations.
The Problem
Vite’s dev server proxy returns a 404 or doesn’t forward requests to the backend:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:3000',
},
},
});
// fetch('/api/users') → 404 Not Found
// Expected: forwards to http://localhost:3000/api/usersOr a rewrite rule silently does nothing:
proxy: {
'/api': {
target: 'http://localhost:3000',
rewrite: (path) => path.replace(/^\/api/, ''), // Wrong key name
},
}
// /api/users → http://localhost:3000/api/users (rewrite ignored)
// Expected: http://localhost:3000/usersOr WebSocket connections fail through the proxy:
WebSocket connection to 'ws://localhost:5173/socket.io/' failedOr a request to an HTTPS backend produces UNABLE_TO_VERIFY_LEAF_SIGNATURE.
Why This Happens
Vite’s proxy is powered by http-proxy. Most failures come from one of these root causes:
- Wrong rewrite option name — the option is
rewrite, notpathRewrite(that’s webpack’s name). Easy to confuse if you’re migrating from webpack/CRA. - Missing
changeOrigin— when the target is on a different host, the HTTPHostheader must be updated. WithoutchangeOrigin: true, some backends reject the request. - Path not starting with the proxy key — Vite matches requests where the URL starts with the key.
/apimatches/api/usersbut not/v1/api/users. - WebSocket proxy not enabled — WebSocket connections require
ws: truein the proxy config. - HTTPS target with self-signed cert — Vite’s proxy rejects invalid TLS certificates by default.
- Target URL with trailing slash —
http://localhost:3000/vshttp://localhost:3000behaves differently when combined with path rewriting.
The most dangerous failure mode is invisible: everything works in vite dev, but the proxy is a dev-only feature. It does not exist in your production build. When you deploy to Vercel, Netlify, or Cloudflare Pages, the static assets make direct requests to your API origin. If your backend does not have CORS configured or a reverse proxy in front of it, every API call fails in production. This is the single most common “works on my machine” trap with Vite proxy configuration.
The blast radius is total. Your dev proxy masks the real network topology. You build and test against localhost:5173/api/users, but production sends requests to api.example.com/users with a different origin, different headers, and no proxy middleware in between. If you have not explicitly handled CORS on the backend or set up a production reverse proxy, your entire frontend is dead on deploy.
Fix 1: Use the Correct Option Names
The proxy option names in Vite differ from webpack’s devServer.proxy:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
// Simplest form — just a string target
'/api': 'http://localhost:3000',
// Full options form
'/api': {
target: 'http://localhost:3000',
changeOrigin: true, // Update the Host header to match target
rewrite: (path) => path.replace(/^\/api/, ''), // ← 'rewrite', not 'pathRewrite'
},
},
},
});Vite vs webpack option names:
| Vite | webpack |
|---|---|
rewrite | pathRewrite |
changeOrigin | changeOrigin |
ws | ws |
target | target |
Common rewrite patterns:
proxy: {
// Strip /api prefix: /api/users → /users
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// Keep /api prefix: /api/users → /api/users (no rewrite needed)
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
// Replace prefix: /v1/users → /api/v1/users
'/v1': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/v1/, '/api/v1'),
},
}Fix 2: Add changeOrigin for External or Different-Port Backends
Without changeOrigin, the Host header sent to your backend is localhost:5173 (the Vite server’s address). Many backends check the Host header and reject mismatches:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true, // Sets Host header to 'localhost:3000'
},
// For external APIs — always use changeOrigin
'/github': {
target: 'https://api.github.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/github/, ''),
},
},
},
});Note: For a local backend (same machine, different port),
changeOriginmay not be strictly required. For any external host or when you see403 Forbiddenfrom the backend, always add it.
Fix 3: Fix WebSocket Proxy
WebSocket connections require the ws option:
export default defineConfig({
server: {
proxy: {
// Proxy WebSocket connections
'/socket.io': {
target: 'http://localhost:3001',
changeOrigin: true,
ws: true, // ← Required for WebSocket support
},
// Proxy both HTTP and WS on the same path
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
ws: true,
},
},
},
});Socket.IO example:
// Frontend — connect via proxy
import { io } from 'socket.io-client';
// In development, connect to Vite's port — the proxy forwards to the backend
const socket = io('/', {
path: '/socket.io',
});
// In production, connect directly to the backend
// const socket = io('https://api.example.com');// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/socket.io': {
target: 'http://localhost:3001',
ws: true,
changeOrigin: true,
},
},
},
});Note: Vite HMR (hot module replacement) also uses WebSocket internally. The proxy’s
ws: trueis for your app’s WebSocket connections, not Vite’s HMR. They use different paths and don’t conflict.
Fix 4: Proxy to HTTPS Backends
When the target is an HTTPS server with a self-signed certificate, Vite’s proxy rejects it:
Error: unable to verify the first certificate
Error: UNABLE_TO_VERIFY_LEAF_SIGNATUREDisable SSL verification for the proxy:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://localhost:8443',
changeOrigin: true,
secure: false, // Disable TLS certificate verification
},
},
},
});Warning: Only use
secure: falsein development. Never disable certificate verification in production code.
For a valid HTTPS cert (staging/dev environment with a real cert):
import fs from 'fs';
import path from 'path';
export default defineConfig({
server: {
// Configure Vite itself to use HTTPS
https: {
key: fs.readFileSync(path.resolve(__dirname, 'certs/dev.key')),
cert: fs.readFileSync(path.resolve(__dirname, 'certs/dev.crt')),
},
proxy: {
'/api': {
target: 'https://api.dev.example.com',
changeOrigin: true,
// No 'secure: false' needed if the target cert is valid
},
},
},
});Fix 5: Match Multiple Paths with RegExp
Vite proxy keys can be regular expressions (as strings) for more flexible matching:
export default defineConfig({
server: {
proxy: {
// Match /api and /auth paths
'^/(api|auth)': {
target: 'http://localhost:3000',
changeOrigin: true,
},
// Match any path with a specific pattern
'^/v[0-9]+/': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});Multiple backends:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000', // Main API
changeOrigin: true,
},
'/auth': {
target: 'http://localhost:3001', // Auth service
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:3002', // File service
changeOrigin: true,
},
},
},
});Fix 6: Debug Proxy Requests
Enable proxy logging to see what’s being forwarded:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
configure: (proxy, options) => {
// Log proxy events
proxy.on('error', (err, req, res) => {
console.log('[proxy error]', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('[proxy request]', req.method, req.url, '→', options.target + req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('[proxy response]', proxyRes.statusCode, req.url);
});
},
},
},
},
});Check the actual request in the browser:
- Open DevTools → Network tab
- Find the failing request
- Check the Request URL — if it shows
localhost:5173/api/..., the proxy should be handling it - Check the Response — if 502, the proxy can’t reach the target; if 404, the target returned 404
Fix 7: Environment-Specific Proxy Config
Keep proxy config out of vite.config.ts for team environments:
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
server: {
proxy: {
'/api': {
target: env.VITE_API_TARGET || 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
});# .env.development
VITE_API_TARGET=http://localhost:3000
# .env.staging
VITE_API_TARGET=https://api-staging.example.comDisable proxy in production builds — the proxy only runs in dev mode (vite dev). Vite builds (vite build) produce static assets that talk directly to the API. For production, configure CORS on the backend or use a reverse proxy like nginx.
Fix 8: Handle the Dev-to-Production Gap
This is the fix most articles skip, and it causes the most production incidents. Vite’s proxy only exists during vite dev. When you run vite build and deploy, the proxy is gone. Every fetch('/api/users') your frontend makes now hits your hosting provider’s static file server, not your backend.
Symptoms in production:
- All API calls return 404 (the CDN has no
/api/usersroute) - All API calls fail with CORS errors (the browser sends requests to a different origin, and the backend does not include
Access-Control-Allow-Origin) - The app loads fine but every interactive feature is broken
The proper production setup depends on your deployment target.
Vercel — use vercel.json rewrites:
{
"rewrites": [
{ "source": "/api/:path*", "destination": "https://api.example.com/api/:path*" }
]
}Netlify — use _redirects or netlify.toml:
# public/_redirects
/api/* https://api.example.com/api/:splat 200Cloudflare Pages — use _redirects or a Worker:
/api/* https://api.example.com/api/:splat 200Nginx reverse proxy:
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://backend:3000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}Or configure CORS on the backend directly:
// Express backend
app.use(cors({
origin: ['https://example.com', 'http://localhost:5173'],
credentials: true,
}));Pro Tip: Add a pre-deploy check to your CI pipeline that verifies CORS headers or reverse proxy rules are configured. A smoke test that runs curl -I https://example.com/api/health after deploy catches this class of failure before users do.
Production Incident Playbook: All API Calls Fail After Deploy
Scenario: You deploy a Vite-built SPA to Vercel. The site loads, but every button click, form submission, and data fetch fails silently or shows a network error. The console is full of CORS errors or 404s.
Blast radius: Total frontend failure. Every user action that touches the API is broken. The static HTML/CSS loads, so the site appears “up” — but no functionality works.
Detection: Monitor API error rates from the frontend. If you use an error tracking service (Sentry, Datadog RUM), you see a spike in network errors immediately after deploy. Without frontend monitoring, this goes undetected until a user reports it.
Diagnosis checklist:
- Open the deployed site in DevTools. Check the Network tab for failing requests.
- If responses are 404 — the CDN is not proxying to your backend. Add rewrite rules.
- If responses show CORS errors — the backend is reachable but rejecting cross-origin requests. Add CORS headers.
- Run
curl -I https://your-site.com/api/healthfrom your terminal. If it returns the CDN’s 404 page, the rewrite rules are missing or misconfigured. - Check that the production API URL matches what the frontend expects. Mismatched protocols (HTTP vs HTTPS), ports, or subdomains are common.
Recovery: Add the appropriate rewrite/proxy rules for your hosting provider (see Fix 8 above), redeploy, and verify with a smoke test. Total recovery time depends on how fast you can push a config change — typically under 10 minutes for Vercel/Netlify.
Prevention: Never rely solely on Vite’s dev proxy. During development, also test with vite preview (which serves the production build locally without the proxy) to catch this gap early. Add a CI step that builds and runs integration tests against the production build.
Still Not Working?
Request goes directly to Vite, not the proxy — Vite only proxies requests made by your app (e.g., fetch('/api/users')). Requests made by the browser’s address bar or external tools skip the Vite proxy. Also verify the URL starts with the exact proxy key.
404 from the proxy itself (not the backend) — if Vite can’t connect to the target at all, it returns 502 Bad Gateway, not 404. A 404 usually means the target received the request but doesn’t have that route. Check the path with and without the rewrite rule.
Proxy works in dev but fails in production — the Vite dev proxy is a local development tool. In production, the static build talks directly to the API. Handle CORS on the server side, or configure nginx/Cloudflare as a reverse proxy for production traffic.
ERR_PROXY_CONNECTION_FAILED or ECONNREFUSED — the target server is not running or is on the wrong port. Verify your backend is up with curl http://localhost:3000/health from the terminal.
Proxy adds a trailing slash and causes a redirect loop — some backends (especially those with strict routing) distinguish between /api/users and /api/users/. If the proxy rewrites the path and adds or strips a trailing slash, you can end up in a redirect loop. Log the proxy requests (Fix 6) to see exactly what URL is being forwarded.
Requests work in Chrome but fail in Firefox — Firefox enforces stricter rules around mixed content (HTTP/HTTPS) and secure cookies. If your proxy targets an HTTPS backend but serves the frontend over HTTP, Firefox may block the request. Ensure protocol consistency between frontend and backend.
Environment variables not loading in proxy config — if env.VITE_API_TARGET is undefined, verify the .env file is in the project root (not src/) and the variable name starts with VITE_. Non-prefixed variables are not loaded by loadEnv unless you pass an empty prefix. For more on this, see Fix: Vite Environment Variables Not Working.
For related issues, see Fix: Vite Failed to Resolve Import, Fix: Vite HMR Connection Lost, and Fix: CORS Access-Control-Allow-Origin Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Webpack/Vite Path Alias Not Working — Module Not Found with @/ Prefix
How to fix path alias errors in webpack and Vite — configuring resolve.alias, tsconfig paths, babel-plugin-module-resolver, Vite alias configuration, and Jest moduleNameMapper.
Fix: Vite Environment Variables Not Working
How to fix Vite environment variables showing as undefined — missing VITE_ prefix, wrong .env file for the mode, import.meta.env vs process.env, TypeScript types, and SSR differences.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.