Skip to content

Commit ef50372

Browse files
os-zhuangclaude
andauthored
fix(hono): allow If-Match in default CORS headers so cross-origin OCC saves work (objectui#2572) (#3067)
The REST record PATCH accepts the OCC token as an If-Match header — objectui's record-level inline edit sends it on every save — but the default CORS allow-list (plugin-hono-server + the hono adapter) omitted it, so on split-origin deployments the preflight failed and every inline-edit save died in the browser with 'Failed to fetch'. Found live while dogfooding objectui#2572 (console dev :5191 against a showcase backend on :4022); same split-origin class as the #2548 Bearer fixes. Explicit user-supplied allowHeaders still win unchanged; new unit test pins If-Match in the default list. Claude-Session: https://claude.ai/code/session_01JQjNZDdqmz2QHGQndpjZrR Co-authored-by: Claude <noreply@anthropic.com>
1 parent 330ceba commit ef50372

4 files changed

Lines changed: 34 additions & 2 deletions

File tree

.changeset/cors-allow-if-match.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-hono-server': patch
3+
'@objectstack/hono': patch
4+
---
5+
6+
CORS default `allowHeaders` now includes `If-Match`. The REST record update
7+
accepts the OCC token as an `If-Match` header (objectui's record-level inline
8+
edit sends it on every save), but the preflight allow-list omitted it — so on
9+
any split-origin deployment (console dev server against a backend on another
10+
origin) the browser failed the preflight and every inline-edit save died with
11+
"Failed to fetch". Found live while dogfooding objectui#2572; same
12+
split-origin failure class as the #2548 Bearer fixes. Explicit user-supplied
13+
`allowHeaders` still win unchanged.

packages/adapters/hono/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
186186
app.use('*', cors({
187187
origin: origin as any,
188188
allowMethods: corsOpts.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
189-
allowHeaders: corsOpts.allowHeaders || ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id'],
189+
// Keep in sync with plugin-hono-server's defaultAllowHeaders — `If-Match`
190+
// carries the OCC token on cross-origin record PATCHes (objectui#2572).
191+
allowHeaders: corsOpts.allowHeaders || ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id', 'If-Match'],
190192
exposeHeaders,
191193
credentials,
192194
maxAge,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ describe('HonoServerPlugin', () => {
255255
expect(corsConfigCapture.last.allowHeaders).toContain('Authorization');
256256
});
257257

258+
it('should allow If-Match by default (OCC token on cross-origin record PATCHes)', async () => {
259+
corsConfigCapture.last = undefined;
260+
261+
const plugin = new HonoServerPlugin();
262+
await plugin.init(context as PluginContext);
263+
264+
// objectui#2572 dogfood find: the record-level inline edit sends the
265+
// OCC token as an `If-Match` header; a preflight that doesn't allow
266+
// it makes every split-origin save fail with "Failed to fetch".
267+
expect(corsConfigCapture.last.allowHeaders).toContain('If-Match');
268+
});
269+
258270
it('should merge user-supplied exposeHeaders with set-auth-token default', async () => {
259271
corsConfigCapture.last = undefined;
260272

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,12 @@ export class HonoServerPlugin implements Plugin {
279279
// the better-auth `bearer()` plugin can deliver rotated
280280
// session tokens to cross-origin clients (see plugin-auth).
281281
// User-supplied exposeHeaders are merged with this default.
282-
const defaultAllowHeaders = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id'];
282+
// `If-Match` carries the OCC token on record PATCHes (objectui's
283+
// record-level inline edit, REST `update` with `ifMatch`) — without
284+
// it in the preflight allow-list, every cross-origin save fails in
285+
// the browser with "Failed to fetch" (objectui#2572 dogfood find;
286+
// same split-origin class as the #2548 Bearer fixes).
287+
const defaultAllowHeaders = ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Tenant-ID', 'X-Environment-Id', 'If-Match'];
283288
const defaultExposeHeaders = ['set-auth-token'];
284289
const allowHeaders = corsOpts.allowHeaders ?? defaultAllowHeaders;
285290
const exposeHeaders = Array.from(new Set([

0 commit comments

Comments
 (0)