mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
harden(authority): add pre-scale diagnostics and request safety
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user