From ff12da514af2a40f9f991ac1199abfc1f472a15c Mon Sep 17 00:00:00 2001
From: Jinho Ayden Jeong <144667387+ayden94@users.noreply.github.com>
Date: Thu, 7 May 2026 21:47:07 +0900
Subject: [PATCH] Resolve #1662: Add runtime edge regression coverage
---
packages/runtime/src/application.test.ts | 113 +++++++++++++++++-
.../runtime/src/node/node-request.test.ts | 25 ++++
packages/runtime/src/web.test.ts | 32 +++++
3 files changed, 167 insertions(+), 3 deletions(-)
diff --git a/packages/runtime/src/application.test.ts b/packages/runtime/src/application.test.ts
index be5f939dd..6e5af5e41 100644
--- a/packages/runtime/src/application.test.ts
+++ b/packages/runtime/src/application.test.ts
@@ -772,6 +772,57 @@ describe('bootstrapApplication', () => {
await app.close();
});
+ it('uses maxBodySize as the Node adapter multipart total-size fallback', async () => {
+ @Controller('/uploads')
+ class UploadController {
+ @Post('/')
+ upload(_input: undefined, context: RequestContext) {
+ return {
+ body: context.request.body,
+ fileCount: context.request.files?.length ?? 0,
+ };
+ }
+ }
+
+ class AppModule {}
+ defineModule(AppModule, {
+ controllers: [UploadController],
+ });
+
+ const port = await findAvailablePort();
+ const app = registerAppForCleanup(await bootstrapNodeApplication(AppModule, {
+ cors: false,
+ maxBodySize: 10,
+ port,
+ }));
+
+ await app.listen();
+
+ const form = new FormData();
+ form.set('name', 'Ada Lovelace');
+ form.set('payload', new Blob(['hello'], { type: 'text/plain' }), 'payload.txt');
+
+ const response = await fetchForTest(`http://127.0.0.1:${String(port)}/uploads`, {
+ body: form,
+ headers: { 'x-request-id': 'req-multipart-fallback' },
+ method: 'POST',
+ });
+
+ expect(response.status).toBe(413);
+ await expect(response.json()).resolves.toEqual({
+ error: {
+ code: 'PAYLOAD_TOO_LARGE',
+ details: undefined,
+ message: 'Multipart body exceeds the maximum size of 10 bytes.',
+ meta: undefined,
+ requestId: 'req-multipart-fallback',
+ status: 413,
+ },
+ });
+
+ await app.close();
+ });
+
it('serves text and HTML bodies over the Node adapter without JSON quoting', async () => {
const docsHtml = '
Docs';
const metricsBody = 'process_cpu_seconds_total 1';
@@ -1284,7 +1335,6 @@ describe('bootstrapApplication', () => {
});
const originalExitCode = process.exitCode;
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number | string | null) => undefined as never) as typeof process.exit);
const port = await findAvailablePort();
const app = await runNodeApplication(AppModule, {
cors: false,
@@ -1301,7 +1351,6 @@ describe('bootstrapApplication', () => {
process.emit('SIGTERM', 'SIGTERM');
await vi.advanceTimersByTimeAsync(26);
- expect(exitSpy).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
expect(loggerEvents).toContain(
'error:FluoFactory:Shutdown timeout exceeded after 25ms; leaving process termination to the host.:none',
@@ -1309,12 +1358,70 @@ describe('bootstrapApplication', () => {
} finally {
app.close = originalClose;
await app.close();
- exitSpy.mockRestore();
process.exitCode = originalExitCode;
vi.useRealTimers();
}
});
+ it('marks signal-driven shutdown failures without terminating the host process directly', async () => {
+ const shutdownLogged = createDeferred();
+ const shutdownError = new Error('close failed');
+ const loggerEvents: string[] = [];
+ const logger: ApplicationLogger = {
+ debug() {},
+ error(message, error, context) {
+ loggerEvents.push(`error:${context}:${message}:${error instanceof Error ? error.message : 'none'}`);
+
+ if (message === 'Failed to shut down the application cleanly.') {
+ shutdownLogged.resolve();
+ }
+ },
+ log() {},
+ warn() {},
+ };
+
+ @Controller('/health')
+ class HealthController {
+ @Get('/')
+ getHealth() {
+ return { ok: true };
+ }
+ }
+
+ class AppModule {}
+ defineModule(AppModule, {
+ controllers: [HealthController],
+ });
+
+ const originalExitCode = process.exitCode;
+ const port = await findAvailablePort();
+ const app = await runNodeApplication(AppModule, {
+ cors: false,
+ logger,
+ port,
+ shutdownSignals: ['SIGTERM'],
+ });
+ const originalClose = app.close.bind(app);
+ let observedSignal: string | undefined;
+ app.close = async (signal?: string) => {
+ observedSignal = signal;
+ throw shutdownError;
+ };
+
+ try {
+ process.emit('SIGTERM', 'SIGTERM');
+ await shutdownLogged.promise;
+
+ expect(observedSignal).toBe('SIGTERM');
+ expect(process.exitCode).toBe(1);
+ expect(loggerEvents).toContain('error:FluoFactory:Failed to shut down the application cleanly.:close failed');
+ } finally {
+ app.close = originalClose;
+ await app.close();
+ process.exitCode = originalExitCode;
+ }
+ });
+
it('supports https startup and reports the https listen URL', async () => {
const loggerEvents: string[] = [];
const logger: ApplicationLogger = {
diff --git a/packages/runtime/src/node/node-request.test.ts b/packages/runtime/src/node/node-request.test.ts
index d4eaf0491..a7b651953 100644
--- a/packages/runtime/src/node/node-request.test.ts
+++ b/packages/runtime/src/node/node-request.test.ts
@@ -195,6 +195,31 @@ describe('node request adapter', () => {
]);
});
+ it('uses maxBodySize as the Node multipart total-size fallback', async () => {
+ const form = new FormData();
+ form.append('name', 'Ada Lovelace');
+ form.append('payload', new Blob(['hello'], { type: 'text/plain' }), 'payload.txt');
+
+ const multipartRequest = new Request('http://localhost/uploads', {
+ body: form,
+ method: 'POST',
+ });
+ const requestBody = Buffer.from(await multipartRequest.arrayBuffer());
+
+ const request = createIncomingMessage({
+ body: requestBody,
+ headers: {
+ 'content-type': multipartRequest.headers.get('content-type') ?? undefined,
+ },
+ method: 'POST',
+ url: '/uploads',
+ });
+
+ const result = createFrameworkRequest(request, new AbortController().signal, undefined, 10);
+
+ await expect(result).rejects.toThrow('Multipart body exceeds the maximum size of 10 bytes.');
+ });
+
it('creates the request shell before materializing body and rawBody', async () => {
let chunksRead = 0;
const request = {
diff --git a/packages/runtime/src/web.test.ts b/packages/runtime/src/web.test.ts
index 7fa38a4ba..09eaa1323 100644
--- a/packages/runtime/src/web.test.ts
+++ b/packages/runtime/src/web.test.ts
@@ -246,6 +246,38 @@ describe('dispatchWebRequest', () => {
expect(producedChunks).toBeLessThanOrEqual(3);
});
+ it('uses maxBodySize as the Web multipart total-size fallback', async () => {
+ const boundary = 'fluo-boundary';
+ const body = `--${boundary}\r\ncontent-disposition: form-data; name="name"\r\n\r\nAda Lovelace\r\n--${boundary}--\r\n`;
+
+ const response = await dispatchWebRequest({
+ dispatcher: {
+ async dispatch() {
+ throw new Error('should not dispatch oversized multipart request');
+ },
+ },
+ maxBodySize: 10,
+ request: new Request('https://runtime.test/upload', {
+ body,
+ headers: {
+ 'content-length': String(Buffer.byteLength(body)),
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
+ 'x-request-id': 'req-web-multipart-fallback',
+ },
+ method: 'POST',
+ }),
+ });
+
+ expect(response.status).toBe(413);
+ await expect(response.json()).resolves.toMatchObject({
+ error: {
+ message: 'Multipart body exceeds the maximum size of 10 bytes.',
+ requestId: 'req-web-multipart-fallback',
+ status: 413,
+ },
+ });
+ });
+
it('reuses an injected web factory without sharing request-specific state', async () => {
const factory = createWebRequestResponseFactory({ rawBody: true });
const seenBodies: unknown[] = [];