import http from 'node:http' import https from 'node:https' const REQUEST_TIMEOUT_MS = 60_000 const MAX_REDIRECTS = 5 const USER_AGENT = 'warp-engine-client' /** A response that arrived but said no. The status matters: 404 is not a failure everywhere. */ export class HttpStatusError extends Error { public constructor (public readonly url: string, public readonly statusCode: number) { super(`${url} answered ${String(statusCode)}`) this.name = 'HttpStatusError' } } /** * GET a URL as text, following redirects. * * Node's own client rather than `fetch`, because this runs in the main process * where the proxy and certificate settings are the system's, and because a moved * repository answers 301. */ export class HttpTextClient { public async readText (url: string, redirectsLeft: number = MAX_REDIRECTS): Promise { return new Promise((resolve: (body: string) => void, reject: (error: Error) => void): void => { const client = url.startsWith('http://') ? http : https const request = client.get(url, { headers: { 'User-Agent': USER_AGENT } }, (response): void => { const status = response.statusCode ?? 0 const location = response.headers.location if (status >= 300 && status < 400 && location !== undefined) { response.resume() if (redirectsLeft <= 0) { reject(new Error(`too many redirects for ${url}`)) return } const next = new URL(location, url).toString() this.readText(next, redirectsLeft - 1).then(resolve, reject) return } if (status !== 200) { response.resume() reject(new HttpStatusError(url, status)) return } let body = '' response.setEncoding('utf8') response.on('data', (chunk: string): void => { body += chunk }) response.on('end', (): void => { resolve(body) }) }) request.setTimeout(REQUEST_TIMEOUT_MS, (): void => { request.destroy(new Error(`${url} timed out`)) }) request.on('error', reject) }) } }