Skip to content

Commit 054d144

Browse files
os-zhuangclaude
andauthored
fix(runtime): serve /discovery with Cache-Control: no-store (cloud#152) (#1963)
The discovery payload reflects MUTABLE runtime config — notably `routes.mcp`, which is only advertised when OS_MCP_SERVER_ENABLED=true. getDiscoveryInfo() (and isMcpEnabled(), which reads process.env fresh) recompute it correctly on every request, but the two discovery routes (`/.well-known/objectstack` and `${prefix}/discovery`) emitted no Cache-Control header, so an edge/CDN could cache the response. After MCP was enabled on a staging env, GET /discovery kept returning `routes.mcp: undefined` (stale, even with cache-busting query params), so the objectui Integrations page rendered "MCP not enabled" against a live MCP server. Mark both discovery routes `Cache-Control: no-store` so the payload is never cached — config changes (enable MCP, add a plugin/service) are reflected immediately. +regression test asserting the header on both routes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 296a0ed commit 054d144

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/runtime/src/dispatcher-plugin.routes.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import { createDispatcherPlugin } from './dispatcher-plugin.js';
1414

1515
function makeFakeServer() {
1616
const routes: string[] = [];
17-
const rec = (verb: string) => (path: string, _handler: unknown) => {
17+
const handlers: Record<string, (req: any, res: any) => any> = {};
18+
const rec = (verb: string) => (path: string, handler: any) => {
1819
routes.push(`${verb} ${path}`);
20+
handlers[`${verb} ${path}`] = handler;
1921
};
2022
return {
2123
routes,
24+
handlers,
2225
server: {
2326
get: rec('GET'),
2427
post: rec('POST'),
@@ -72,4 +75,25 @@ describe('createDispatcherPlugin — HTTP route registration', () => {
7275
expect(routes).toContain('POST /v2/mcp');
7376
expect(routes).toContain('POST /v2/keys');
7477
});
78+
79+
// cloud#152: discovery reflects mutable runtime config (e.g. routes.mcp toggles
80+
// with OS_MCP_SERVER_ENABLED). It must be served Cache-Control: no-store so an
81+
// edge/CDN never serves a stale payload after the config changes.
82+
it('serves both discovery routes with Cache-Control: no-store', async () => {
83+
const { server, handlers } = makeFakeServer();
84+
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
85+
await plugin.start?.(makeCtx(server));
86+
87+
for (const route of ['GET /.well-known/objectstack', 'GET /api/v1/discovery']) {
88+
const handler = handlers[route];
89+
expect(handler, `${route} should be registered`).toBeTypeOf('function');
90+
const headers: Record<string, string> = {};
91+
const res: any = {
92+
header: (k: string, v: string) => { headers[k] = v; },
93+
json: () => {},
94+
};
95+
await handler({}, res);
96+
expect(headers['Cache-Control'], `${route} Cache-Control`).toBe('no-store');
97+
}
98+
});
7599
});

packages/runtime/src/dispatcher-plugin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,14 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
452452
res.header(k, v);
453453
}
454454
}
455+
// Discovery reflects MUTABLE runtime config (which routes/services
456+
// are live — e.g. `mcp` only when OS_MCP_SERVER_ENABLED=true). It
457+
// must never be cached by an edge/CDN, or a config change (enable
458+
// MCP) leaves clients reading a stale payload that still says the
459+
// route is absent — the Integrations UI then shows "MCP not
460+
// enabled" against a live server (cloud#152). The body is computed
461+
// fresh per request; the only staleness is the HTTP cache layer.
462+
res.header('Cache-Control', 'no-store');
455463
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
456464
});
457465

@@ -462,6 +470,9 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
462470
res.header(k, v);
463471
}
464472
}
473+
// See the .well-known handler above: discovery must not be cached
474+
// (mutable runtime config; cloud#152 stale `routes.mcp`).
475+
res.header('Cache-Control', 'no-store');
465476
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
466477
});
467478

0 commit comments

Comments
 (0)