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.
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { findDispatcherViolations, scanRepository, DISPATCHER_OWNER } from './verify-no-bare-dispatcher.ts'
|
|
|
|
const FILE = 'packages/web/web-fetch-http/src/network.ts'
|
|
|
|
function reasons(source: string, file = FILE): string[] {
|
|
return findDispatcherViolations(file, source).map(violation => violation.what)
|
|
}
|
|
|
|
describe('bare dispatcher check', () => {
|
|
it('rejects the shape that silently bypassed the proxy before this rule existed', () => {
|
|
expect(reasons(`
|
|
import { Agent } from 'undici'
|
|
const dispatcher = new Agent({ connect: { lookup } })
|
|
const response = await fetch(url, { dispatcher })
|
|
`)).toEqual(['constructs an undici agent', 'passes an explicit \`dispatcher\`'])
|
|
})
|
|
|
|
it('rejects an explicit dispatcher option however the agent was obtained', () => {
|
|
expect(reasons(" const response = await fetch(url, { method: 'GET', dispatcher: pooled })"))
|
|
.toEqual(['passes an explicit \`dispatcher\`'])
|
|
})
|
|
|
|
it('rejects a namespaced construction', () => {
|
|
expect(reasons(`
|
|
import * as undici from 'undici'
|
|
const agent = new undici.ProxyAgent(uri)
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('reports the offending line number and text', () => {
|
|
const source = "import { Agent } from 'undici'\nconst a = 1\nconst b = new Agent({})"
|
|
expect(findDispatcherViolations(FILE, source)).toEqual([
|
|
{ file: FILE, line: 3, what: 'constructs an undici agent', text: 'const b = new Agent({})' },
|
|
])
|
|
})
|
|
|
|
it('accepts the sanctioned factory', () => {
|
|
expect(reasons(' const route = proxyRouteFor(url)')).toEqual([])
|
|
})
|
|
|
|
it('rejects the shorthand form a line-wise regex misses', () => {
|
|
expect(reasons(`
|
|
import { Agent } from 'undici'
|
|
const dispatcher = pool
|
|
const response = await fetch(url, { method: 'GET', dispatcher })
|
|
`)).toEqual(['passes an explicit \`dispatcher\`'])
|
|
})
|
|
|
|
it('rejects a quoted dispatcher key', () => {
|
|
expect(reasons(" await fetch(url, { 'dispatcher': pooled })"))
|
|
.toEqual(['passes an explicit \`dispatcher\`'])
|
|
})
|
|
|
|
it('rejects construction through an import alias', () => {
|
|
expect(reasons(`
|
|
import { Agent as CustomAgent } from 'undici'
|
|
const agent = new CustomAgent({})
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('rejects a destructured dynamic import, the form this repository loads undici with', () => {
|
|
expect(reasons(`
|
|
const { Agent } = await import('undici')
|
|
const agent = new Agent({})
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('rejects a renamed binding from a dynamic import', () => {
|
|
expect(reasons(`
|
|
const { ProxyAgent: Tunnel } = await import('undici')
|
|
const agent = new Tunnel({ uri })
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('rejects a namespace bound by a dynamic import', () => {
|
|
expect(reasons(`
|
|
const undici = await import('undici')
|
|
const agent = new undici.Agent({})
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('finds a dynamic import nested inside a function, not only at the top level', () => {
|
|
expect(reasons(`
|
|
export async function requestWith(url) {
|
|
const { Agent } = await import('undici')
|
|
return new Agent({})
|
|
}
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('accepts a dynamic import of an unrelated module that exports Agent', () => {
|
|
expect(reasons(`
|
|
const { Agent } = await import('./our-own-agent.ts')
|
|
const agent = new Agent({})
|
|
`)).toEqual([])
|
|
})
|
|
|
|
it('accepts an unrelated class that happens to be named Agent', () => {
|
|
expect(reasons(`
|
|
import { Agent } from './our-own-agent.ts'
|
|
const agent = new Agent({})
|
|
`)).toEqual([])
|
|
})
|
|
|
|
it('accepts an exemption annotated on the line above, where a long line puts it', () => {
|
|
expect(reasons(`
|
|
import { Agent } from 'undici'
|
|
// proxy-exempt: the dispatcher already applied the active policy.
|
|
const response = await fetch(url, { method: 'GET', headers, dispatcher })
|
|
`)).toEqual([])
|
|
})
|
|
|
|
it('accepts an annotated exemption', () => {
|
|
expect(reasons(`
|
|
import { Agent } from 'undici'
|
|
const agent = new Agent({}) // proxy-exempt: loopback transport for the local test server
|
|
`)).toEqual([])
|
|
})
|
|
|
|
it('exempts the package that owns dispatcher construction', () => {
|
|
expect(reasons("import { EnvHttpProxyAgent } from 'undici'\nconst agent = new EnvHttpProxyAgent()", `${DISPATCHER_OWNER}src/install.ts`)).toEqual([])
|
|
})
|
|
|
|
it('normalizes native separators before exempting the owning package', () => {
|
|
expect(reasons("import { Agent } from 'undici'\nconst agent = new Agent({})", DISPATCHER_OWNER.replaceAll('/', '\\') + 'src\\install.ts')).toEqual([])
|
|
})
|
|
|
|
it('parses only a file naming undici or the dispatcher option', () => {
|
|
// The pre-filter that keeps this gate from parsing 1576 of 1597 repository files excludes a
|
|
// file mentioning neither word. Both violations require one of them in source, so nothing
|
|
// detectable is excluded — the second case proves a violating shape survives the filter.
|
|
expect(reasons(' const agent = new Agent({ keepAlive: true })')).toEqual([])
|
|
expect(reasons(`
|
|
import { Agent } from 'undici'
|
|
const agent = new Agent({})
|
|
`)).toEqual(['constructs an undici agent'])
|
|
})
|
|
|
|
it('passes on the current tree', () => {
|
|
expect(scanRepository()).toEqual([])
|
|
})
|
|
})
|