Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface HTTPEvent<_RequestT extends EventHandlerRequest = EventHandlerR

export class H3Event<
_RequestT extends EventHandlerRequest = EventHandlerRequest,
_ContextT extends H3EventContext = H3EventContext,
> implements HTTPEvent<_RequestT> {
/**
* Access to the H3 application instance.
Expand All @@ -50,14 +51,14 @@ export class H3Event<
/**
* Event context.
*/
readonly context: H3EventContext;
readonly context: _ContextT;

/**
* @internal
*/
static __is_event__ = true;

constructor(req: ServerRequest, context?: H3EventContext, app?: H3Core) {
constructor(req: ServerRequest, context?: _ContextT, app?: H3Core) {
this.context = context || req.context || new EmptyObject();
this.req = req;
this.app = app;
Expand Down
10 changes: 8 additions & 2 deletions src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class H3Core implements H3CoreType {
}

export const H3 = /* @__PURE__ */ (() => {
class H3 extends H3Core {
class H3<_ContextT extends H3EventContext = H3EventContext> extends H3Core {
"~rou3": RouterContext;

constructor(config: H3Config = {}) {
Expand All @@ -117,7 +117,7 @@ export const H3 = /* @__PURE__ */ (() => {
request(
_req: ServerRequest | URL | string,
_init?: RequestInit,
context?: H3EventContext,
context?: _ContextT,
): Response | Promise<Response> {
return this["~request"](toRequest(_req, _init), context);
}
Expand Down Expand Up @@ -204,6 +204,12 @@ export const H3 = /* @__PURE__ */ (() => {
this["~middleware"].push(normalizeMiddleware(fn as Middleware, { ...opts, route }));
return this;
}

extendContext(key: string, valOrFn: unknown): this {
return this.use(((event) => {
event.context[key] = typeof valOrFn === "function" ? valOrFn(event) : valOrFn;
}) as Middleware);
}
Comment on lines +208 to +212

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

extendContext fluent typing doesn't match runtime scoping.

The declared signature in src/types/h3.ts returns H3<_ContextT & Record<K, V>> (and the JSDoc on line 146 there says it returns "a new H3 instance"), implying that only handlers/middleware registered after the call see the extra key. The implementation, however, just calls this.use(...) and returns this, so the appended middleware runs for every route on the instance — including ones registered before extendContext. Earlier routes get the key at runtime but their event.context is typed without it, while the original H3<_ContextT> reference still being held by callers becomes unsound.

Two related concerns:

  1. The typing should either be honored by returning a new instance (or scoping the middleware to subsequently-added routes), or the JSDoc should be corrected and the return type should remain this (H3<_ContextT & Record<K, V>> cast aside) with a documented caveat.
  2. valOrFn: unknown + typeof valOrFn === "function" means a caller using the value-overload with a function value will have it invoked as a factory. Consider distinguishing by overload at the implementation level (e.g., separate methods, or an isFn flag) if that case matters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/h3.ts` around lines 208 - 212, The extendContext implementation currently
mutates the instance with this.use(...) but its signature/JSDoc promise a new
H3<_ContextT & Record<K,V>> instance and also treats any function-valued valOrFn
as a factory; fix by either (A) changing extendContext to create and return a
new H3 instance that clones current routes/middleware and registers the new
middleware so only subsequently-added handlers see the new context (update JSDoc
and preserve the H3<_ContextT & Record<K,V>> return type), or (B) keep returning
this but change the TypeScript signature and JSDoc to return this and drop the
widened context type, documenting the global effect; additionally disambiguate
valOrFn by using an overload or explicit predicate (not plain typeof) so a
user-supplied function value isn’t unintentionally invoked (refer to
extendContext, use, Middleware, H3, and event.context when making these edits).

}

// prettier-ignore
Expand Down
52 changes: 33 additions & 19 deletions src/types/h3.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { H3EventContext } from "./context.ts";
import type { HTTPHandler, EventHandler, Middleware } from "./handler.ts";
import type { HTTPHandler, EventHandler, Middleware, EventHandlerRequest } from "./handler.ts";
import type { HTTPError } from "../error.ts";
import type { MaybePromise } from "./_utils.ts";
import type { FetchHandler, ServerRequest } from "srvx";
Expand Down Expand Up @@ -60,8 +60,8 @@ export interface H3Route {

// --- H3 App ---

export type RouteOptions = {
middleware?: Middleware[];
export type RouteOptions<_ContextT extends H3EventContext = H3EventContext> = {
middleware?: Middleware<_ContextT>[];
meta?: H3RouteMeta;
};

Expand Down Expand Up @@ -114,7 +114,7 @@ export declare class H3Core {
"~addRoute"(_route: H3Route): void;
}

export declare class H3 extends H3Core {
export declare class H3<_ContextT extends H3EventContext = H3EventContext> extends H3Core {
/** @internal */
"~rou3": RouterContext;

Expand All @@ -128,22 +128,36 @@ export declare class H3 extends H3Core {
request(
request: ServerRequest | URL | string,
options?: RequestInit,
context?: H3EventContext,
context?: _ContextT,
): Response | Promise<Response>;

/**
* Register a global middleware.
*/
use(route: string, handler: Middleware | H3, opts?: MiddlewareOptions): this;
use(handler: Middleware | H3, opts?: MiddlewareOptions): this;
use(
route: string,
handler: Middleware<_ContextT> | H3<_ContextT>,
opts?: MiddlewareOptions,
): this;
use(handler: Middleware<_ContextT> | H3<_ContextT>, opts?: MiddlewareOptions): this;

/**
* Extend the event context with a key and a typed value
* @returns a new H3 instance with the extended context type
*/
extendContext<K extends string, V>(
key: K,
valFn: (event: H3Event<EventHandlerRequest, _ContextT>) => V,
): H3<_ContextT & Record<K, V>>;
extendContext<K extends string, V>(key: K, val: V): H3<_ContextT & Record<K, V>>;

/**
* Register a route handler for the specified HTTP method and route.
*/
on(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: string,
handler: HTTPHandler,
handler: HTTPHandler<_ContextT>,
opts?: RouteOptions,
Comment on lines 157 to 161

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the route options definition and the updated route-registration signatures.
sed -n '63,66p' src/types/h3.ts
sed -n '153,190p' src/types/h3.ts

Repository: h3js/h3

Length of output: 1835


🏁 Script executed:

#!/bin/bash
# Find the Middleware type definition and related context types
rg "export type Middleware" src/types/ -A 3
rg "export type HTTPHandler" src/types/ -A 3
rg "export.*H3EventContext" src/types/ -A 2

Repository: h3js/h3

Length of output: 1638


🏁 Script executed:

#!/bin/bash
# Check all usages of RouteOptions and how it's threaded through the codebase
rg "RouteOptions" src/types/ -B 2 -A 1
rg "RouteOptions" src/ --include="*.ts" | grep -v test | head -30

Repository: h3js/h3

Length of output: 1936


🏁 Script executed:

#!/bin/bash
# Check H3Route definition and implementation details
rg "export.*type H3Route" src/types/ -A 5
rg "class H3" src/ -t ts -A 10 | head -40

Repository: h3js/h3

Length of output: 1462


🏁 Script executed:

#!/bin/bash
# Look for H3Route type definition
rg "type H3Route" src/types/ -B 2 -A 5

Repository: h3js/h3

Length of output: 33


🏁 Script executed:

#!/bin/bash
# Search for H3Route definition more broadly
rg "H3Route" src/types/ -B 1 -A 3
rg "interface H3Route" src/ -t ts -A 5

Repository: h3js/h3

Length of output: 2954


🏁 Script executed:

#!/bin/bash
# Check EventHandler definition
rg "export type EventHandler" src/types/ -A 3
# Check the actual implementation of on() method
sed -n '100,250p' src/h3.ts | head -80

Repository: h3js/h3

Length of output: 3357


🏁 Script executed:

#!/bin/bash
# Look at more of the on() implementation and check all verb methods
sed -n '100,160p' src/h3.ts | cat -n
# Also check what HTTPHandler signature expects
rg "export type HTTPHandler" src/types/handler.ts -A 5

Repository: h3js/h3

Length of output: 2646


Route-level middleware loses the typed context.

on() and all verb helpers accept handler: HTTPHandler<_ContextT> for context awareness, but opts remains plain RouteOptions with middleware?: Middleware[]. Since Middleware is already generic (Middleware<_ContextT>), route-level middleware cannot see the added context keys.

Make RouteOptions generic and thread it through all routing methods:

Suggested direction
-export type RouteOptions = {
-  middleware?: Middleware[];
+export type RouteOptions<_ContextT extends H3EventContext = H3EventContext> = {
+  middleware?: Middleware<_ContextT>[];
   meta?: H3RouteMeta;
 };

Then update on(), all(), and the verb helpers (get, post, put, delete, patch, head, options, connect, trace) to accept opts?: RouteOptions<_ContextT>. Mirror this on H3Route if needed for internal route records.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/h3.ts` around lines 156 - 160, Route-level middleware loses context
because RouteOptions is non-generic and uses Middleware[]; make RouteOptions
generic (e.g., RouteOptions<_ContextT>) and change its middleware property to
Middleware<_ContextT>[] so middleware can access the route's context. Thread
this generic through all routing APIs: update on(), all(), and verb helpers
(get, post, put, delete, patch, head, options, connect, trace) to accept opts?:
RouteOptions<_ContextT>, and adjust H3Route (or internal route record types) to
use H3Route<_ContextT> where routes store opts/handler so everything preserves
the _ContextT generic from HTTPHandler<_ContextT> to middleware.

): this;

Expand All @@ -164,15 +178,15 @@ export declare class H3 extends H3Core {
/**
* Register a route handler for all HTTP methods.
*/
all(route: string, handler: HTTPHandler, opts?: RouteOptions): this;

get(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
post(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
put(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
head(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
options(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
all(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;

get(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
post(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
put(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
delete(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
patch(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
head(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
options(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
connect(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
trace(route: string, handler: HTTPHandler<_ContextT>, opts?: RouteOptions): this;
}
18 changes: 12 additions & 6 deletions src/types/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@ import type { H3Event, HTTPEvent } from "../event.ts";
import type { MaybePromise } from "./_utils.ts";
import type { H3RouteMeta } from "./h3.ts";
import type { H3Core } from "../h3.ts";
import type { H3EventContext } from "./context.ts";

export type HTTPHandler = EventHandler | FetchableObject | H3Core;
export type HTTPHandler<_ContextT extends H3EventContext = H3EventContext> =
| EventHandler<EventHandlerRequest, EventHandlerResponse, _ContextT>
| FetchableObject
| H3Core;

// --- event handler ---

export interface EventHandler<
_RequestT extends EventHandlerRequest = EventHandlerRequest,
_ResponseT extends EventHandlerResponse = EventHandlerResponse,
_ContextT extends H3EventContext = H3EventContext,
> {
(event: H3Event<_RequestT>): _ResponseT;
(event: H3Event<_RequestT, _ContextT>): _ResponseT;
meta?: H3RouteMeta;
}

export interface EventHandlerObject<
_RequestT extends EventHandlerRequest = EventHandlerRequest,
_ResponseT extends EventHandlerResponse = EventHandlerResponse,
_ContextT extends H3EventContext = H3EventContext,
> {
handler?: EventHandler<_RequestT, _ResponseT>;
handler?: EventHandler<_RequestT, _ResponseT, _ContextT>;
fetch?: FetchHandler;
middleware?: Middleware[];
middleware?: Middleware<_ContextT>[];
meta?: H3RouteMeta;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down Expand Up @@ -62,8 +68,8 @@ export type EventHandlerFetch<T extends Response | TypedResponse = Response> = (

// --- middleware ---

export type Middleware = (
event: H3Event,
export type Middleware<_ContextT extends H3EventContext = H3EventContext> = (
event: H3Event<EventHandlerRequest, _ContextT>,
next: () => MaybePromise<unknown | undefined>,
) => MaybePromise<unknown | undefined>;

Expand Down
Loading