TracepilotThat OpenAI Call From Your Browser Is Failing. Here's Why. You're fetching from...
You're fetching from https://api.openai.com/v1/chat/completions directly in the browser.
It works in dev. Then production hits you with a CORS error. Or a 401. Or worse — a silent failure where the request goes out but never comes back.
Sound familiar?
Let's cut through the noise. Here's what's actually breaking.
Browser JavaScript can't call OpenAI directly. OpenAI's API doesn't set Access-Control-Allow-Origin headers for browser origins. That's by design — they don't want API keys sitting in client-side code.
But you already know that. The issue is deeper.
When you do this:
// ❌ This will fail in production
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}`
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [...] })
});
Three things happen:
The fix isn't "just use a proxy." The fix is understanding why your specific setup is failing.
OpenAI's API response headers include:
access-control-allow-origin: *
Wait, that looks fine. So why does it fail?
Because the preflight request — the OPTIONS call the browser sends first — doesn't get past OpenAI's CDN. Cloudflare blocks it. The browser never sees the access-control-allow-origin header on the actual response because the preflight never completed.
Check your browser's network tab. You'll see:
OPTIONS https://api.openai.com/v1/chat/completions → 403
That's Cloudflare. Not OpenAI. Not your code.
You need a backend endpoint. Period. Here's the minimal Express proxy:
// server.js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.post('/api/chat', express.json(), async (req, res) => {
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3001);
This works. But now you're managing a proxy server. You need to handle streaming. You need to handle errors. You need to handle rate limiting.
The proxy works. But now you can't see why your agent is failing. The proxy logs say 200 OK. The response is garbage. What happened inside?
You don't know if:
This is where TracePilot comes in. One line change.
// Before — you're blind
const response = await openai.chat.completions.create({ model, messages });
// After — you see everything
const { result, spanId } = await tp.wrapOpenAI(
() => openai.chat.completions.create({ model, messages }),
messages
);
That's it. Now every call is traced. You open the dashboard, see the exact prompt that was sent, the exact output, the tokens used, the latency. If it fails, you fork the trace, edit the prompt, and replay.
No more guessing. No more "works on my machine."
Don't call OpenAI from the browser. Use a backend route. Instrument it with TracePilot. Now you have:
The proxy solves the network problem. TracePilot solves the debugging problem. Together, they solve the actual problem: your agent fails and you have no idea why.
That CORS error? It's a symptom. The real bug is you can't see what your agent is doing. Fix the visibility, and the network problems become trivial.
Debugging AI agents shouldn't feel like reading The Matrix.
Join other engineers who are building reliable autonomous workflows in our community: TracePilot Discord