Skip to content

Commit e98715d

Browse files
authored
fix(realtime): noindex the socket server's 404 responses (#6129)
* fix(realtime): noindex the socket server's 404 responses The sockets.* hostnames are served by this server and return a plain JSON 404, which Google Search Console reports as crawl errors. Mark unmatched routes noindex so crawlers drop the hostnames instead of retrying them. * fix(realtime): noindex every response, not just the 404 Setting the header only on the 404 fallback covered the one response that crawlers already drop on status code alone, while /health — the sole route returning 200 with a body, and so the only indexable surface on the socket hostnames — stayed uncovered, with a test pinning it that way. Set it once on the handler instead. Node merges setHeader values into writeHead and no branch sets X-Robots-Tag, so it reaches every response.
1 parent c0b19da commit e98715d

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { IncomingMessage, ServerResponse } from 'http'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import type { IRoomManager } from '@/rooms'
4+
import { createHttpHandler } from '@/routes/http'
5+
6+
function createMocks(req: Partial<IncomingMessage>) {
7+
const setHeader = vi.fn()
8+
const writeHead = vi.fn()
9+
const end = vi.fn()
10+
const logger = { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() }
11+
const roomManager = {
12+
getTotalActiveConnections: vi.fn().mockResolvedValue(0),
13+
isReady: vi.fn().mockReturnValue(true),
14+
} as unknown as IRoomManager
15+
16+
return {
17+
handler: createHttpHandler(roomManager, logger),
18+
req: { headers: {}, ...req } as IncomingMessage,
19+
res: { setHeader, writeHead, end } as unknown as ServerResponse,
20+
setHeader,
21+
writeHead,
22+
end,
23+
}
24+
}
25+
26+
describe('createHttpHandler', () => {
27+
/**
28+
* `/health` is the only route on this server that returns 200 with a body, so
29+
* it is the only genuinely indexable surface on the `sockets.*` hostnames.
30+
* Node merges `setHeader` values into `writeHead`, and no branch here sets
31+
* `X-Robots-Tag`, so the handler-level call reaches every response.
32+
*/
33+
it.each([
34+
['health check', { method: 'GET', url: '/health' }],
35+
['unmatched route', { method: 'GET', url: '/' }],
36+
['unauthenticated internal API call', { method: 'POST', url: '/api/workflow-deleted' }],
37+
])('marks the %s noindex', async (_label, req) => {
38+
const { handler, req: request, res, setHeader } = createMocks(req)
39+
40+
await handler(request, res)
41+
42+
expect(setHeader).toHaveBeenCalledWith('X-Robots-Tag', 'noindex, nofollow')
43+
})
44+
45+
it('still serves the unmatched-route 404 unchanged', async () => {
46+
const { handler, req, res, writeHead, end } = createMocks({ method: 'GET', url: '/' })
47+
48+
await handler(req, res)
49+
50+
expect(writeHead).toHaveBeenCalledWith(404, { 'Content-Type': 'application/json' })
51+
expect(end).toHaveBeenCalledWith(JSON.stringify({ error: 'Not found' }))
52+
})
53+
54+
it('still serves the health check as 200', async () => {
55+
const { handler, req, res, writeHead } = createMocks({ method: 'GET', url: '/health' })
56+
57+
await handler(req, res)
58+
59+
expect(writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' })
60+
})
61+
})

apps/realtime/src/routes/http.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function sendError(res: ServerResponse, message: string, status = 500): void {
5959
*/
6060
export function createHttpHandler(roomManager: IRoomManager, logger: Logger) {
6161
return async (req: IncomingMessage, res: ServerResponse) => {
62+
res.setHeader('X-Robots-Tag', 'noindex, nofollow')
63+
6264
// Health check doesn't require auth
6365
if (req.method === 'GET' && req.url === '/health') {
6466
try {

0 commit comments

Comments
 (0)