82 lines
3.1 KiB
JavaScript
82 lines
3.1 KiB
JavaScript
import { request } from 'node:http'
|
|
|
|
const MAX_RESPONSE_BYTES = 256 * 1024
|
|
const UNAVAILABLE_CODES = new Set(['ENOENT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ABORT_ERR'])
|
|
|
|
export class OutboundProxyControlError extends Error {
|
|
constructor(error, statusCode, path) {
|
|
super(error?.message ?? `Outbound Proxy returned HTTP ${String(statusCode)}`)
|
|
this.name = 'OutboundProxyControlError'
|
|
this.statusCode = statusCode
|
|
this.code = error?.code ?? 'PROXY_MANAGER_UNAVAILABLE'
|
|
this.stage = error?.stage ?? 'unknown'
|
|
this.retryable = error?.retryable === true
|
|
this.localApiPath = path
|
|
this.proxyError = Object.freeze({
|
|
code: this.code,
|
|
message: this.message,
|
|
stage: this.stage,
|
|
retryable: this.retryable,
|
|
})
|
|
}
|
|
}
|
|
|
|
export class OutboundProxyControlClient {
|
|
constructor(socketPath, { timeoutMs = 10_000 } = {}) {
|
|
this.socketPath = socketPath
|
|
this.timeoutMs = timeoutMs
|
|
}
|
|
|
|
request(method, path, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const bytes = body === undefined ? undefined : Buffer.from(JSON.stringify(body))
|
|
const outgoing = request({
|
|
socketPath: this.socketPath,
|
|
method,
|
|
path,
|
|
headers: bytes === undefined ? {} : {
|
|
'content-type': 'application/json',
|
|
'content-length': bytes.byteLength,
|
|
},
|
|
signal: AbortSignal.timeout(this.timeoutMs),
|
|
}, response => {
|
|
const chunks = []
|
|
let size = 0
|
|
response.on('data', chunk => {
|
|
size += chunk.byteLength
|
|
if (size > MAX_RESPONSE_BYTES) outgoing.destroy(new Error('Outbound Proxy response is too large'))
|
|
else chunks.push(chunk)
|
|
})
|
|
response.on('end', () => {
|
|
let value
|
|
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')) } catch {
|
|
reject(new Error('Outbound Proxy response is invalid'))
|
|
return
|
|
}
|
|
const statusCode = response.statusCode ?? 500
|
|
if (statusCode >= 400) reject(new OutboundProxyControlError(value.error, statusCode, path))
|
|
else resolve(value)
|
|
})
|
|
})
|
|
outgoing.once('error', error => {
|
|
if (UNAVAILABLE_CODES.has(error?.code) || error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
reject(new OutboundProxyControlError({
|
|
code: 'PROXY_MANAGER_UNAVAILABLE',
|
|
message: 'Outbound Proxy control service is unavailable',
|
|
stage: 'control',
|
|
retryable: true,
|
|
}, 503, path))
|
|
} else reject(error)
|
|
})
|
|
outgoing.end(bytes)
|
|
})
|
|
}
|
|
|
|
configuration() { return this.request('GET', '/v1/configuration') }
|
|
updateConfiguration(value) { return this.request('PUT', '/v1/configuration', value) }
|
|
startTest(value) { return this.request('POST', '/v1/test', value) }
|
|
test(taskId) { return this.request('GET', `/v1/test/tasks/${encodeURIComponent(taskId)}`) }
|
|
cancelTest(taskId) { return this.request('DELETE', `/v1/test/tasks/${encodeURIComponent(taskId)}`) }
|
|
status() { return this.request('GET', '/v1/status') }
|
|
}
|