fix(desktop): handle proxy response resets (#1050, #1056)

This commit is contained in:
程序员阿江(Relakkes)
2026-07-17 18:33:36 +08:00
parent a6ebcfb00c
commit cdc0d0faff
2 changed files with 51 additions and 2 deletions
@@ -83,6 +83,28 @@ describe('SystemProxyBridge', () => {
.toBe('request-fallback')
})
it('terminates the downstream response when a selected proxy resets after sending headers', async () => {
const resettingProxy = net.createServer(socket => {
socket.once('data', () => {
socket.write(
'HTTP/1.1 200 OK\r\nContent-Length: 100\r\nConnection: close\r\n\r\npartial',
() => setTimeout(() => socket.resetAndDestroy(), 25),
)
})
})
servers.push(resettingProxy)
const resettingPort = await listen(resettingProxy)
const bridge = await startBridge(async () => `PROXY 127.0.0.1:${resettingPort}`)
const outcome = await Promise.race([
requestOutcomeThroughProxy(bridge, 'http://foreign.example/after-reset'),
new Promise<'timed-out'>(resolve => setTimeout(() => resolve('timed-out'), 500)),
])
expect(outcome).not.toBe('timed-out')
expect(outcome).not.toBe('end')
})
it('does not replay a non-idempotent request after its selected proxy drops it', async () => {
let firstProxyRequests = 0
const droppingProxy = net.createServer(socket => {
@@ -314,6 +336,28 @@ function requestThroughProxy(
})
}
function requestOutcomeThroughProxy(
proxyUrl: string,
targetUrl: string,
): Promise<'aborted' | 'response-error' | 'request-error' | 'end'> {
const proxy = new URL(proxyUrl)
return new Promise(resolve => {
const request = http.request({
host: proxy.hostname,
port: Number(proxy.port),
path: targetUrl,
headers: { Host: new URL(targetUrl).host },
}, response => {
response.resume()
response.once('aborted', () => resolve('aborted'))
response.once('error', () => resolve('response-error'))
response.once('end', () => resolve('end'))
})
request.once('error', () => resolve('request-error'))
request.end()
})
}
function connectAndEcho(
proxyUrl: string,
authority: string,
@@ -4,6 +4,7 @@ import net from 'node:net'
import tls from 'node:tls'
import { lookup } from 'node:dns/promises'
import type { Duplex } from 'node:stream'
import { pipeline } from 'node:stream/promises'
export const SYSTEM_PROXY_BRIDGE_HOST = '127.0.0.1'
const CONNECT_TIMEOUT_MS = 10_000
@@ -163,9 +164,13 @@ export class SystemProxyBridge implements SystemProxyBridgeLike {
onSocket,
)
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.statusMessage, upstreamResponse.headers)
upstreamResponse.pipe(response)
await pipeline(upstreamResponse, response)
} catch (error) {
if (!response.headersSent) response.writeHead(502, { Connection: 'close' })
if (response.headersSent) {
response.destroy()
return
}
response.writeHead(502, { Connection: 'close' })
response.end(`System proxy bridge failed: ${error instanceof Error ? error.message : String(error)}`)
}
}