Skip to content

Commit 2bb23b2

Browse files
committed
cors
1 parent c87c335 commit 2bb23b2

File tree

3 files changed

+47
-5
lines changed

3 files changed

+47
-5
lines changed

packages/plugins/plugin-hono-server/src/hono-plugin.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
99
import { serveStatic } from '@hono/node-server/serve-static';
1010
import * as fs from 'fs';
1111
import * as path from 'path';
12-
import { createOriginMatcher, hasWildcardPattern } from './pattern-matcher';
12+
import { createOriginMatcher, hasWildcardPattern, isLocalhostOrigin } from './pattern-matcher';
1313

1414
export interface StaticMount {
1515
root: string;
@@ -129,7 +129,11 @@ export class HonoServerPlugin implements Plugin {
129129
const credentials = corsOpts.credentials ?? (process.env.CORS_CREDENTIALS !== 'false');
130130
const maxAge = corsOpts.maxAge ?? (process.env.CORS_MAX_AGE ? parseInt(process.env.CORS_MAX_AGE, 10) : 86400);
131131

132-
// Determine origin handler based on configuration
132+
// Determine origin handler based on configuration.
133+
// Always use a function so that localhost origins are
134+
// automatically allowed regardless of the configured
135+
// pattern list (handled inside matchOriginPattern /
136+
// createOriginMatcher).
133137
let origin: string | string[] | ((origin: string) => string | undefined | null);
134138

135139
// When credentials is true, browsers reject wildcard '*' for Access-Control-Allow-Origin.
@@ -143,8 +147,10 @@ export class HonoServerPlugin implements Plugin {
143147
// Use pattern matcher to support subdomain and port wildcards
144148
origin = createOriginMatcher(configuredOrigin);
145149
} else {
146-
// Exact origin(s) - pass through as-is
147-
origin = configuredOrigin;
150+
// Exact origin(s) — wrap in a function so localhost is
151+
// still auto-allowed via the matcher.
152+
const matcher = createOriginMatcher(configuredOrigin);
153+
origin = (requestOrigin: string) => matcher(requestOrigin);
148154
}
149155

150156
const rawApp = this.server.getRawApp();

packages/plugins/plugin-hono-server/src/pattern-matcher.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,22 @@ describe('createOriginMatcher', () => {
119119

120120
expect(matcher('http://localhost:3000')).toBe('http://localhost:3000');
121121
expect(matcher('http://localhost:8080')).toBe('http://localhost:8080');
122-
expect(matcher('http://127.0.0.1:3000')).toBe(null);
122+
// 127.0.0.1 is also considered localhost and always allowed
123+
expect(matcher('http://127.0.0.1:3000')).toBe('http://127.0.0.1:3000');
124+
});
125+
126+
it('should always allow localhost origins regardless of configured patterns', () => {
127+
// Even with a restrictive pattern, localhost should be allowed
128+
const matcher = createOriginMatcher('https://app.example.com');
129+
130+
expect(matcher('http://localhost:3000')).toBe('http://localhost:3000');
131+
expect(matcher('http://localhost:5173')).toBe('http://localhost:5173');
132+
expect(matcher('https://localhost:8443')).toBe('https://localhost:8443');
133+
expect(matcher('http://localhost')).toBe('http://localhost');
134+
expect(matcher('http://127.0.0.1:3000')).toBe('http://127.0.0.1:3000');
135+
expect(matcher('http://[::1]:3000')).toBe('http://[::1]:3000');
136+
// Non-localhost should still be rejected
137+
expect(matcher('https://evil.com')).toBe(null);
123138
});
124139
});
125140

packages/plugins/plugin-hono-server/src/pattern-matcher.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,35 @@
1818
* CORS errors on Vercel.
1919
*/
2020

21+
/**
22+
* Returns true when the origin points to localhost (any port, http or https).
23+
*
24+
* Matches:
25+
* - `http://localhost`
26+
* - `http://localhost:3000`
27+
* - `https://localhost:8443`
28+
* - `http://127.0.0.1:5173`
29+
* - `http://[::1]:3000`
30+
*/
31+
export function isLocalhostOrigin(origin: string): boolean {
32+
return /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin);
33+
}
34+
2135
/**
2236
* Check if an origin matches a pattern with wildcards.
2337
*
38+
* Localhost origins (`http(s)://localhost:<any-port>`, `127.0.0.1`, `[::1]`)
39+
* are **always allowed** regardless of the pattern — this removes the need to
40+
* enumerate every development port in `CORS_ORIGIN`.
41+
*
2442
* @param origin The origin to check (e.g., `https://app.example.com`)
2543
* @param pattern The pattern to match against (supports `*` wildcard)
2644
* @returns true if origin matches the pattern
2745
*/
2846
export function matchOriginPattern(origin: string, pattern: string): boolean {
47+
// Always allow localhost for development convenience
48+
if (isLocalhostOrigin(origin)) return true;
49+
2950
if (pattern === '*') return true;
3051
if (pattern === origin) return true;
3152

0 commit comments

Comments
 (0)