- app.ts 单体路由拆分为 routes/ + helpers + middleware + static 独立模块 - 类型去重:CheckFailure/CheckResult 以 shared/api.ts 为唯一源头,收紧 phase 联合类型 - es-toolkit 替换:isPlainObject/isNil/isEmptyObject/isEqual/isError/Semaphore/groupBy - Bun 内置 API:Object.fromEntries 替代手写 headersToRecord - bun:sqlite 规范:prepare() → query() 利用内置缓存,避免 N+1 查询 - 新增 getLatestChecksMap/allGetTargetStats 批量查询方法 - 新增 backend-code-quality/api-route-separation/batch-data-queries 规范 - 补充 openspec/config.yaml 后端开发规范与 DEVELOPMENT.md 后端开发指引
94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
import type { CheckResult, ResolvedHttpTarget } from "./types";
|
|
import { checkHttpExpect } from "./expect/http";
|
|
import { errorFailure } from "./expect/failure";
|
|
import { isError } from "es-toolkit";
|
|
|
|
export async function runHttpCheck(target: ResolvedHttpTarget): Promise<CheckResult> {
|
|
const timestamp = new Date().toISOString();
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), target.timeoutMs);
|
|
|
|
try {
|
|
const start = performance.now();
|
|
|
|
const response = await fetch(target.http.url, {
|
|
method: target.http.method,
|
|
headers: target.http.headers,
|
|
body: target.http.method !== "GET" && target.http.method !== "HEAD" ? target.http.body : undefined,
|
|
signal: controller.signal,
|
|
});
|
|
|
|
const durationMs = Math.round(performance.now() - start);
|
|
const statusCode = response.status;
|
|
const responseHeaders = Object.fromEntries(response.headers);
|
|
|
|
const hasBodyRules = !!(target.expect?.body && target.expect.body.length > 0);
|
|
|
|
const preBodyExpect = target.expect
|
|
? { status: target.expect.status, maxDurationMs: target.expect.maxDurationMs, headers: target.expect.headers }
|
|
: undefined;
|
|
|
|
const preBodyObs = { statusCode, headers: responseHeaders, body: null as string | null, durationMs };
|
|
const preBodyResult = checkHttpExpect(preBodyObs, preBodyExpect);
|
|
|
|
if (!hasBodyRules || !preBodyResult.matched) {
|
|
clearTimeout(timeoutId);
|
|
return {
|
|
targetName: target.name,
|
|
timestamp,
|
|
matched: preBodyResult.matched,
|
|
durationMs,
|
|
statusDetail: `HTTP ${statusCode}`,
|
|
failure: preBodyResult.failure,
|
|
};
|
|
}
|
|
|
|
const bodyBuffer = await response.arrayBuffer();
|
|
clearTimeout(timeoutId);
|
|
|
|
if (bodyBuffer.byteLength > target.http.maxBodyBytes) {
|
|
return {
|
|
targetName: target.name,
|
|
timestamp,
|
|
matched: false,
|
|
durationMs,
|
|
statusDetail: `HTTP ${statusCode}`,
|
|
failure: errorFailure(
|
|
"body",
|
|
"body",
|
|
`响应体大小 ${bodyBuffer.byteLength} 超过限制 ${target.http.maxBodyBytes}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
const body = new TextDecoder().decode(bodyBuffer);
|
|
const fullObs = { statusCode, headers: responseHeaders, body, durationMs };
|
|
const fullResult = checkHttpExpect(fullObs, target.expect);
|
|
|
|
return {
|
|
targetName: target.name,
|
|
timestamp,
|
|
matched: fullResult.matched,
|
|
durationMs,
|
|
statusDetail: `HTTP ${statusCode}`,
|
|
failure: fullResult.failure,
|
|
};
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
const isTimeout = error instanceof DOMException && error.name === "AbortError";
|
|
|
|
return {
|
|
targetName: target.name,
|
|
timestamp,
|
|
matched: false,
|
|
durationMs: null,
|
|
statusDetail: null,
|
|
failure: errorFailure(
|
|
"status",
|
|
"request",
|
|
isTimeout ? `请求超时 (${target.timeoutMs}ms)` : isError(error) ? error.message : String(error),
|
|
),
|
|
};
|
|
}
|
|
}
|