refactor: dedupe gateway trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 00:34:02 +01:00
parent 4cfa4b95c3
commit 3ff56020b1
15 changed files with 60 additions and 44 deletions

View File

@@ -110,7 +110,9 @@ type TailscaleUser = {
type TailscaleWhoisLookup = (ip: string) => Promise<TailscaleWhoisIdentity | null>;
function hasExplicitSharedSecretAuth(connectAuth?: ConnectAuth | null): boolean {
return Boolean(connectAuth?.token?.trim() || connectAuth?.password?.trim());
return Boolean(
normalizeOptionalString(connectAuth?.token) || normalizeOptionalString(connectAuth?.password),
);
}
function normalizeLogin(login: string): string {

View File

@@ -13,7 +13,10 @@ import type {
MemoryEmbeddingProvider,
MemoryEmbeddingProviderAdapter,
} from "../plugins/memory-embedding-providers.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { sendJson } from "./http-common.js";
@@ -227,7 +230,7 @@ export async function handleOpenAiEmbeddingsHttpRequest(
}
const payload = coerceRequest(handled.body);
const requestModel = typeof payload.model === "string" ? payload.model.trim() : "";
const requestModel = normalizeOptionalString(payload.model) ?? "";
if (!requestModel) {
sendJson(res, 400, {
error: { message: "Missing `model`.", type: "invalid_request_error" },
@@ -268,7 +271,10 @@ export async function handleOpenAiEmbeddingsHttpRequest(
const agentDir = resolveAgentDir(cfg, agentId);
const memorySearch = resolveMemorySearchConfig(cfg, agentId);
const configuredProvider = memorySearch?.provider ?? "openai";
const overrideModel = getHeader(req, "x-openclaw-model")?.trim() || memorySearch?.model || "";
const overrideModel =
normalizeOptionalString(getHeader(req, "x-openclaw-model")) ||
normalizeOptionalString(memorySearch?.model) ||
"";
const target = resolveEmbeddingsTarget({
requestModel: overrideModel,
configuredProvider,

View File

@@ -9,7 +9,10 @@ import {
} from "../agents/model-selection.js";
import { loadConfig } from "../config/config.js";
import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import {
@@ -36,12 +39,11 @@ export function getHeader(req: IncomingMessage, name: string): string | undefine
}
export function getBearerToken(req: IncomingMessage): string | undefined {
const raw = getHeader(req, "authorization")?.trim() ?? "";
const raw = normalizeOptionalString(getHeader(req, "authorization")) ?? "";
if (!normalizeLowercaseStringOrEmpty(raw).startsWith("bearer ")) {
return undefined;
}
const token = raw.slice(7).trim();
return token || undefined;
return normalizeOptionalString(raw.slice(7));
}
type SharedSecretGatewayAuth = Pick<ResolvedGatewayAuth, "mode">;
@@ -191,8 +193,8 @@ export function resolveOpenAiCompatibleHttpSenderIsOwner(
export function resolveAgentIdFromHeader(req: IncomingMessage): string | undefined {
const raw =
getHeader(req, "x-openclaw-agent-id")?.trim() ||
getHeader(req, "x-openclaw-agent")?.trim() ||
normalizeOptionalString(getHeader(req, "x-openclaw-agent-id")) ||
normalizeOptionalString(getHeader(req, "x-openclaw-agent")) ||
"";
if (!raw) {
return undefined;

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type GatewayProbeTargetResolution = {
gatewayMode: "local" | "remote";
@@ -8,8 +9,7 @@ export type GatewayProbeTargetResolution = {
export function resolveGatewayProbeTarget(cfg: OpenClawConfig): GatewayProbeTargetResolution {
const gatewayMode = cfg.gateway?.mode === "remote" ? "remote" : "local";
const remoteUrlRaw =
typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
const remoteUrlRaw = normalizeOptionalString(cfg.gateway?.remote?.url) ?? "";
const remoteUrlMissing = gatewayMode === "remote" && !remoteUrlRaw;
return {
gatewayMode,

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export const ConnectErrorDetailCodes = {
AUTH_REQUIRED: "AUTH_REQUIRED",
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
@@ -126,8 +128,7 @@ export function readConnectErrorRecoveryAdvice(details: unknown): ConnectErrorRe
};
const canRetryWithDeviceToken =
typeof raw.canRetryWithDeviceToken === "boolean" ? raw.canRetryWithDeviceToken : undefined;
const normalizedNextStep =
typeof raw.recommendedNextStep === "string" ? raw.recommendedNextStep.trim() : "";
const normalizedNextStep = normalizeOptionalString(raw.recommendedNextStep) ?? "";
const recommendedNextStep = CONNECT_RECOVERY_NEXT_STEP_VALUES.has(
normalizedNextStep as ConnectRecoveryNextStep,
)

View File

@@ -6,6 +6,7 @@ import { stopGmailWatcher } from "../hooks/gmail-watcher.js";
import type { HeartbeatRunner } from "../infra/heartbeat-runner.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { PluginServicesHandle } from "../plugins/services.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
const shutdownLog = createSubsystemLogger("gateway/shutdown");
const WEBSOCKET_CLOSE_GRACE_MS = 1_000;
@@ -42,7 +43,7 @@ export function createGatewayCloseHandler(params: {
}) {
return async (opts?: { reason?: string; restartExpectedMs?: number | null }) => {
try {
const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : "";
const reasonRaw = normalizeOptionalString(opts?.reason) ?? "";
const reason = reasonRaw || "gateway stopping";
const restartExpectedMs =
typeof opts?.restartExpectedMs === "number" && Number.isFinite(opts.restartExpectedMs)

View File

@@ -1,5 +1,6 @@
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type {
ExecApprovalIdLookupResult,
ExecApprovalManager,
@@ -112,7 +113,7 @@ export async function handleApprovalWaitDecision<TPayload>(params: {
inputId: unknown;
respond: RespondFn;
}): Promise<void> {
const id = typeof params.inputId === "string" ? params.inputId.trim() : "";
const id = normalizeOptionalString(params.inputId) ?? "";
if (!id) {
params.respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required"));
return;

View File

@@ -61,11 +61,8 @@ export function respondUnavailableOnNodeInvokeError<T extends { ok: boolean; err
res.error && typeof res.error === "object"
? (res.error as { code?: unknown; message?: unknown })
: null;
const nodeCode = typeof nodeError?.code === "string" ? nodeError.code.trim() : "";
const nodeMessage =
typeof nodeError?.message === "string" && nodeError.message.trim().length > 0
? nodeError.message.trim()
: "node invoke failed";
const nodeCode = normalizeOptionalString(nodeError?.code) ?? "";
const nodeMessage = normalizeOptionalString(nodeError?.message) ?? "node invoke failed";
const message = nodeCode ? `${nodeCode}: ${nodeMessage}` : nodeMessage;
respond(
false,

View File

@@ -198,8 +198,8 @@ function shouldQueueAsPendingForegroundAction(params: {
params.error && typeof params.error === "object"
? (params.error as { code?: unknown; message?: unknown })
: null;
const code = typeof error?.code === "string" ? error.code.trim().toUpperCase() : "";
const message = typeof error?.message === "string" ? error.message.trim().toUpperCase() : "";
const code = normalizeOptionalString(error?.code)?.toUpperCase() ?? "";
const message = normalizeOptionalString(error?.message)?.toUpperCase() ?? "";
return code === "NODE_BACKGROUND_UNAVAILABLE" || message.includes("BACKGROUND_UNAVAILABLE");
}

View File

@@ -8,6 +8,7 @@ import {
sendApnsAlert,
shouldClearStoredApnsRegistration,
} from "../../infra/push-apns.js";
import { normalizeStringifiedOptionalString } from "../../shared/string-coerce.js";
import { ErrorCodes, errorShape, validatePushTestParams } from "../protocol/index.js";
import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js";
import { normalizeTrimmedString } from "./record-shared.js";
@@ -24,7 +25,7 @@ export const pushHandlers: GatewayRequestHandlers = {
return;
}
const nodeId = String(params.nodeId ?? "").trim();
const nodeId = normalizeStringifiedOptionalString(params.nodeId) ?? "";
if (!nodeId) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"));
return;

View File

@@ -7,7 +7,11 @@ import { getLastHeartbeatEvent } from "../../infra/heartbeat-events.js";
import { setHeartbeatsEnabled } from "../../infra/heartbeat-runner.js";
import { enqueueSystemEvent, isSystemEventContextChanged } from "../../infra/system-events.js";
import { listSystemPresence, updateSystemPresence } from "../../infra/system-presence.js";
import { normalizeLowercaseStringOrEmpty, readStringValue } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
readStringValue,
} from "../../shared/string-coerce.js";
import { ErrorCodes, errorShape } from "../protocol/index.js";
import { broadcastPresenceSnapshot } from "../server/presence-events.js";
import type { GatewayRequestHandlers } from "./types.js";
@@ -48,7 +52,7 @@ export const systemHandlers: GatewayRequestHandlers = {
respond(true, presence, undefined);
},
"system-event": ({ params, respond, context }) => {
const text = typeof params.text === "string" ? params.text.trim() : "";
const text = normalizeOptionalString(params.text) ?? "";
if (!text) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "text required"));
return;
@@ -115,18 +119,18 @@ export const systemHandlers: GatewayRequestHandlers = {
const contextChanged = isSystemEventContextChanged(sessionKey, presenceUpdate.key);
const parts: string[] = [];
if (contextChanged || hostChanged || ipChanged) {
const hostLabel = next.host?.trim() || "Unknown";
const ipLabel = next.ip?.trim();
const hostLabel = normalizeOptionalString(next.host) ?? "Unknown";
const ipLabel = normalizeOptionalString(next.ip);
parts.push(`Node: ${hostLabel}${ipLabel ? ` (${ipLabel})` : ""}`);
}
if (versionChanged) {
parts.push(`app ${next.version?.trim() || "unknown"}`);
parts.push(`app ${normalizeOptionalString(next.version) ?? "unknown"}`);
}
if (modeChanged) {
parts.push(`mode ${next.mode?.trim() || "unknown"}`);
parts.push(`mode ${normalizeOptionalString(next.mode) ?? "unknown"}`);
}
if (reasonChanged) {
parts.push(`reason ${reasonValue?.trim() || "event"}`);
parts.push(`reason ${normalizeOptionalString(reasonValue) ?? "event"}`);
}
const deltaText = parts.join(" · ");
if (deltaText) {

View File

@@ -12,6 +12,7 @@ import {
import { summarizeToolDescriptionText } from "../../agents/tool-description-summary.js";
import { loadConfig } from "../../config/config.js";
import { getPluginToolMeta, resolvePluginTools } from "../../plugins/tools.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
ErrorCodes,
errorShape,
@@ -42,7 +43,7 @@ type ToolCatalogGroup = {
function resolveAgentIdOrRespondError(rawAgentId: unknown, respond: RespondFn) {
const cfg = loadConfig();
const knownAgents = listAgentIds(cfg);
const requestedAgentId = typeof rawAgentId === "string" ? rawAgentId.trim() : "";
const requestedAgentId = normalizeOptionalString(rawAgentId) ?? "";
const agentId = requestedAgentId || resolveDefaultAgentId(cfg);
if (requestedAgentId && !knownAgents.includes(agentId)) {
respond(
@@ -105,7 +106,7 @@ function buildPluginGroups(params: {
} as ToolCatalogGroup);
existing.tools.push({
id: tool.name,
label: typeof tool.label === "string" && tool.label.trim() ? tool.label.trim() : tool.name,
label: normalizeOptionalString(tool.label) ?? tool.name,
description: summarizeToolDescriptionText({
rawDescription: typeof tool.description === "string" ? tool.description : undefined,
displaySummary: tool.displaySummary,
@@ -130,7 +131,7 @@ export function buildToolsCatalogResult(params: {
agentId?: string;
includePlugins?: boolean;
}): ToolsCatalogResult {
const agentId = params.agentId?.trim() || resolveDefaultAgentId(params.cfg);
const agentId = normalizeOptionalString(params.agentId) || resolveDefaultAgentId(params.cfg);
const includePlugins = params.includePlugins !== false;
const groups = buildCoreGroups();
if (includePlugins) {

View File

@@ -22,6 +22,7 @@ import {
} from "../../infra/session-cost-usage.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolvePreferredSessionKeyForSessionIdMatches } from "../../sessions/session-id-resolution.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
buildUsageAggregateTail,
mergeUsageDailyLatency,
@@ -404,7 +405,7 @@ export const usageHandlers: GatewayRequestHandlers = {
});
const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? p.limit : 50;
const includeContextWeight = p.includeContextWeight ?? false;
const specificKey = typeof p.key === "string" ? p.key.trim() : null;
const specificKey = normalizeOptionalString(p.key) ?? null;
// Load session store for named sessions
const { storePath, store } = loadCombinedSessionStoreForGateway(config);
@@ -824,7 +825,7 @@ export const usageHandlers: GatewayRequestHandlers = {
respond(true, result, undefined);
},
"sessions.usage.timeseries": async ({ respond, params }) => {
const key = typeof params?.key === "string" ? params.key.trim() : null;
const key = normalizeOptionalString(params?.key) ?? null;
if (!key) {
respond(
false,
@@ -861,7 +862,7 @@ export const usageHandlers: GatewayRequestHandlers = {
respond(true, timeseries, undefined);
},
"sessions.usage.logs": async ({ respond, params }) => {
const key = typeof params?.key === "string" ? params.key.trim() : null;
const key = normalizeOptionalString(params?.key) ?? null;
if (!key) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key is required for logs"));
return;

View File

@@ -1,10 +1,11 @@
import { defaultVoiceWakeTriggers } from "../infra/voicewake.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function normalizeVoiceWakeTriggers(input: unknown): string[] {
const raw = Array.isArray(input) ? input : [];
const cleaned = raw
.map((v) => (typeof v === "string" ? v.trim() : ""))
.filter((v) => v.length > 0)
.map((v) => normalizeOptionalString(v))
.filter((v): v is string => v !== undefined)
.slice(0, 32)
.map((v) => v.slice(0, 64));
return cleaned.length > 0 ? cleaned : defaultVoiceWakeTriggers();

View File

@@ -5,6 +5,7 @@ import type {
OpenClawConfig,
} from "../config/config.js";
import { replaceConfigFile } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
hasConfiguredGatewayAuthSecretInput,
resolveGatewayPasswordSecretRefValue,
@@ -244,15 +245,12 @@ export function assertHooksTokenSeparateFromGatewayAuth(params: {
if (params.cfg.hooks?.enabled !== true) {
return;
}
const hooksToken =
typeof params.cfg.hooks.token === "string" ? params.cfg.hooks.token.trim() : "";
const hooksToken = normalizeOptionalString(params.cfg.hooks.token) ?? "";
if (!hooksToken) {
return;
}
const gatewayToken =
params.auth.mode === "token" && typeof params.auth.token === "string"
? params.auth.token.trim()
: "";
params.auth.mode === "token" ? (normalizeOptionalString(params.auth.token) ?? "") : "";
if (!gatewayToken) {
return;
}