test(09-01): verify x-forwarded-for over a real socket, not just presence
CI / check (push) Failing after 6m14s

The prior test drove the proxying app via Hono's in-process app.request()
harness, which has no real Node socket — so it could only assert the header
was non-empty-or-absent, not that the real client IP round-trips. Spin the
proxying app up with @hono/node-server (real loopback socket) and assert the
upstream actually receives 127.0.0.1/::1, exercising the same
remoteAddress lookup arenaProxy uses in production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-30 21:34:13 -04:00
co-authored by Claude Fable 5
parent 0511b97cb9
commit a95cadaf9e
+33 -14
View File
@@ -103,6 +103,24 @@ function buildProxyingApp() {
return app
}
// A real Node HTTP server for the proxying app itself, needed to exercise
// the actual socket.remoteAddress lookup arenaProxy uses for x-forwarded-for
// — Hono's in-process app.request() harness has no real socket to read.
async function withRealProxyingServer<T>(fn: (baseUrl: string) => Promise<T>): Promise<T> {
const app = buildProxyingApp()
let server: ServerType
const baseUrl = await new Promise<string>((resolve) => {
server = serve({ fetch: app.fetch, port: 0 }, (info) => {
resolve(`http://127.0.0.1:${(info as AddressInfo).port}`)
})
})
try {
return await fn(baseUrl)
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()))
}
}
describe('arenaProxy', () => {
const originalEnv = process.env.ARENA_UPSTREAM_URL
@@ -229,22 +247,23 @@ describe('arenaProxy', () => {
it('forwards the client address in x-forwarded-for', async () => {
process.env.ARENA_UPSTREAM_URL = upstreamUrl
const app = buildProxyingApp()
const res = await app.request('/api/echo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
// Drive the proxying app over a REAL socket (loopback) so
// c.env.incoming.socket.remoteAddress is actually populated, exercising
// the real code path instead of Hono's in-process app.request() harness.
const body = await withRealProxyingServer(async (baseUrl) => {
const res = await fetch(`${baseUrl}/api/echo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(200)
return await res.json() as { xff: string | null }
})
expect(res.status).toBe(200)
const body = await res.json() as { xff: string | null }
// app.request() drives the Hono app directly (no real Node socket), so the
// remoteAddress lookup this proxy relies on may legitimately be
// undetermined here — the behavioral contract is that the header is
// either forwarded (non-empty) or cleanly absent, never invented/garbage.
if (body.xff !== null) {
expect(body.xff.length).toBeGreaterThan(0)
}
expect(body.xff).toBeTruthy()
// Loopback connection — either IPv4 or IPv6-mapped loopback form.
expect(body.xff).toMatch(/127\.0\.0\.1|::1|::ffff:127\.0\.0\.1/)
})
it('answers 502 when the arena is unreachable', async () => {