Skip to content

Commit 7824061

Browse files
fix(core): enforce SpecT result type on setRequestHandler (no loose-overload fallthrough)
The SpecT-typed overload's P constraint (StandardSchemaV1<SpecParams>) was too tight to ever match real schemas, so calls always fell through to the loose (method: string, schema, h: => Result) overload, silently accepting any return type. Loosen overload 1's P to bare StandardSchemaV1 (params type still inferred from the schema; only the result type is SpecT-constrained) and never-guard the loose overload's method param against SpecRequests<SpecT> so spec-method calls that don't satisfy the typed overload error instead of falling through. Same guard on setNotificationHandler for symmetry. JSDoc updated; type tests added.
1 parent c0baba4 commit 7824061

2 files changed

Lines changed: 22 additions & 11 deletions

File tree

packages/core/src/shared/protocol.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,11 +1095,11 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
10951095
* no-params methods use `z.object({})`. `paramsSchema` may be any Standard Schema (Zod,
10961096
* Valibot, ArkType, etc.). The handler's `params` type is inferred from the passed
10971097
* `paramsSchema`; when `method` is listed in this instance's {@linkcode ProtocolSpec},
1098-
* `paramsSchema`'s input and the handler's result type are constrained by `SpecT`.
1098+
* the handler's result type is constrained to `SpecT`'s declared result.
10991099
* - **Zod schema** — `setRequestHandler(RequestZodSchema, (request, ctx) => …)`. The method
11001100
* name is read from the schema's `method` literal; the handler receives the parsed request.
11011101
*/
1102-
setRequestHandler<K extends SpecRequests<SpecT>, P extends StandardSchemaV1<_Requests<SpecT>[K]['params']>>(
1102+
setRequestHandler<K extends SpecRequests<SpecT>, P extends StandardSchemaV1>(
11031103
method: K,
11041104
paramsSchema: P,
11051105
handler: (
@@ -1111,8 +1111,8 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
11111111
method: M,
11121112
handler: (request: RequestTypeMap[M], ctx: ContextT) => Result | Promise<Result>
11131113
): void;
1114-
setRequestHandler<P extends StandardSchemaV1>(
1115-
method: string,
1114+
setRequestHandler<M extends string, P extends StandardSchemaV1>(
1115+
method: M extends SpecRequests<SpecT> ? never : M,
11161116
paramsSchema: P,
11171117
handler: (params: StandardSchemaV1.InferOutput<P>, ctx: ContextT) => Result | Promise<Result>
11181118
): void;
@@ -1237,12 +1237,10 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
12371237
*
12381238
* Mirrors {@linkcode setRequestHandler}: a two-arg spec-method form (handler receives the full
12391239
* notification object), a three-arg form with a `paramsSchema` (handler receives validated
1240-
* `params`), and a Zod-schema form (method read from the schema's `method` literal). When the
1241-
* three-arg form's `method` is listed in this instance's {@linkcode ProtocolSpec},
1242-
* `paramsSchema`'s input is constrained by `SpecT`; the handler's `params` type is always
1243-
* inferred from the passed schema.
1240+
* `params`), and a Zod-schema form (method read from the schema's `method` literal). The
1241+
* handler's `params` type is always inferred from the passed schema.
12441242
*/
1245-
setNotificationHandler<K extends SpecNotifications<SpecT>, P extends StandardSchemaV1<_Notifications<SpecT>[K]['params']>>(
1243+
setNotificationHandler<K extends SpecNotifications<SpecT>, P extends StandardSchemaV1>(
12461244
method: K,
12471245
paramsSchema: P,
12481246
handler: (params: StandardSchemaV1.InferOutput<P>) => void | Promise<void>
@@ -1251,8 +1249,8 @@ export abstract class Protocol<ContextT extends BaseContext = BaseContext, SpecT
12511249
method: M,
12521250
handler: (notification: NotificationTypeMap[M]) => void | Promise<void>
12531251
): void;
1254-
setNotificationHandler<P extends StandardSchemaV1>(
1255-
method: string,
1252+
setNotificationHandler<M extends string, P extends StandardSchemaV1>(
1253+
method: M extends SpecNotifications<SpecT> ? never : M,
12561254
paramsSchema: P,
12571255
handler: (params: StandardSchemaV1.InferOutput<P>) => void | Promise<void>
12581256
): void;

packages/core/test/shared/protocolSpec.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,17 @@ describe('ProtocolSpec typing', () => {
6565
return { ok: true };
6666
});
6767
});
68+
69+
it('typed-SpecT setRequestHandler enforces result type (no fallthrough to loose string overload)', () => {
70+
const p = new TestProtocol<AppSpec>();
71+
// @ts-expect-error -- result must be { opened: boolean }; string overload is `never`-guarded for spec methods
72+
p.setRequestHandler('ui/open-link', z.object({ url: z.string() }), () => ({ ok: 'wrong-type' }));
73+
// @ts-expect-error -- empty object doesn't satisfy { opened: boolean }
74+
p.setRequestHandler('ui/open-link', z.object({ url: z.string() }), () => ({}));
75+
// non-spec methods still allow loose Result
76+
p.setRequestHandler('not/in-spec', z.object({}), () => ({ anything: 1 }));
77+
// notifications: spec and non-spec both allow any schema and return void
78+
p.setNotificationHandler('ui/size-changed', z.object({ width: z.number(), height: z.number() }), () => {});
79+
p.setNotificationHandler('not/in-spec', z.object({ x: z.number() }), () => {});
80+
});
6881
});

0 commit comments

Comments
 (0)