GuidesNODE.JS

Fix CORS errors in Hono

Browsers block cross-origin reads unless the target server responds with the expected access control headers. Configuring hono cors middleware on your routes emits those headers for plain requests, preflight checks, and credentialed calls, and the same code runs unchanged on Cloudflare Workers, Node.js, Bun, and Deno.

Confirm it is a CORS problem

Confirm the failure comes from browser cross-origin policy rather than an application crash or a network failure. The diagnosis guide walks through reading the console message and the failing network entry. If you are calling a third-party API you do not control, you cannot change its headers and need a proxy instead of middleware.

No package to install

Hono ships CORS support in its core distribution, exported from hono/cors. There is no separate npm package to install or version-match.

Enable CORS for all routes

Built in, mounted per path
import { Hono } from 'hono';
import { cors } from 'hono/cors';

const app = new Hono();

// Before routes; '/api/*' scopes the headers to that prefix,
// use '*' to cover every route
app.use('/api/*', cors());

app.all('/api/abc', (c) => {
  return c.json({ success: true });
});

export default app;

Mount cors() with app.use() before defining the routes it should cover. The path pattern scopes the middleware: '*' applies the headers to every endpoint, while '/api/*' restricts them to routes under the /api prefix.

Route handlers registered before the app.use() call run without the middleware, so requests to those routes return responses without CORS headers.

Restrict origins in production

Fixed allowlist or a function
app.use(
  '/api/*',
  cors({
    origin: ['https://app.example.com', 'https://admin.example.com'],
  }),
);

// Function form: return the origin to allow it, or a fallback
app.use(
  '/admin/*',
  cors({
    origin: (origin) =>
      origin.endsWith('.example.com') ? origin : 'https://app.example.com',
  }),
);

The origin default of * lets any client origin read API responses. For production services paired with specific web applications, set origin to an exact domain string, an array of allowed domains, or a function.

The function form (origin, c) => string receives the request origin and the Hono context. Return the origin string to allow the caller, or return a fixed fallback such as your own production origin.

How preflight requests are handled

The middleware answers preflight OPTIONS requests on the paths where it is mounted and returns a 204 response. It populates Access-Control-Allow-Methods from allowMethods, which defaults to ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY'].

The allowHeaders default is an empty list, so no Access-Control-Allow-Headers header is sent. Requests carrying non-safelisted headers, such as Authorization or a Content-Type of application/json, fail preflight until you list those headers in allowHeaders explicitly.

Sending cookies or Authorization

Credentials plus the headers they travel in
app.use(
  '/api/*',
  cors({
    origin: 'https://app.example.com',
    credentials: true,
    allowHeaders: ['Content-Type', 'Authorization'],
  }),
);

// Browser side, cookies only flow when requested:
await fetch('https://api.example.com/account', { credentials: 'include' });
// axios: axios.get(url, { withCredentials: true })

Setting credentials: true adds Access-Control-Allow-Credentials: true to the response. Browsers reject a credentialed response whose Access-Control-Allow-Origin is *, so origin must resolve to an explicit string.

The client declares credentials on the outgoing call. Use fetch(url, { credentials: 'include' }), or configure axios with withCredentials: true.

Verify the fix

Probe the plain request and the preflight
$ curl -i -H "Origin: https://app.example.com" http://localhost:3000/api/abc

HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com

$ curl -i -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" http://localhost:3000/api/abc

HTTP/1.1 204 No Content
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, HEAD, PUT, POST, DELETE, PATCH, QUERY

A valid preflight returns 204 with Access-Control-Allow-Origin echoing your frontend origin and Access-Control-Allow-Methods and Access-Control-Allow-Headers covering the request. Missing Access-Control-Allow-Headers on a JSON POST points at the empty allowHeaders default. Paste the response into the header checker to evaluate it.

Good questions.

Does the same CORS middleware work on Cloudflare Workers and Node.js?

Yes. The hono/cors middleware is runtime-agnostic and behaves identically on Cloudflare Workers, Node.js, Bun, and Deno, so the configuration carries over when you switch runtimes.

Why does my JSON POST fail preflight in Hono?

allowHeaders defaults to an empty list. Because Content-Type: application/json is not CORS-safelisted, the browser's preflight fails until you set allowHeaders: ['Content-Type'] or a wider list.

Does CORS stop requests from running on the server?

No. CORS is enforced by the browser to protect client-side data. The Hono server still receives, executes, and answers the request; the browser withholds the response from your script when the headers are missing or wrong.

Why does Vite interfere with Hono CORS headers in local development?

Vite's dev server applies its own CORS handling by default. Set server: { cors: false } in vite.config.ts so the Hono middleware owns the headers; see the Vite recipe for the general proxy setup.