Skip to content

Commit 9df9ede

Browse files
roomoteedelauna
authored andcommitted
fix: declare utf-8 for oauth callback html
1 parent 4b35ab1 commit 9df9ede

2 files changed

Lines changed: 49 additions & 34 deletions

File tree

src/services/mcp/utils/__tests__/callbackServer.spec.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,38 @@ vi.mock("http", () => ({
66
createServer: vi.fn(),
77
}))
88

9+
function createMockServer() {
10+
const mockServer = {
11+
listen: vi.fn((port, host, callback) => {
12+
callback()
13+
return mockServer
14+
}),
15+
address: vi.fn(() => ({ port: 3000 })),
16+
on: vi.fn(),
17+
close: vi.fn(),
18+
}
19+
20+
return mockServer
21+
}
22+
23+
function createMockResponse() {
24+
return {
25+
writeHead: vi.fn(),
26+
end: vi.fn(),
27+
on: vi.fn((event, cb) => {
28+
if (event === "finish") setImmediate(cb)
29+
}),
30+
}
31+
}
32+
933
describe("startCallbackServer", () => {
1034
beforeEach(() => {
1135
vi.restoreAllMocks()
1236
delete process.env.MCP_OAUTH_TEST_MODE
1337
})
1438

1539
it("should start server and resolve with callback result", async () => {
16-
const mockServer = {
17-
listen: vi.fn((port, host, callback) => {
18-
callback()
19-
return mockServer
20-
}),
21-
address: vi.fn(() => ({ port: 3000 })),
22-
on: vi.fn(),
23-
close: vi.fn(),
24-
}
40+
const mockServer = createMockServer()
2541

2642
;(http.createServer as any).mockReturnValue(mockServer)
2743

@@ -38,31 +54,24 @@ describe("startCallbackServer", () => {
3854
url: "/callback?code=test-code&state=test-state",
3955
method: "GET",
4056
}
41-
const mockRes = {
42-
writeHead: vi.fn(),
43-
end: vi.fn(),
44-
on: vi.fn((event, cb) => {
45-
if (event === "finish") setImmediate(cb)
46-
}),
47-
}
57+
const mockRes = createMockResponse()
4858

4959
requestHandler(mockReq, mockRes)
5060

5161
const callbackResult = await result
5262
expect(callbackResult.code).toBe("test-code")
5363
expect(callbackResult.state).toBe("test-state")
64+
expect(mockRes.writeHead).toHaveBeenCalledWith(
65+
200,
66+
expect.objectContaining({
67+
"Content-Type": "text/html; charset=utf-8",
68+
}),
69+
)
70+
expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining('<meta charset="utf-8">'))
5471
})
5572

5673
it("should reject invalid state", async () => {
57-
const mockServer = {
58-
listen: vi.fn((port, host, callback) => {
59-
callback()
60-
return mockServer
61-
}),
62-
address: vi.fn(() => ({ port: 3000 })),
63-
on: vi.fn(),
64-
close: vi.fn(),
65-
}
74+
const mockServer = createMockServer()
6675

6776
;(http.createServer as any).mockReturnValue(mockServer)
6877

@@ -76,17 +85,18 @@ describe("startCallbackServer", () => {
7685
url: "/callback?code=test-code&state=wrong-state",
7786
method: "GET",
7887
}
79-
const mockRes = {
80-
writeHead: vi.fn(),
81-
end: vi.fn(),
82-
on: vi.fn((event, cb) => {
83-
if (event === "finish") setImmediate(cb)
84-
}),
85-
}
88+
const mockRes = createMockResponse()
8689

8790
requestHandler(mockReq, mockRes)
8891

8992
await expect(result).rejects.toThrow("Invalid state parameter")
93+
expect(mockRes.writeHead).toHaveBeenCalledWith(
94+
400,
95+
expect.objectContaining({
96+
"Content-Type": "text/html; charset=utf-8",
97+
}),
98+
)
99+
expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining('<meta charset="utf-8">'))
90100
})
91101
})
92102

src/services/mcp/utils/callbackServer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import * as http from "http"
22
import { t } from "../../../i18n"
33
import { OAUTH_FLOW_TIMEOUT_MS } from "../constants"
44

5+
const HTML_RESPONSE_CONTENT_TYPE = "text/html; charset=utf-8"
6+
const CHARSET_META_TAG = '<meta charset="utf-8">'
7+
58
export interface CallbackResult {
69
code?: string
710
error?: string
@@ -93,13 +96,14 @@ export function startCallbackServer(
9396
// Verify state for CSRF protection
9497
if (expectedState && state !== expectedState) {
9598
res.writeHead(400, {
96-
"Content-Type": "text/html",
99+
"Content-Type": HTML_RESPONSE_CONTENT_TYPE,
97100
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
98101
})
99102
res.end(`
100103
<!DOCTYPE html>
101104
<html>
102105
<head>
106+
${CHARSET_META_TAG}
103107
<title>${t("mcp:oauth.callback.title")}</title>
104108
</head>
105109
<body>
@@ -115,14 +119,15 @@ export function startCallbackServer(
115119

116120
// Send HTML response
117121
res.writeHead(200, {
118-
"Content-Type": "text/html",
122+
"Content-Type": HTML_RESPONSE_CONTENT_TYPE,
119123
"Content-Security-Policy":
120124
"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'",
121125
})
122126
res.end(`
123127
<!DOCTYPE html>
124128
<html>
125129
<head>
130+
${CHARSET_META_TAG}
126131
<title>${t("mcp:oauth.callback.title")}</title>
127132
<meta name="viewport" content="width=device-width, initial-scale=1">
128133
<style>

0 commit comments

Comments
 (0)