Skip to content

Commit 577162c

Browse files
committed
update
1 parent 02e7e19 commit 577162c

File tree

3 files changed

+107
-141
lines changed

3 files changed

+107
-141
lines changed

src/utils/json-rpc.ts

Lines changed: 45 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import type { EventHandler, EventHandlerRequest, Middleware } from "../types/handler.ts";
1+
import type {
2+
EventHandler,
3+
EventHandlerObject,
4+
EventHandlerRequest,
5+
Middleware,
6+
} from "../types/handler.ts";
7+
import type { Hooks as WebSocketHooks, Peer as WebSocketPeer } from "crossws";
28
import type { H3Event } from "../event.ts";
39
import { defineHandler } from "../handler.ts";
410
import { defineWebSocketHandler } from "./ws.ts";
511
import { HTTPError } from "../error.ts";
6-
7-
import type { Hooks as WebSocketHooks, Peer as WebSocketPeer } from "crossws";
12+
import { HTTPResponse } from "../response.ts";
813

914
/**
1015
* JSON-RPC 2.0 Interfaces based on the specification.
@@ -39,16 +44,8 @@ export interface JsonRpcError {
3944
* JSON-RPC 2.0 Response object.
4045
*/
4146
export type JsonRpcResponse<O = unknown> =
42-
| {
43-
jsonrpc: "2.0";
44-
id: string | number | null;
45-
result: O;
46-
}
47-
| {
48-
jsonrpc: "2.0";
49-
id: string | number | null;
50-
error: JsonRpcError;
51-
};
47+
| { jsonrpc: "2.0"; id: string | number | null; result: O }
48+
| { jsonrpc: "2.0"; id: string | number | null; error: JsonRpcError };
5249

5350
/**
5451
* A function that handles a JSON-RPC method call.
@@ -68,25 +65,10 @@ export type JsonRpcWebSocketMethod<
6865
I extends JsonRpcParams | undefined = JsonRpcParams | undefined,
6966
> = (data: JsonRpcRequest<I>, peer: WebSocketPeer) => O | Promise<O>;
7067

71-
/**
72-
* Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
73-
*/
74-
const PARSE_ERROR = -32_700;
75-
76-
/**
77-
* The JSON sent is not a valid Request object.
78-
*/
79-
const INVALID_REQUEST = -32_600;
80-
/**
81-
* The method does not exist / is not available.
82-
*/
83-
84-
const METHOD_NOT_FOUND = -32_601;
85-
86-
/**
87-
* Invalid method parameter(s).
88-
*/
89-
const INVALID_PARAMS = -32_602;
68+
const PARSE_ERROR = -32_700; // Invalid JSON was received by the server.
69+
const INVALID_REQUEST = -32_600; // The JSON sent is not a valid Request object.
70+
const METHOD_NOT_FOUND = -32_601; // The method does not exist / is not available.
71+
const INVALID_PARAMS = -32_602; // Invalid method parameter(s).
9072

9173
/**
9274
* Creates an H3 event handler that implements the JSON-RPC 2.0 specification.
@@ -97,51 +79,37 @@ const INVALID_PARAMS = -32_602;
9779
*
9880
* @example
9981
* app.post("/rpc", defineJsonRpcHandler({
100-
* echo: ({ params }, event) => {
101-
* return `Received \`${params}\` on path \`${event.url.pathname}\``;
102-
* },
103-
* sum: ({ params }, event) => {
104-
* return params.a + params.b;
82+
* methods: {
83+
* echo: ({ params }, event) => {
84+
* return `Received \`${params}\` on path \`${event.url.pathname}\``;
85+
* },
86+
* sum: ({ params }, event) => {
87+
* return params.a + params.b;
88+
* },
10589
* },
10690
* }));
10791
*/
10892
export function defineJsonRpcHandler<RequestT extends EventHandlerRequest = EventHandlerRequest>(
109-
methods: Record<string, JsonRpcMethod>,
110-
middleware?: Middleware[],
93+
opts: Omit<EventHandlerObject<RequestT>, "handler" | "fetch"> & {
94+
methods: Record<string, JsonRpcMethod>;
95+
} = {} as any,
11196
): EventHandler<RequestT> {
112-
const methodMap = createMethodMap(methods);
113-
97+
const methodMap = createMethodMap(opts.methods);
11498
const handler = async (event: H3Event) => {
11599
// JSON-RPC requests MUST be POST.
116100
if (event.req.method !== "POST") {
117-
throw new HTTPError({
118-
status: 405,
119-
message: "Method Not Allowed",
120-
});
101+
throw new HTTPError({ status: 405 });
121102
}
122-
123103
let body: unknown;
124104
try {
125105
body = await event.req.json();
126106
} catch {
127107
return createJsonRpcError(null, PARSE_ERROR, "Parse error");
128108
}
129-
130109
const result = await processJsonRpcBody(body, methodMap, event);
131-
132-
if (result === undefined) {
133-
event.res.status = 202;
134-
return "";
135-
}
136-
137-
event.res.headers.set("Content-Type", "application/json");
138-
return result;
110+
return result === undefined ? new HTTPResponse("", { status: 202 }) : result;
139111
};
140-
141-
return defineHandler<RequestT>({
142-
handler,
143-
middleware,
144-
});
112+
return defineHandler<RequestT>({ ...opts, handler });
145113
}
146114

147115
/**
@@ -151,25 +119,27 @@ export function defineJsonRpcHandler<RequestT extends EventHandlerRequest = Even
151119
* connections for bi-directional messaging. Each incoming WebSocket text message
152120
* is processed as a JSON-RPC request, and responses are sent back to the peer.
153121
*
154-
* @param methods A map of RPC method names to their handler functions.
155-
* @param options Optional configuration including additional WebSocket hooks.
122+
* @param opts Options including methods map and optional WebSocket hooks.
156123
* @returns An H3 EventHandler that upgrades to a WebSocket connection.
157124
*
158125
* @example
159126
* app.get("/rpc/ws", defineJsonRpcWebSocketHandler({
160-
* echo: ({ params }) => {
161-
* return `Received: ${Array.isArray(params) ? params[0] : params?.message}`;
162-
* },
163-
* sum: ({ params }) => {
164-
* return params.a + params.b;
127+
* methods: {
128+
* echo: ({ params }) => {
129+
* return `Received: ${Array.isArray(params) ? params[0] : params?.message}`;
130+
* },
131+
* sum: ({ params }) => {
132+
* return params.a + params.b;
133+
* },
165134
* },
166135
* }));
167136
*
168137
* @example
169138
* // With additional WebSocket hooks
170139
* app.get("/rpc/ws", defineJsonRpcWebSocketHandler({
171-
* greet: ({ params }) => `Hello, ${params.name}!`,
172-
* }, {
140+
* methods: {
141+
* greet: ({ params }) => `Hello, ${params.name}!`,
142+
* },
173143
* hooks: {
174144
* open(peer) {
175145
* console.log(`Peer connected: ${peer.id}`);
@@ -180,19 +150,13 @@ export function defineJsonRpcHandler<RequestT extends EventHandlerRequest = Even
180150
* },
181151
* }));
182152
*/
183-
export function defineJsonRpcWebSocketHandler(
184-
methods: Record<string, JsonRpcWebSocketMethod>,
185-
options?: {
186-
hooks?: Partial<Omit<WebSocketHooks, "message">>;
187-
},
188-
): EventHandler {
189-
const methodMap = createMethodMap(methods);
190-
153+
export function defineJsonRpcWebSocketHandler(opts: {
154+
methods: Record<string, JsonRpcWebSocketMethod>;
155+
hooks?: Partial<Omit<WebSocketHooks, "message">>;
156+
}): EventHandler {
157+
const methodMap = createMethodMap(opts.methods);
191158
return defineWebSocketHandler({
192-
upgrade: options?.hooks?.upgrade,
193-
open: options?.hooks?.open,
194-
close: options?.hooks?.close,
195-
error: options?.hooks?.error,
159+
...opts.hooks,
196160
async message(peer, message) {
197161
let body: unknown;
198162
try {
@@ -201,9 +165,7 @@ export function defineJsonRpcWebSocketHandler(
201165
peer.send(JSON.stringify(createJsonRpcError(null, PARSE_ERROR, "Parse error")));
202166
return;
203167
}
204-
205168
const result = await processJsonRpcBody(body, methodMap, peer);
206-
207169
if (result !== undefined) {
208170
peer.send(JSON.stringify(result));
209171
}

test/json-rpc-ws.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("defineJsonRpcWebSocketHandler", () => {
5252

5353
describe("handler structure", () => {
5454
it("should return an EventHandler that produces a 426 response with crossws hooks", () => {
55-
const handler = defineJsonRpcWebSocketHandler(methods);
55+
const handler = defineJsonRpcWebSocketHandler({ methods });
5656
const res = handler({} as any);
5757
expect(res).toBeInstanceOf(Response);
5858
expect((res as Response).status).toBe(426);
@@ -65,7 +65,8 @@ describe("defineJsonRpcWebSocketHandler", () => {
6565
const closeFn = vi.fn();
6666
const errorFn = vi.fn();
6767

68-
const handler = defineJsonRpcWebSocketHandler(methods, {
68+
const handler = defineJsonRpcWebSocketHandler({
69+
methods,
6970
hooks: {
7071
open: openFn,
7172
close: closeFn,
@@ -81,7 +82,7 @@ describe("defineJsonRpcWebSocketHandler", () => {
8182
});
8283

8384
it("should not override message hook with user hooks", () => {
84-
const handler = defineJsonRpcWebSocketHandler(methods);
85+
const handler = defineJsonRpcWebSocketHandler({ methods });
8586
const res = handler({} as any);
8687
const hooks = (res as any).crossws;
8788
// The message hook should be the internal JSON-RPC processor.
@@ -94,7 +95,7 @@ describe("defineJsonRpcWebSocketHandler", () => {
9495
messageData: unknown,
9596
methodsMap?: Record<string, any>,
9697
): Promise<{ peer: ReturnType<typeof createMockPeer>; sent: string[] }> {
97-
const handler = defineJsonRpcWebSocketHandler(methodsMap || methods);
98+
const handler = defineJsonRpcWebSocketHandler({ methods: methodsMap || methods });
9899
const res = handler({} as any);
99100
const hooks = (res as any).crossws;
100101

@@ -331,12 +332,14 @@ describe("defineJsonRpcWebSocketHandler", () => {
331332
messageData: unknown,
332333
): Promise<{ peer: ReturnType<typeof createMockPeer>; sent: string[] }> {
333334
const handler = defineJsonRpcWebSocketHandler({
334-
echo: ({ params }: any) => {
335-
const message = Array.isArray(params) ? params[0] : params?.message;
336-
return `Received ${message}`;
337-
},
338-
sum: ({ params }: any) => {
339-
return params.a + params.b;
335+
methods: {
336+
echo: ({ params }: any) => {
337+
const message = Array.isArray(params) ? params[0] : params?.message;
338+
return `Received ${message}`;
339+
},
340+
sum: ({ params }: any) => {
341+
return params.a + params.b;
342+
},
340343
},
341344
});
342345
const res = handler({} as any);
@@ -404,7 +407,7 @@ describe("defineJsonRpcWebSocketHandler", () => {
404407
});
405408

406409
const handler = defineJsonRpcWebSocketHandler({
407-
test: methodSpy,
410+
methods: { test: methodSpy },
408411
});
409412
const res = handler({} as any);
410413
const hooks = (res as any).crossws;

test/json-rpc.test.ts

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,52 +22,54 @@ describeMatrix("json-rpc", (t, { describe, it, expect }) => {
2222
};
2323

2424
const eventHandler = defineJsonRpcHandler({
25-
echo,
26-
sum,
27-
error: () => {
28-
throw new Error("Handler error");
29-
},
30-
errorPrimitive: () => {
31-
throw "Primitive error";
32-
},
33-
// "constructor" is a valid method name — the null-prototype map
34-
// ensures it doesn't resolve to Object.prototype.constructor.
35-
constructor: () => {
36-
return "ok";
37-
},
38-
// HTTP error handlers for testing error code mapping
39-
unauthorized: () => {
40-
throw new HTTPError({ status: 401, message: "Authentication required" });
41-
},
42-
forbidden: () => {
43-
throw new HTTPError({ status: 403, message: "Access denied" });
44-
},
45-
notFound: () => {
46-
throw new HTTPError({ status: 404, message: "Resource not found" });
47-
},
48-
badRequest: () => {
49-
throw new HTTPError({ status: 400, message: "Bad request data" });
50-
},
51-
conflict: () => {
52-
throw new HTTPError({ status: 409, message: "Resource conflict" });
53-
},
54-
rateLimited: () => {
55-
throw new HTTPError({ status: 429, message: "Too many requests" });
56-
},
57-
serverError: () => {
58-
throw new HTTPError({ status: 500, message: "Server exploded" });
59-
},
60-
redirect: () => {
61-
throw new HTTPError({ status: 301, message: "Resource moved permanently" });
62-
},
63-
errorWithZeroData: () => {
64-
throw new HTTPError({ status: 400, message: "Validation failed", data: 0 });
65-
},
66-
errorWithEmptyStringData: () => {
67-
throw new HTTPError({ status: 400, message: "Validation failed", data: "" });
68-
},
69-
errorWithFalseData: () => {
70-
throw new HTTPError({ status: 400, message: "Validation failed", data: false });
25+
methods: {
26+
echo,
27+
sum,
28+
error: () => {
29+
throw new Error("Handler error");
30+
},
31+
errorPrimitive: () => {
32+
throw "Primitive error";
33+
},
34+
// "constructor" is a valid method name — the null-prototype map
35+
// ensures it doesn't resolve to Object.prototype.constructor.
36+
constructor: () => {
37+
return "ok";
38+
},
39+
// HTTP error handlers for testing error code mapping
40+
unauthorized: () => {
41+
throw new HTTPError({ status: 401, message: "Authentication required" });
42+
},
43+
forbidden: () => {
44+
throw new HTTPError({ status: 403, message: "Access denied" });
45+
},
46+
notFound: () => {
47+
throw new HTTPError({ status: 404, message: "Resource not found" });
48+
},
49+
badRequest: () => {
50+
throw new HTTPError({ status: 400, message: "Bad request data" });
51+
},
52+
conflict: () => {
53+
throw new HTTPError({ status: 409, message: "Resource conflict" });
54+
},
55+
rateLimited: () => {
56+
throw new HTTPError({ status: 429, message: "Too many requests" });
57+
},
58+
serverError: () => {
59+
throw new HTTPError({ status: 500, message: "Server exploded" });
60+
},
61+
redirect: () => {
62+
throw new HTTPError({ status: 301, message: "Resource moved permanently" });
63+
},
64+
errorWithZeroData: () => {
65+
throw new HTTPError({ status: 400, message: "Validation failed", data: 0 });
66+
},
67+
errorWithEmptyStringData: () => {
68+
throw new HTTPError({ status: 400, message: "Validation failed", data: "" });
69+
},
70+
errorWithFalseData: () => {
71+
throw new HTTPError({ status: 400, message: "Validation failed", data: false });
72+
},
7173
},
7274
});
7375

@@ -309,7 +311,6 @@ describeMatrix("json-rpc", (t, { describe, it, expect }) => {
309311
method: "GET",
310312
});
311313
expect(result.status).toBe(405);
312-
expect(await result.text()).toContain("Method Not Allowed");
313314
});
314315

315316
it("should return parse error for invalid JSON", async () => {

0 commit comments

Comments
 (0)