Skip to content

Fix: Vite Proxy Not Working — API Requests Not Forwarded or 404/502 Errors

FixDevs ·

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/users

Or 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/users

Or WebSocket connections fail through the proxy:

WebSocket connection to 'ws://localhost:5173/socket.io/' failed

Or 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, not pathRewrite (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 HTTP Host header must be updated. Without changeOrigin: true, some backends reject the request.
  • Path not starting with the proxy key — Vite matches requests where the URL starts with the key. /api matches /api/users but not /v1/api/users.
  • WebSocket proxy not enabled — WebSocket connections require ws: true in the proxy config.
  • HTTPS target with self-signed cert — Vite’s proxy rejects invalid TLS certificates by default.
  • Target URL with trailing slashhttp://localhost:3000/ vs http://localhost:3000 behaves 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:

Vitewebpack
rewritepathRewrite
changeOriginchangeOrigin
wsws
targettarget

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), changeOrigin may not be strictly required. For any external host or when you see 403 Forbidden from 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: true is 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_SIGNATURE

Disable 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: false in 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:

  1. Open DevTools → Network tab
  2. Find the failing request
  3. Check the Request URL — if it shows localhost:5173/api/..., the proxy should be handling it
  4. 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.com

Disable 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles