refactor(core): enhance HttpKernel pipeline and matcher system with full context and error handling

BREAKING CHANGE: `parseQuery` utility removed; `IRouteMatcher` now includes query parsing; `RouteBuilder.middleware` and `handle` are now strictly typed per builder instance.

- Add `isHandler` and `isMiddleware` runtime type guards for validation in `HttpKernel`.
- Introduce `createEmptyContext` for constructing default context objects.
- Support custom HTTP error handlers (`404`, `500`) via `IHttpKernelConfig.httpErrorHandlers`.
- Default error handlers return meaningful HTTP status text (e.g., "Not Found").
- Replace legacy `parseQuery` logic with integrated query extraction via `createRouteMatcher`.

- Strongly type `RouteBuilder.middleware()` and `.handle()` methods without generic overrides.
- Simplify `HttpKernel.handle()` and `executePipeline()` through precise control flow and validation.
- Remove deprecated `registerRoute.ts` and `HttpKernelConfig.ts` in favor of colocated type exports.

- Add tests for integrated query parsing in `createRouteMatcher`.
- Improve error handling tests: middleware/handler validation, double `next()` call, thrown exceptions.
- Replace `assertRejects` with plain response code checks (via updated error handling).

- Removed `parseQuery.ts` and all related tests — query parsing is now built into route matching.
- `IRouteMatcher` signature changed to return `{ params, query }` instead of only `params`.
- `HttpKernelConfig` now uses `DeepPartial` and includes `httpErrorHandlers`.
- `RouteBuilder`'s generics are simplified for better DX and improved type safety.

This refactor improves clarity, test coverage, and runtime safety of the request lifecycle while reducing boilerplate and eliminating duplicated query handling logic.

Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
2025-05-07 16:38:53 +02:00
parent 9059bdda62
commit b7410b44dd
33 changed files with 915 additions and 220 deletions

View File

@@ -1,7 +1,4 @@
import {
assertEquals,
assertRejects,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
@@ -11,7 +8,7 @@ Deno.test('HttpKernel: matches static route and executes handler', async () => {
const def: IRouteDefinition = { method: 'GET', path: '/hello' };
let called = false;
kernel.route(def).handle(() => {
kernel.route(def).handle((_ctx) => {
called = true;
return Promise.resolve(new Response('OK', { status: 200 }));
});
@@ -31,7 +28,7 @@ Deno.test('HttpKernel: supports dynamic matcher', async () => {
matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null,
};
kernel.route(def).handle(() =>
kernel.route(def).handle((_ctx) =>
Promise.resolve(new Response('Dyn', { status: 200 }))
);
@@ -45,15 +42,15 @@ Deno.test('HttpKernel: calls middleware in order and passes to handler', async (
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/test' })
.middleware(async (ctx, next) => {
.middleware(async (_ctx, next) => {
calls.push('mw1');
return await next();
})
.middleware(async (ctx, next) => {
.middleware(async (_ctx, next) => {
calls.push('mw2');
return await next();
})
.handle(() => {
.handle((_ctx) => {
calls.push('handler');
return Promise.resolve(new Response('done'));
});
@@ -70,15 +67,15 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/stop' })
.middleware(() => {
.middleware((_ctx, _next) => {
calls.push('mw1');
return Promise.resolve(new Response('blocked', { status: 403 }));
})
.middleware(() => {
.middleware((_ctx, _next) => {
calls.push('mw2');
return Promise.resolve(new Response('should-not-call'));
})
.handle(() => {
.handle((_ctx) => {
calls.push('handler');
return Promise.resolve(new Response('ok'));
});
@@ -91,6 +88,32 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
assertEquals(calls, ['mw1']);
});
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => {
const kernel = new HttpKernel();
// Middleware with wrong signature (missing ctx, next)
kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware
.middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok')));
const res1 = await kernel.handle(new Request('http://localhost/bad-mw'));
assertEquals(res1.status, 500);
assertEquals(await res1.text(), 'Internal Server Error');
// Handler with wrong signature (no ctx)
kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler
.handle(() => new Response('invalid'));
const res2 = await kernel.handle(
new Request('http://localhost/bad-handler'),
);
assertEquals(res2.status, 500);
assertEquals(await res2.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: 404 for unmatched route', async () => {
const kernel = new HttpKernel();
const res = await kernel.handle(new Request('http://localhost/nothing'));
@@ -113,18 +136,16 @@ Deno.test('HttpKernel: throws on next() called twice', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/bad' })
.middleware(async (ctx, next) => {
.middleware(async (_ctx, next) => {
await next();
await next(); // ❌
return new Response('should never reach');
})
.handle(() => Promise.resolve(new Response('OK')));
.handle((_ctx) => Promise.resolve(new Response('OK')));
await assertRejects(
() => kernel.handle(new Request('http://localhost/bad')),
Error,
'next() called multiple times',
);
const res = await kernel.handle(new Request('http://localhost/bad'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: handler throws → error propagates', async () => {
@@ -135,11 +156,9 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
throw new Error('fail!');
});
await assertRejects(
() => kernel.handle(new Request('http://localhost/throw')),
Error,
'fail!',
);
const res = await kernel.handle(new Request('http://localhost/throw'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => {
@@ -157,5 +176,5 @@ Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async (
const res = await kernel.handle(new Request('http://localhost/fail'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal error');
assertEquals(await res.text(), 'Internal Server Error');
});