import { type Got, HTTPError, RequestError } from "got"; import { IAdvancedMatcher } from "@/service/network/AdvancedMatcher.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { HttpStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { Agent as HttpsAgent } from "https"; import { Agent as HttpAgent } from "http"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; import { NETWORK_ERROR, isStatusUp } from "@/service/network/utils.js"; import CacheableLookup from "cacheable-lookup"; export class HttpProvider implements IStatusProvider { readonly type = "http"; // Shared, pooled agents reused across every check private readonly httpAgent: HttpAgent; private readonly httpsAgent: HttpsAgent; private readonly httpsAgentInsecure: HttpsAgent; constructor( private got: Got, private advancedMatcher: IAdvancedMatcher ) { const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 }); this.got = got.extend({ dnsCache: cacheable, timeout: { request: 30000, }, retry: { limit: 1 }, }); const agentOptions = { keepAlive: true, maxSockets: 256, maxFreeSockets: 256 }; this.httpAgent = new HttpAgent(agentOptions); this.httpsAgent = new HttpsAgent({ ...agentOptions, rejectUnauthorized: true }); this.httpsAgentInsecure = new HttpsAgent({ ...agentOptions, rejectUnauthorized: false }); } supports(type: MonitorType) { return type === "http"; } private buildResponse( monitor: Monitor, opts: { body: string; contentType: string; statusCode: number; statusUp: boolean; message: string; responseTime: number; timings?: MonitorStatusResponse["timings"]; } ): MonitorStatusResponse { const { body, contentType, statusCode, statusUp, message, responseTime, timings } = opts; // Return early for HEAD requests, no body to parse if (monitor.method === "HEAD") { return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, status: statusUp, code: statusCode, message, responseTime, timings, payload: body as T | string, }; } const isJson = contentType.includes("application/json"); if (monitor.jsonPath && !isJson) { return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, status: false, code: statusCode, message: "Response is not JSON", responseTime, timings, payload: body, }; } let payload: T | string; if (isJson) { try { payload = JSON.parse(body) as T; } catch { payload = body; } } else { payload = body; } const matchResult = this.advancedMatcher.validate(payload, monitor); return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, status: statusUp && matchResult.ok, code: statusCode, message: matchResult.ok ? message : matchResult.message, responseTime, timings, payload, extracted: matchResult.extracted, }; } private handleHttpError(error: unknown, monitor: Monitor): MonitorStatusResponse { if (error instanceof HTTPError || error instanceof RequestError) { const statusCode = error.response?.statusCode; const statusUp = isStatusUp(statusCode, monitor.customUpCodes); const responseTime = error.timings?.phases?.firstByte ?? error.timings?.phases?.total ?? 0; if (!statusUp) { return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, status: false, code: statusCode ?? NETWORK_ERROR, message: error.message, responseTime, timings: error.timings, payload: null as T, }; } return this.buildResponse(monitor, { body: (error.response?.body ?? "") as string, contentType: error.response?.headers?.["content-type"] || "", statusCode: statusCode ?? NETWORK_ERROR, statusUp, message: error.message, responseTime, timings: error.timings, }); } return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, status: false, code: NETWORK_ERROR, message: error instanceof Error ? error.message : String(error), responseTime: 0, payload: null as T, }; } async handle(monitor: Monitor): Promise> { const { url, secret, ignoreTlsErrors } = monitor; if (!url) { throw new Error("URL is required for HTTP monitor"); } const options: Record = { headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined, }; options.agent = { http: this.httpAgent, https: ignoreTlsErrors ? this.httpsAgentInsecure : this.httpsAgent, }; options.method = monitor.method; try { const response = await this.got(url, options); const statusUp = isStatusUp(response.statusCode, monitor.customUpCodes); return this.buildResponse(monitor, { body: response.body, contentType: response.headers["content-type"] || "", statusCode: response.statusCode, statusUp, message: response.statusMessage ?? "OK", responseTime: response.timings.phases.firstByte ?? 0, timings: response.timings, }); } catch (error: unknown) { return this.handleHttpError(error, monitor); } } }