Sechno
Web Development

Practical Patterns to Fix CORS When Calling AI APIs from the Browser

A hands-on guide for developers: why CORS breaks AI API calls, safe proxy patterns (Express, serverless/edge), streaming considerations, and security tradeoffs with actionable code examples.

SSechno Team 6 min read 55 views
Practical Patterns to Fix CORS When Calling AI APIs from the Browser

Why CORS becomes a problem with AI APIs

Browser apps often try to call AI provider endpoints directly. Many AI APIs either don’t send permissive CORS headers or require an API key you mustn’t expose to client-side code. The result: blocked requests, failed preflights, or leaked credentials. This guide shows practical, production-ready patterns to resolve those issues while minimizing latency and maintaining security.

High-level patterns

  • Server-side proxy (same origin): Your backend handles the API key and forwards requests. Simple and secure, but adds server load and latency.
  • Serverless / Edge proxy: Run a tiny function (Cloudflare Worker, Vercel Edge) close to users. Lower cold-starts and latency; ideal for global apps.
  • Token exchange / short-lived tokens: Backend mints short-lived tokens for the client so you avoid long-lived keys in the browser.
  • Direct CORS-enabled calls (only if supported): If the provider supports CORS and you can avoid sending secrets, call directly from the browser.

Tradeoffs — quick summary

  • Security vs simplicity: direct calls are simplest, proxies are more secure.
  • Latency: edge functions typically beat centralized APIs or origin servers for global users.
  • Streaming support: proxies must preserve streaming (chunked) responses; some serverless runtimes add complexity.
  • Cost & maintenance: running a proxy (even serverless) introduces additional cost and monitoring needs.

Example 1 — Minimal Express proxy (non-streaming)

Use this when you need a quick, auditable backend that injects an Authorization header and enforces an allow-list. This example forwards JSON requests and returns JSON responses.

const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json());
 
// Replace with your allowed origins
const ALLOWED_ORIGINS = new Set(['https://yourapp.example']);
 
app.use((req, res, next) => {
  const origin = req.get('Origin');
  if (ALLOWED_ORIGINS.has(origin)) {
    res.set('Access-Control-Allow-Origin', origin);
    res.set('Access-Control-Allow-Credentials', 'false');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
  }
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});
 
app.post('/api/ai-proxy', async (req, res) => {
  try {
    const resp = await fetch('https://api.ai-provider.example/v1/generate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.AI_API_KEY}`
      },
      body: JSON.stringify(req.body)
    });
 
    const data = await resp.json();
    res.status(resp.status).json(data);
  } catch (err) {
    console.error(err);
    res.status(502).json({ error: 'Upstream request failed' });
  }
});
 
app.listen(3000, () => console.log('Proxy listening on :3000'));

Notes

  • Only allow known origins and don't echo arbitrary Origin headers.
  • Add rate-limiting, request validation, and logging to avoid abuse.
  • For production, terminate TLS at a load balancer or platform like Heroku, Fly, or a managed container service.

Example 2 — Edge proxy with streaming (Cloudflare Worker)

Edge functions can forward streaming responses to the browser with low latency. The following Cloudflare Worker shows how to inject an Authorization header and set CORS headers while preserving streaming if the provider sends chunked responses.

addEventListener('fetch', event => event.respondWith(handle(event.request)));
 
async function handle(request) {
  // Only allow POST from your app origin
  const origin = request.headers.get('Origin') || '';
  if (request.method === 'OPTIONS') {
    return new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type'
      }
    });
  }
 
  if (origin !== 'https://yourapp.example') {
    return new Response('Origin not allowed', { status: 403 });
  }
 
  // Forward request to AI provider, preserving incoming body
  const upstreamUrl = 'https://api.ai-provider.example/v1/stream';
  const upstreamReq = new Request(upstreamUrl, {
    method: request.method,
    headers: {
      'Content-Type': request.headers.get('Content-Type') || 'application/json',
      'Authorization': `Bearer ${AI_API_TOKEN}` // store env secret in worker scope
    },
    body: request.body
  });
 
  const upstreamResp = await fetch(upstreamReq);
 
  // Mirror status and streaming body, with CORS header
  const responseHeaders = new Headers(upstreamResp.headers);
  responseHeaders.set('Access-Control-Allow-Origin', origin);
  responseHeaders.set('Access-Control-Expose-Headers', 'Content-Type');
 
  return new Response(upstreamResp.body, {
    status: upstreamResp.status,
    headers: responseHeaders
  });
}

Notes

  • Cloudflare Workers and other edge runtimes can access secrets (do not embed keys in code).
  • Some providers require extra headers or specific streaming behavior; test end-to-end.

Client-side: call the proxy from the browser

From the browser, call your proxy endpoint on the same origin (or an allowed origin). This avoids CORS failures as long as the server responds with the correct Access-Control-Allow-* headers.

async function callAi(payload) {
  const resp = await fetch('/api/ai-proxy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
  if (!resp.ok) throw new Error('Proxy error: ' + resp.status);
  return resp.json();
}
 
// Example usage
callAi({ prompt: 'Summarize the paragraph' })
  .then(result => console.log(result))
  .catch(err => console.error(err));

Streaming in the browser

If the AI provider streams responses, the proxy must forward the raw response body. With modern fetch and ReadableStream in the browser you can process tokens as they arrive. When using an edge proxy, return the upstream body directly to preserve streaming semantics (see the Worker example).

Common CORS errors and how to fix them

  • Blocked by CORS: No 'Access-Control-Allow-Origin' header — Ensure your proxy sets Access-Control-Allow-Origin to the allowed origin(s) rather than '*', especially when credentials are involved.
  • Preflight failures — OPTIONS requests must return 200/204 with Access-Control-Allow-Methods and Access-Control-Allow-Headers for the intended methods/headers.
  • 403/401 from upstream — Verify the proxy injects the correct Authorization header and that the token is valid. Log upstream responses on the server side.
  • Streaming broken — Confirm your runtime supports streaming fetch and that you return the upstream body stream without buffering it entirely.

Security and operational checklist

  1. Never embed long-lived API keys in client-side code or ship them to CDNs without encryption.
  2. Enforce a strict allow-list of origins or implement an authentication layer (session tokens or JWTs) between client and proxy.
  3. Implement rate-limiting, request size limits, and payload validation to mitigate abuse and cost spikes.
  4. Use short-lived tokens or token-exchange flows if your provider supports them.
  5. Monitor latency, error rates, and upstream quota usage; alert on anomalies.

When to choose each pattern

  • Small apps / prototypes: Simple Express or serverless POST proxy. Fast to implement.
  • Global user base / low latency: Edge proxy (Cloudflare Worker, Vercel Edge).
  • High streaming throughput: Use an environment that supports streaming without buffering (Workers, some serverless platforms, or a managed container).
  • Strict compliance or logging needs: Use a centralized backend so you can control logs, retention, and audits.

Troubleshooting tips

  • Inspect browser devtools network tab for preflight (OPTIONS) responses and missing headers.
  • Log upstream request/response headers and bodies on the proxy (redact secrets) to debug 401/403 errors.
  • Test from curl first: curl -v -X POST <proxy-url> -H 'Origin: https://yourapp.example' ... to validate server CORS behavior independent of the browser.

Conclusion

CORS errors with AI APIs are symptoms of two core constraints: browser-enforced origin policies and the need to keep API credentials secret. The right fix is usually a small server-side component (proxy or token-exchange) or an edge function that injects credentials and returns properly CORS-enabled responses. Choose the approach that balances latency, security, streaming needs, and operational cost for your app.

Further reading: see the original bootcamp tale for a developer perspective: How I Beat CORS Errors With AI APIs — A Bootcamp Tale.

Was this helpful?

Share this post

Comments (0)

Want to join the conversation?

Log in or sign up to leave a comment and share your thoughts.

Log in to Comment