Why My API Works in Postman but Not in Browser — Complete Debugging Guide

You test your API in Postman — works perfectly. You paste the same URL into JavaScript — and it crashes. This is one of the most common frustrations in web development, and it has a handful of well-known root causes. This guide covers every one of them with working fixes.

#1

cause: missing CORS headers

95%

of cases solved by 3 fixes

5+

root causes covered here

2 min

to diagnose with this guide

1

Why Postman Succeeds Where the Browser Fails

Postman is a native application or browser extension that sends requests directly, without any browser security model applied. Your browser, on the other hand, enforces several security policies that Postman blissfully ignores.

ItemPostman / curlBrowser Fetch / XHR
CORS enforced?❌ Never✅ Always
Preflight OPTIONS?❌ Never✅ For non-simple requests
Cookie scopeSends any cookie you addSameSite / Domain rules apply
Auth headersAlways sentBlocked if CORS not set up
Mixed contentNot applicableHTTP blocked on HTTPS pages
Origin headerNone (or manual)Auto-set by browser

Quick fact

The browser is not broken — it is enforcing security policies that protect your users. The fix almost always lives on the server, not in your frontend code.

2

Root Cause 1 — CORS (Cross-Origin Resource Sharing)

CORS is by far the most common culprit. The browser checks whether the server explicitly permits requests from your frontend's origin (scheme + host + port). If the server doesn't include the right response headers, the browser blocks the response — even if the server returned 200 OK.

Browser sends request

Server responds (any status)

Browser checks Access-Control-Allow-Origin

Header matches origin?

Allow — JS receives response

Classic CORS error in console

Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The fix is always server-side. Add the correct response headers:

httpServer Response Headers
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true   # only if you send cookies

Here's how to set this in common backend frameworks:

javascriptExpress.js (Node)
const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',      // your frontend origin
  methods: ['GET','POST','PUT','DELETE'],
  allowedHeaders: ['Content-Type','Authorization'],
  credentials: true,                 // only if using cookies
}));
pythonFastAPI (Python)
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
  CORSMiddleware,
  allow_origins=["https://myapp.com"],
  allow_credentials=True,
  allow_methods=["*"],
  allow_headers=["*"],
)

Do NOT use wildcard (*) with credentials

Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true cannot be combined. Use an explicit origin instead.
3

Root Cause 2 — Preflight OPTIONS Request

For any request with a custom header (like Authorization) or a non-GET/POST method, the browser first sends a silent OPTIONS preflight to ask the server for permission. If the server doesn't handle OPTIONS, it returns 405 Method Not Allowed, and the real request never fires.

Missing OPTIONS handler

❌ Bad
# Server ignores OPTIONS — preflight fails
app.post('/api/data', handler)
# No OPTIONS route defined

OPTIONS handled

✅ Good
# Express: cors() handles OPTIONS automatically
app.use(cors(corsOptions));

# Or manually:
app.options('*', cors(corsOptions));  // enable preflight for all routes
app.post('/api/data', cors(corsOptions), handler);

How to spot a preflight issue

Open DevTools → Network tab → look for an OPTIONS request to your endpoint. If it returns 4xx or has no CORS headers — that's your problem.
4

Root Cause 3 — Auth Headers Not Sent

Postman lets you manually attach any header. The browser's fetch() API requires you to explicitly include credentials in your code. A missing Authorization header is the second most common issue after CORS.

No auth header

❌ Bad
// Auth header forgotten — server returns 401
fetch('https://api.example.com/data')
  .then(res => res.json())

Auth header included

✅ Good
// Include Authorization header explicitly
const token = localStorage.getItem('token');

fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
})
  .then(res => res.json())
5

Root Cause 4 — Cookies & Credentials

By default, fetch() does not send cookies. You must opt in withcredentials: 'include'. The server must also explicitly allow credentials in its CORS headers, and the cookie must meet SameSite requirements.

Cookies not sent

❌ Bad
// Cookies not sent — session breaks
fetch('/api/user/profile')
  .then(res => res.json())

Cookies included

✅ Good
// Send cookies with every request
fetch('/api/user/profile', {
  credentials: 'include',   // sends cookies
})
  .then(res => res.json())

// Server must respond with:
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: https://myapp.com (exact, no wildcard)

credentials: "omit"

Default — no cookies, no TLS certs sent.

credentials: "same-origin"

Cookies only to same domain. Good default for most apps.

credentials: "include"

Always send cookies. Required for cross-origin sessions.

SameSite=None; Secure

Cookie attribute required for cross-site cookies in modern browsers.

6

Root Cause 5 — Mixed Content (HTTP vs HTTPS)

If your frontend is served over HTTPS but your API is HTTP, the browser blocks the request entirely. This is called mixed content and is a security protection — not a bug.

Mixed content error

Mixed Content: The page at 'https://myapp.com' was loaded over HTTPS, but requested an insecure resource 'http://api.example.com/data'. This request has been blocked; the content must be served over HTTPS.
1

Upgrade your API to HTTPS

Use a free Let's Encrypt certificate. This is the correct long-term fix.

2

Use a reverse proxy

Serve your HTTP API behind an Nginx/Cloudflare HTTPS proxy.

3

Use environment variables for API base URL

Set API_URL=https://... in production so the URL is always HTTPS in prod.

const API = process.env.NEXT_PUBLIC_API_URL || "https://api.example.com";
4

Temporary: serve frontend over HTTP too

Not recommended for production — only for local development.

7

Root Cause 6 — Content-Type Header

Sending a JSON body without Content-Type: application/json causes the server to misread the body. This makes the request fail on the server side, while it worked in Postman because Postman auto-sets it.

Missing Content-Type

❌ Bad
// Body is sent as plain text — server can't parse it
fetch('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice', age: 30 }),
})

Content-Type set

✅ Good
// Correct: tell the server you're sending JSON
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({ name: 'Alice', age: 30 }),
})
8

Diagnostic Checklist

1

Open DevTools → Network tab

Find your request. Look at the Response Headers for Access-Control-Allow-Origin.

2

Check the Console tab

CORS errors, mixed-content warnings, and auth failures all appear here with exact details.

3

Look for the OPTIONS preflight

If you see a red OPTIONS request before your main request — preflight is failing.

4

Check the request headers

Click your request → Headers tab → verify Authorization and Content-Type are present.

5

Check the response status

401 = auth missing/wrong. 403 = forbidden. 0 = CORS blocked (browser won't show response).

6

Confirm API URL uses HTTPS if frontend does

Check the request URL in the network tab — http:// on an https:// page is instant block.

9

Quick Reference: Error Messages Decoded

ItemError MessageRoot Cause & Fix
No Access-Control-Allow-Origin headerCORS errorAdd CORS headers on server
Response to preflight has invalid HTTP statusPreflight (OPTIONS) failHandle OPTIONS on server
401 UnauthorizedAuth header missingAdd Authorization header in fetch()
Mixed Content blockedHTTP API from HTTPS pageUse HTTPS for API
Failed to parse JSON bodyContent-Type missingAdd Content-Type: application/json
Request blocked by cookie policySameSite/credentials mismatchUse credentials: include + SameSite=None
10

Development Proxy: The Full Workaround

During local development, you can use a proxy to avoid CORS entirely by making the browser think the API is on the same origin:

jsonnext.config.js (Next.js)
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:8000/:path*', // your backend
      },
    ];
  },
};
jsonvite.config.js (Vite)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
  },
};

Best practice for production

In production, use a proper CORS configuration on your backend instead of a proxy. The proxy approach is ideal for local dev to avoid touching server config.

Frequently Asked Questions