Skip to content

Commit 756adc3

Browse files
gxklclaude
andcommitted
fix(service-worker): address PR review findings
- FetchEventHandler: run initRoutes inside the try so a route-registration failure returns the unified 500 instead of rejecting; non-Error throws use String(e); merge ctx.responseHeaders via getSetCookie() so multiple Set-Cookie are preserved (not folded). - ResponseUtils: return Uint8Array / ArrayBuffer / Blob / URLSearchParams / FormData (and any ArrayBuffer view) as a body instead of JSON-serializing. - controller-plugin: guard `config.mcp.hooks` assignment with mcpEnable() so it does not throw when MCP is disabled. - controller-runtime HTTPMethodRegister: scope hostRouter per host iteration so a falsy host does not inherit the previous host's router. - wiki: bump stale updated_at on the two touched pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6276726 commit 756adc3

8 files changed

Lines changed: 58 additions & 8 deletions

File tree

tegg/core/controller-runtime/src/lib/impl/http/HTTPMethodRegister.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ export abstract class HTTPMethodRegister {
6161
this.checkDuplicateInRouter(this.router);
6262

6363
// 2. check duplicate with host tegg controller
64-
let hostRouter: Router | undefined;
6564
const hosts = this.controllerMeta.getMethodHosts(this.methodMeta) || [];
6665
hosts.forEach((h) => {
66+
// Per-iteration so a falsy host does not inherit the previous host's router.
67+
let hostRouter: Router | undefined;
6768
if (h) {
6869
hostRouter = this.checkRouters.get(h);
6970
if (!hostRouter) {

tegg/plugin/controller/src/app.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
147147
this.controllerLoadUnitHandler = new ControllerLoadUnitHandler(this.app);
148148
await this.controllerLoadUnitHandler.ready();
149149

150-
this.app.config.mcp.hooks = EggMcpRouter.hooks;
150+
// Guarded like every other config.mcp access — when MCP is disabled
151+
// config.mcp may be absent, and there is no router to publish hooks for.
152+
if (this.mcpEnable()) {
153+
this.app.config.mcp.hooks = EggMcpRouter.hooks;
154+
}
151155
});
152156
}
153157

tegg/standalone/service-worker-controller/src/http/FetchEventHandler.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ export class FetchEventHandler extends AbstractEventHandler<FetchEvent, Response
5757
}
5858

5959
async handleEvent(event: FetchEvent): Promise<Response> {
60-
await this.initRoutes();
6160
const ctx = this.fetchContextFactory?.create({ event }) ?? new ServiceWorkerFetchContext({ event });
6261
try {
62+
// Inside the try so a route-registration failure returns the unified 500
63+
// error shape instead of rejecting handleEvent's promise.
64+
await this.initRoutes();
6365
await this.#routes!(ctx, async () => {
6466
/* noop */
6567
});
@@ -68,13 +70,14 @@ export class FetchEventHandler extends AbstractEventHandler<FetchEvent, Response
6870
return ResponseUtils.createErrorResponse(404, 'NOT_FOUND', `${ctx.method} ${ctx.path} not found`);
6971
}
7072
return await this.#guardResponseStream(this.#mergeResponseHeaders(response, ctx.responseHeaders));
71-
} catch (e: any) {
73+
} catch (e) {
7274
const mapped = this.errorResponseMapper?.toResponse(e, ctx);
7375
if (mapped) {
7476
return mapped;
7577
}
7678
console.error('[service-worker] handle fetch event failed:', e);
77-
return ResponseUtils.createErrorResponse(500, 'INTERNAL_SERVER_ERROR', e?.message ?? 'internal error');
79+
const message = e instanceof Error ? e.message : String(e);
80+
return ResponseUtils.createErrorResponse(500, 'INTERNAL_SERVER_ERROR', message);
7881
}
7982
}
8083

@@ -95,8 +98,16 @@ export class FetchEventHandler extends AbstractEventHandler<FetchEvent, Response
9598
}
9699
const headers = new Headers(response.headers);
97100
for (const [key, value] of extra.entries()) {
101+
// `entries()` folds multiple Set-Cookie into one comma-joined value, which
102+
// corrupts cookies; carry them over individually via getSetCookie().
103+
if (key === 'set-cookie') {
104+
continue;
105+
}
98106
headers.set(key, value);
99107
}
108+
for (const cookie of extra.getSetCookie()) {
109+
headers.append('set-cookie', cookie);
110+
}
100111
return new Response(response.body, {
101112
status: response.status,
102113
statusText: response.statusText,

tegg/standalone/service-worker-controller/src/utils/ResponseUtils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,18 @@ export class ResponseUtils {
1919
if (typeof body === 'undefined' || body === null) {
2020
return new Response(null, { status: 204 });
2121
}
22-
if (Buffer.isBuffer(body) || typeof body === 'string' || body instanceof ReadableStream) {
22+
if (
23+
typeof body === 'string' ||
24+
body instanceof ReadableStream ||
25+
// Buffer is a Uint8Array; also cover the other web-standard BodyInit types
26+
// so a controller returning binary/form bodies is not JSON-serialized.
27+
body instanceof Uint8Array ||
28+
body instanceof ArrayBuffer ||
29+
body instanceof Blob ||
30+
body instanceof URLSearchParams ||
31+
body instanceof FormData ||
32+
ArrayBuffer.isView(body)
33+
) {
2334
return new Response(body as BodyInit, { status: 200 });
2435
}
2536
if (body instanceof Readable) {

tegg/standalone/service-worker/test/ServiceWorkerApp.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ describe('standalone/service-worker/test/ServiceWorkerApp.test.ts', () => {
7878
assert.equal(res.headers.get('x-added'), '1');
7979
});
8080

81+
it('should return a Uint8Array body as bytes, not JSON', async () => {
82+
const res = await fetch(`${base}/edge/bytes`);
83+
assert.equal(await res.text(), 'byte-body');
84+
});
85+
86+
it('should preserve multiple ctx.responseHeaders Set-Cookie on the merged response', async () => {
87+
const res = await fetch(`${base}/edge/mw-cookies`);
88+
assert.deepEqual(res.headers.getSetCookie(), ['x=1', 'y=2']);
89+
assert.deepEqual(await res.json(), { ok: true });
90+
});
91+
8192
it('should run aop-mode middlewares (@Middleware with advice classes)', async () => {
8293
const res = await fetch(`${base}/aop-mw/aop`);
8394
assert.deepEqual(await res.json(), { body: { msg: 'hello' } });

tegg/standalone/service-worker/test/fixtures/hello-app/EdgeController.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,16 @@ export class EdgeController {
2424
ctx.responseHeaders.set('x-added', '1');
2525
return Response.redirect('http://localhost/dest', 302);
2626
}
27+
28+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/bytes' })
29+
async bytes() {
30+
return new TextEncoder().encode('byte-body');
31+
}
32+
33+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/mw-cookies' })
34+
async mwCookies(@InjectContext() ctx: any) {
35+
ctx.responseHeaders.append('set-cookie', 'x=1');
36+
ctx.responseHeaders.append('set-cookie', 'y=2');
37+
return { ok: true };
38+
}
2739
}

wiki/concepts/tegg-module-plugin.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ source_files:
3232
- tegg/plugin/controller/src/lib/impl/http/EggHTTPControllerRegistrar.ts
3333
- tegg/plugin/controller/src/lib/impl/mcp/EggMCPRegisterProvider.ts
3434
- tegg/standalone/service-worker-controller/src/http/FetchEventHandler.ts
35-
updated_at: 2026-07-13
35+
updated_at: 2026-07-14
3636
status: active
3737
---
3838

wiki/packages/service-worker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ source_files:
99
- tegg/core/controller-runtime/src
1010
- tegg/plugin/controller/src
1111
- examples/helloworld-service-worker
12-
updated_at: 2026-07-10
12+
updated_at: 2026-07-14
1313
status: active
1414
---
1515

0 commit comments

Comments
 (0)