harden(authority): add pre-scale diagnostics and request safety

This commit is contained in:
Youzini-afk
2026-04-28 22:28:21 +08:00
parent aa62efe5b9
commit 2dee3cd8ff
9 changed files with 644 additions and 49 deletions

View File

@@ -31,6 +31,15 @@ function normalizeHeaderName(name = "") {
return String(name || "").trim().toLowerCase();
}
function normalizeTimeoutMs(value, fallbackValue = 0) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
const fallback = Number(fallbackValue);
return Number.isFinite(fallback) && fallback > 0 ? Math.floor(fallback) : 0;
}
return Math.floor(parsed);
}
function hasSessionHeader(headers = {}) {
return Object.keys(headers || {}).some((name) => normalizeHeaderName(name) === AUTHORITY_SESSION_HEADER);
}
@@ -59,6 +68,72 @@ function readPayloadErrorMessage(payload = null, fallback = "") {
return String(payload.error || payload.message || payload.reason || fallback || "");
}
function readPayloadCode(payload = null) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return "";
return String(payload.code || payload.reason || payload.category || payload.errorCode || "").trim().toLowerCase();
}
function isSessionRetryCandidate(status = 0, payload = null) {
const numericStatus = Number(status || 0);
if (numericStatus === 401) return true;
if (numericStatus !== 403) return false;
const code = readPayloadCode(payload);
const message = readPayloadErrorMessage(payload, "").toLowerCase();
return /session|token/.test(`${code} ${message}`) && /invalid|expired|missing|unauthorized/.test(`${code} ${message}`);
}
function classifyAuthorityError({ status = 0, payload = null, error = null, timedOut = false, aborted = false } = {}) {
const numericStatus = Number(status || 0);
const payloadCategory = String(payload?.category || "").trim();
if (payloadCategory) return payloadCategory;
if (timedOut || numericStatus === 408) return "timeout";
if (aborted) return "aborted";
if (isSessionRetryCandidate(numericStatus, payload)) return "session";
if (numericStatus === 403) return "permission";
if (numericStatus === 404) return "not-found";
if (numericStatus === 413) return "payload-too-large";
if (numericStatus === 429) return "rate-limit";
if (numericStatus >= 500) return "server";
if (numericStatus >= 400) return "validation";
if (error) return "network";
return "";
}
function createRequestSignal(signal = undefined, timeoutMs = 0) {
const normalizedTimeoutMs = normalizeTimeoutMs(timeoutMs, 0);
if (!signal && normalizedTimeoutMs <= 0) {
return { signal: undefined, cleanup: () => {}, timedOut: () => false };
}
if (typeof AbortController !== "function") {
return { signal, cleanup: () => {}, timedOut: () => false };
}
const controller = new AbortController();
let timeoutId = null;
let timedOut = false;
const abortFromSignal = () => {
controller.abort(signal?.reason || Object.assign(new Error("Authority request aborted"), { name: "AbortError" }));
};
if (signal?.aborted) {
abortFromSignal();
} else if (signal) {
signal.addEventListener("abort", abortFromSignal, { once: true });
}
if (normalizedTimeoutMs > 0) {
timeoutId = setTimeout(() => {
timedOut = true;
controller.abort(Object.assign(new Error("Authority request timed out"), { name: "AbortError" }));
}, normalizedTimeoutMs);
}
return {
signal: controller.signal,
cleanup: () => {
if (timeoutId != null) clearTimeout(timeoutId);
if (signal) signal.removeEventListener("abort", abortFromSignal);
},
timedOut: () => timedOut,
};
}
async function readResponsePayload(response = null) {
if (!response) return {};
const contentType = String(response.headers?.get?.("content-type") || "").toLowerCase();
@@ -91,7 +166,7 @@ export class AuthorityHttpError extends Error {
this.name = "AuthorityHttpError";
this.status = Number(options.status || 0);
this.code = String(options.code || "");
this.category = String(options.category || "");
this.category = String(options.category || classifyAuthorityError(options));
this.payload = clonePlain(options.payload, null);
this.path = String(options.path || "");
this.protocol = String(options.protocol || "");
@@ -107,6 +182,7 @@ export class AuthorityHttpClient {
this.sessionToken = String(options.sessionToken || options.authoritySessionToken || "");
this.sessionInitConfig = buildDefaultSessionInitConfig(options.sessionInitConfig || options.initConfig || options);
this.sessionPromise = null;
this.timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? options.authorityTimeoutMs, 0);
}
async buildHeaders({ session = false } = {}) {
@@ -154,6 +230,10 @@ export class AuthorityHttpClient {
}
async requestJson(path, options = {}) {
return await this._requestJson(path, options, { allowSessionRetry: true });
}
async _requestJson(path, options = {}, state = {}) {
if (typeof this.fetchImpl !== "function") {
throw new AuthorityHttpError("Authority fetch unavailable", {
path,
@@ -166,20 +246,54 @@ export class AuthorityHttpClient {
await this.ensureSession();
}
const headers = await this.buildHeaders({ session });
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
method,
headers,
...(method === "GET" || options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
...(options.signal ? { signal: options.signal } : {}),
});
const requestSignal = createRequestSignal(options.signal, normalizeTimeoutMs(options.timeoutMs, this.timeoutMs));
let response = null;
let payload = {};
try {
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
method,
headers,
...(method === "GET" || options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
...(requestSignal.signal ? { signal: requestSignal.signal } : {}),
});
payload = await readResponsePayload(response);
} catch (error) {
const timedOut = requestSignal.timedOut();
const aborted = error?.name === "AbortError" && !timedOut;
throw new AuthorityHttpError(
timedOut
? `Authority request timed out after ${normalizeTimeoutMs(options.timeoutMs, this.timeoutMs)}ms`
: error?.message || String(error) || "Authority request failed",
{
status: 0,
code: timedOut ? "timeout" : aborted ? "aborted" : "network-error",
category: classifyAuthorityError({ error, timedOut, aborted }),
payload: null,
path,
protocol: options.protocol || this.protocol,
},
);
} finally {
requestSignal.cleanup();
}
const status = Number(response?.status || 0);
const payload = await readResponsePayload(response);
if (!response?.ok) {
const message = readPayloadErrorMessage(payload, `Authority HTTP ${status || "unknown"}`);
if (
session &&
state.allowSessionRetry !== false &&
options.retrySession !== false &&
isSessionRetryCandidate(status, payload)
) {
this.sessionToken = "";
this.sessionPromise = null;
await this.ensureSession();
return await this._requestJson(path, options, { allowSessionRetry: false });
}
throw new AuthorityHttpError(message || `Authority HTTP ${status || "unknown"}`, {
status,
code: payload?.code,
category: payload?.category,
category: classifyAuthorityError({ status, payload }),
payload,
path,
protocol: options.protocol || this.protocol,