Files
deepseek-harness/packages/web/web-fetch-http/tests/proxy.spec.ts
T
Yichen Jiang 8470ddef1d refactor(http-proxy): converge the proxy API on four functions
The package exported six functions, four of them shaped by one SDK's
transport each: a dispatcher factory, a `node:http` agent factory, a
proxy-URL lookup, and a policy accessor. Review asked whether the call
sites could converge instead of the package growing an export per SDK.

They could, and each removal took a whole shape with it:

- The OTLP exporter moves to the SDK's `fetch` delegate, retiring
  `createNodeHttpAgent`. Its Node-version floor goes too: `proxyEnv` on
  an `http.Agent` needs 22.21 or 24.5, inside the engines range, so
  telemetry was direct on 22.19, 22.20, and 24.0-24.4. The cost is
  `compression`, a Node-transport option; the plugin now refuses it,
  `keepAlive`, and `httpAgentOptions` at load instead of ignoring them.
- `web-fetch-http` builds its own address-pinning agent under an
  annotated `proxy-exempt:` exemption, retiring `createDispatcher`.
  Pinning is per-request state a process-wide dispatcher cannot hold.
- E2B reads `route.proxy`, retiring `proxyUrlFor`.

What remains is `installProxyFromEnvironment`, `proxyRouteFor`,
`proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller
can need the policy. Installation absorbs resolution and diagnostic
reporting, which no caller needed apart.

`proxyRouteFor` also closes a defect the old accessor made expressible:
`web-fetch-http` read the policy to decide whether to pin, then read it
again to build a transport, so an unmount between the two returned a
direct, unpinned agent for a URL the first read had cleared as proxied.
A route carries the answer and the transport that answer assumed.

Every egress spec now installs through `installProxyFromEnvironment`, so
no test asserts a policy object a real launch could not produce.
2026-09-01 21:15:04 +08:00

160 lines
6.5 KiB
TypeScript

import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
import type { AddressInfo } from 'node:net'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
import { HttpFetchProvider } from '@deepseek-ai/dsh-web-fetch-http'
import type { HttpFetchLimits } from '@deepseek-ai/dsh-web-fetch-http'
import { isNonPublicIpLiteral, publicHttpNetwork } from '../src/network.ts'
const limits: HttpFetchLimits = {
maxResponseBytes: 5_000_000,
maxBodyChars: 100_000,
timeoutMs: 5_000,
maxRedirects: 5,
userAgent: 'test-agent/1.0',
}
/** Absolute-form targets the fake proxy saw; a populated entry proves the hop was tunnelled. */
let proxied: string[]
let proxy: Server
let origin: Server
let proxyUrl: string
let originUrl: string
/**
* The target for every assertion about a tunnelled hop. Loopback cannot serve: no policy routes
* this machine through a proxy. The host never resolves — the proxy answers the absolute-form
* request — which is also what makes the skipped resolver observable.
*/
const proxyTarget = 'http://origin.test/page'
let disposeProxy: (() => Promise<void>) | undefined
function listen(server: Server): Promise<AddressInfo> {
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => { resolve(server.address() as AddressInfo) })
})
}
function respond(_request: IncomingMessage, response: ServerResponse, body: string): void {
response.writeHead(200, { 'content-type': 'text/plain' })
response.end(body)
}
beforeEach(async () => {
proxied = []
proxy = createServer((request, response) => {
proxied.push(request.url ?? '')
respond(request, response, 'via-proxy')
})
origin = createServer((request, response) => { respond(request, response, 'direct') })
const [proxyAddress, originAddress] = await Promise.all([listen(proxy), listen(origin)])
proxyUrl = `http://127.0.0.1:${String(proxyAddress.port)}`
originUrl = `http://127.0.0.1:${String(originAddress.port)}/page`
})
afterEach(async () => {
await disposeProxy?.()
disposeProxy = undefined
vi.restoreAllMocks()
await Promise.all([
new Promise<void>((resolve) => { proxy.close(() => { resolve() }) }),
new Promise<void>((resolve) => { origin.close(() => { resolve() }) }),
])
})
/**
* Install the policy of a user who exported one proxy for both schemes; the fixture disposes it
* after every case.
*/
async function installProxy(): Promise<() => Promise<void>> {
const env = { get: (name: string) => (name === 'HTTP_PROXY' || name === 'HTTPS_PROXY' ? { value: proxyUrl } : undefined) }
return await installProxyFromEnvironment(env, () => undefined)
}
describe('fetching through a proxy', () => {
it('tunnels the request and never resolves a public address for it', async () => {
const resolve = vi.spyOn(publicHttpNetwork, 'resolve')
disposeProxy = await installProxy()
const result = await new HttpFetchProvider(limits).fetch({ url: proxyTarget })
expect(result.body.content).toBe('via-proxy')
expect(proxied).toEqual([proxyTarget])
// Through a proxy the origin's DNS happens proxy-side, so the resolver that rejects non-public
// destinations is not consulted at all.
expect(resolve).not.toHaveBeenCalled()
})
it('keeps resolving and pinning a hop the policy does not proxy', async () => {
const resolve = vi.spyOn(publicHttpNetwork, 'resolve')
.mockResolvedValue([{ address: '127.0.0.1', family: 4 }])
// No bypass entry needed: a resolved policy never routes loopback through a proxy, which is
// exactly the case this asserts still resolves and pins.
disposeProxy = await installProxy()
const result = await new HttpFetchProvider(limits).fetch({ url: originUrl })
expect(result.body.content).toBe('direct')
expect(proxied).toEqual([])
expect(resolve).toHaveBeenCalledOnce()
})
it('resolves and pins when no proxy is installed', async () => {
const resolve = vi.spyOn(publicHttpNetwork, 'resolve')
.mockResolvedValue([{ address: '127.0.0.1', family: 4 }])
const result = await new HttpFetchProvider(limits).fetch({ url: originUrl })
expect(result.body.content).toBe('direct')
expect(resolve).toHaveBeenCalledOnce()
})
it.each(['10.0.0.5', '169.254.169.254', '127.0.0.2'])(
'refuses %s instead of letting the proxy reach it for us',
async (host) => {
const resolve = vi.spyOn(publicHttpNetwork, 'resolve')
disposeProxy = await installProxy()
// The proxied path exists because a proxy resolves the origin; a literal needs no resolution,
// so taking it would spend the address checks for nothing and hand a proxy on this machine
// the private or loopback destination those checks exist to refuse. The hop therefore takes
// the validated path instead, where the existing refusal already covers it.
await expect(new HttpFetchProvider(limits).fetch({ url: `http://${host}:8080/` }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_BLOCKED_URL' }))
expect(proxied).toEqual([])
expect(resolve).toHaveBeenCalledOnce()
},
)
it('reads an IPv4-mapped literal as non-public without asking the network', () => {
// Driven through the predicate rather than a fetch: an IPv6 literal sends `resolvePublicAddresses`
// looking for a NAT64 prefix before it refuses anything, and that is a real DNS query. The three
// IPv4 cases above already prove the branch end to end without one.
expect(isNonPublicIpLiteral('[::ffff:7f00:1]')).toBe(true)
expect(isNonPublicIpLiteral('[::1]')).toBe(true)
expect(isNonPublicIpLiteral('[::ffff:808:808]')).toBe(false)
expect(isNonPublicIpLiteral('example.com')).toBe(false)
})
it('still refuses a cross-origin redirect on the proxied path', async () => {
proxy.removeAllListeners('request')
proxy.on('request', (request, response) => {
proxied.push(request.url ?? '')
response.writeHead(302, { location: 'http://elsewhere.example/next' })
response.end()
})
disposeProxy = await installProxy()
await expect(new HttpFetchProvider(limits).fetch({ url: proxyTarget }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_REDIRECT_BLOCKED' }))
})
it('still refuses a URL the transport policy rejects before any hop', async () => {
disposeProxy = await installProxy()
await expect(new HttpFetchProvider(limits).fetch({ url: 'ftp://example.com/x' }))
.rejects.toThrow(expect.objectContaining({ code: 'WEB_INVALID_URL' }))
expect(proxied).toEqual([])
})
})