Skip to content

Commit 368d666

Browse files
committed
Merge branch 'main' into ihrpr/streamable-http-client
2 parents 1d18a57 + d205ad2 commit 368d666

File tree

2 files changed

+120
-1
lines changed

2 files changed

+120
-1
lines changed

src/server/sse.test.ts

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import http from 'http';
2+
import { jest } from '@jest/globals';
3+
import { SSEServerTransport } from './sse.js';
4+
5+
const createMockResponse = () => {
6+
const res = {
7+
writeHead: jest.fn<http.ServerResponse['writeHead']>(),
8+
write: jest.fn<http.ServerResponse['write']>().mockReturnValue(true),
9+
on: jest.fn<http.ServerResponse['on']>(),
10+
};
11+
res.writeHead.mockReturnThis();
12+
res.on.mockReturnThis();
13+
14+
return res as unknown as http.ServerResponse;
15+
};
16+
17+
describe('SSEServerTransport', () => {
18+
describe('start method', () => {
19+
it('should correctly append sessionId to a simple relative endpoint', async () => {
20+
const mockRes = createMockResponse();
21+
const endpoint = '/messages';
22+
const transport = new SSEServerTransport(endpoint, mockRes);
23+
const expectedSessionId = transport.sessionId;
24+
25+
await transport.start();
26+
27+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
28+
expect(mockRes.write).toHaveBeenCalledTimes(1);
29+
expect(mockRes.write).toHaveBeenCalledWith(
30+
`event: endpoint\ndata: /messages?sessionId=${expectedSessionId}\n\n`
31+
);
32+
});
33+
34+
it('should correctly append sessionId to an endpoint with existing query parameters', async () => {
35+
const mockRes = createMockResponse();
36+
const endpoint = '/messages?foo=bar&baz=qux';
37+
const transport = new SSEServerTransport(endpoint, mockRes);
38+
const expectedSessionId = transport.sessionId;
39+
40+
await transport.start();
41+
42+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
43+
expect(mockRes.write).toHaveBeenCalledTimes(1);
44+
expect(mockRes.write).toHaveBeenCalledWith(
45+
`event: endpoint\ndata: /messages?foo=bar&baz=qux&sessionId=${expectedSessionId}\n\n`
46+
);
47+
});
48+
49+
it('should correctly append sessionId to an endpoint with a hash fragment', async () => {
50+
const mockRes = createMockResponse();
51+
const endpoint = '/messages#section1';
52+
const transport = new SSEServerTransport(endpoint, mockRes);
53+
const expectedSessionId = transport.sessionId;
54+
55+
await transport.start();
56+
57+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
58+
expect(mockRes.write).toHaveBeenCalledTimes(1);
59+
expect(mockRes.write).toHaveBeenCalledWith(
60+
`event: endpoint\ndata: /messages?sessionId=${expectedSessionId}#section1\n\n`
61+
);
62+
});
63+
64+
it('should correctly append sessionId to an endpoint with query parameters and a hash fragment', async () => {
65+
const mockRes = createMockResponse();
66+
const endpoint = '/messages?key=value#section2';
67+
const transport = new SSEServerTransport(endpoint, mockRes);
68+
const expectedSessionId = transport.sessionId;
69+
70+
await transport.start();
71+
72+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
73+
expect(mockRes.write).toHaveBeenCalledTimes(1);
74+
expect(mockRes.write).toHaveBeenCalledWith(
75+
`event: endpoint\ndata: /messages?key=value&sessionId=${expectedSessionId}#section2\n\n`
76+
);
77+
});
78+
79+
it('should correctly handle the root path endpoint "/"', async () => {
80+
const mockRes = createMockResponse();
81+
const endpoint = '/';
82+
const transport = new SSEServerTransport(endpoint, mockRes);
83+
const expectedSessionId = transport.sessionId;
84+
85+
await transport.start();
86+
87+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
88+
expect(mockRes.write).toHaveBeenCalledTimes(1);
89+
expect(mockRes.write).toHaveBeenCalledWith(
90+
`event: endpoint\ndata: /?sessionId=${expectedSessionId}\n\n`
91+
);
92+
});
93+
94+
it('should correctly handle an empty string endpoint ""', async () => {
95+
const mockRes = createMockResponse();
96+
const endpoint = '';
97+
const transport = new SSEServerTransport(endpoint, mockRes);
98+
const expectedSessionId = transport.sessionId;
99+
100+
await transport.start();
101+
102+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
103+
expect(mockRes.write).toHaveBeenCalledTimes(1);
104+
expect(mockRes.write).toHaveBeenCalledWith(
105+
`event: endpoint\ndata: /?sessionId=${expectedSessionId}\n\n`
106+
);
107+
});
108+
});
109+
});

src/server/sse.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Transport } from "../shared/transport.js";
44
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
55
import getRawBody from "raw-body";
66
import contentType from "content-type";
7+
import { URL } from 'url';
78

89
const MAXIMUM_MESSAGE_SIZE = "4mb";
910

@@ -49,8 +50,17 @@ export class SSEServerTransport implements Transport {
4950
});
5051

5152
// Send the endpoint event
53+
// Use a dummy base URL because this._endpoint is relative.
54+
// This allows using URL/URLSearchParams for robust parameter handling.
55+
const dummyBase = 'http://localhost'; // Any valid base works
56+
const endpointUrl = new URL(this._endpoint, dummyBase);
57+
endpointUrl.searchParams.set('sessionId', this._sessionId);
58+
59+
// Reconstruct the relative URL string (pathname + search + hash)
60+
const relativeUrlWithSession = endpointUrl.pathname + endpointUrl.search + endpointUrl.hash;
61+
5262
this.res.write(
53-
`event: endpoint\ndata: ${encodeURI(this._endpoint)}?sessionId=${this._sessionId}\n\n`,
63+
`event: endpoint\ndata: ${relativeUrlWithSession}\n\n`,
5464
);
5565

5666
this._sseResponse = this.res;

0 commit comments

Comments
 (0)