Files

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') }
}