Fix: Vite Proxy Not Working — API Requests Not Forwarded or 404/502 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:
- 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.
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.
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.
For related Vite issues, see Fix: Vite Failed to Resolve Import and Fix: Vite HMR Connection Lost.
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.