Fixing CSP and CORS Port Mismatch Errors in Express Development

Summary

An Express server running on localhost:3000 served an API that worked fine in Postman and the VS Code terminal but failed in Chrome with Content Security Policy (CSP) violations, 404 errors, and unexpected redirects to port 3001. The problem stemmed from the CSP header, the origin misconfiguration in CORS, and a missing connect-src directive that prevented the browser from fetching the API while Postman was unaffected.

Root Cause

  • CSP mismatch: The page’s CSP (commented out but still active in some browsers via Meta or server headers) listed connect-src 'self' http://localhost:3000. The request was made from http://localhost:3000 but the fetch used a relative URL (/api/v1/imageAndQuote) that resolved to http://localhost:3001 due to a mis‑set port variable in the client’s environment.
  • CORS origin error: corsOptions.origin was set to http://localhost:${port}—but port referenced the client after a node process restart, not the server’s port, leading to an origin mismatch.
  • Port conflict: The dev server (e.g., vite or webpack-dev-server) automatically fell back to 3001 when 3000 was occupied, causing the browser to request the API from an unconfigured port.
  • Static file path: The <script src="/public/js/index.js"> resolved incorrectly due to Express’s express.static("public") serving files from /public, not /public/js, resulting in 404 for the client script.

Why This Happens in Real Systems

  • Multiple runtimes (developer machine, CI, Docker) often run on different ports, so hard‑coding ports in code is fragile.
  • Browsers enforce CSP strictly; a mis‑declared policy silently blocks requests while external tools like Postman ignore it.
  • Webpack/Vite dev servers automatically proxy to the next available port, which can break hard‑coded API URLs if not handled.
  • Missing separation of environment variables for client vs. server leads to circular references.

Real-World Impact

  • Users see blank pages or 404 errors instead of the expected data.
  • Debugging time increases due to silent CSP errors that require inspecting dev‑tools console logs.
  • Production deployment may silently fail if CSP/port mismatches persist, causing data fetch failures and poor user experience.

Example or Code (if necessary and relevant)

// index.js (client)
async function imageAndQuote() {
  try {
    const endpoint = `${window.location.origin}/api/v1/imageAndQuote`;
    const response = await fetch(endpoint);
    const result = await response.json();
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}
imageAndQuote();
// server.js (Express)
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';

dotenv.config();
const app = express();
const PORT = 3000;

const corsOptions = {
  origin: `http://localhost:${PORT}`,
  credentials: true,
};

app.use(cors(corsOptions));
app.use(express.static('public'));
app.use(express.json());

app.get('/api/v1/imageAndQuote', async (req, res) => {
  try {
    const imageUrl = await fetch(
      `https://api.unsplash.com/topics?client_id=${process.env.UNSPLASH_KEY}`
    ).then(r => r.json());
    res.json({ status: 200, data: imageUrl });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}`);
});

How Senior Engineers Fix It

  • Separate environment files: Use .env.development and .env.production with distinct API_URL and PORT variables.
  • Dynamic CSP: Generate CSP headers at runtime using the actual origin and port.
    const csp = `default-src 'self'; connect-src 'self' http://localhost:${PORT};`;
    app.use((req, res, next) => {
      res.set('Content-Security-Policy', csp);
      next();
    });
  • Proxy API calls in the dev server: Configure Vite/webpack to proxy /api to http://localhost:3000 so the browser always hits the same port.
  • Use absolute URLs in fetch calls and rely on environment variables so the client never assumes the port.
  • Add comprehensive unit tests for network requests that simulate different ports and CSP scenarios.

Why Juniors Miss It

  • Assuming hard‑coded ports work: New developers often forget port conflicts and dev server fallbacks.
  • Overlooking CSP: They may not realize CSP is enforced by browsers but ignored by tools like Postman.
  • Ignoring environment separation: Mixing server and client config leads to circular dependencies.
  • Relying on relative URLs: The assumption that /api/... always resolves correctly ignores port and host differences.

By separating concerns, generating runtime headers, and testing across environments, you can avoid these pitfalls and ensure your API calls work consistently in both development and production.

Leave a Comment