From 3ff56020b16f77183b35e622fb2eefb0bc7b2ac7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 00:34:02 +0100 Subject: [PATCH] refactor: dedupe gateway trimmed readers --- src/gateway/auth.ts | 4 +++- src/gateway/embeddings-http.ts | 12 +++++++++--- src/gateway/http-utils.ts | 14 ++++++++------ src/gateway/probe-target.ts | 4 ++-- src/gateway/protocol/connect-error-details.ts | 5 +++-- src/gateway/server-close.ts | 3 ++- src/gateway/server-methods/approval-shared.ts | 3 ++- src/gateway/server-methods/nodes.helpers.ts | 7 ++----- src/gateway/server-methods/nodes.ts | 4 ++-- src/gateway/server-methods/push.ts | 3 ++- src/gateway/server-methods/system.ts | 18 +++++++++++------- src/gateway/server-methods/tools-catalog.ts | 7 ++++--- src/gateway/server-methods/usage.ts | 7 ++++--- src/gateway/server-utils.ts | 5 +++-- src/gateway/startup-auth.ts | 8 +++----- 15 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index 6d49991c49e..7a6c1013492 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -110,7 +110,9 @@ type TailscaleUser = { type TailscaleWhoisLookup = (ip: string) => Promise; 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 { diff --git a/src/gateway/embeddings-http.ts b/src/gateway/embeddings-http.ts index f08c8ecba7c..2e3134d1f2d 100644 --- a/src/gateway/embeddings-http.ts +++ b/src/gateway/embeddings-http.ts @@ -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, diff --git a/src/gateway/http-utils.ts b/src/gateway/http-utils.ts index c60b79390d5..55a2b661f11 100644 --- a/src/gateway/http-utils.ts +++ b/src/gateway/http-utils.ts @@ -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; @@ -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; diff --git a/src/gateway/probe-target.ts b/src/gateway/probe-target.ts index 1b9b42b6b78..e20bb2b1ca3 100644 --- a/src/gateway/probe-target.ts +++ b/src/gateway/probe-target.ts @@ -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, diff --git a/src/gateway/protocol/connect-error-details.ts b/src/gateway/protocol/connect-error-details.ts index aa1a30d8866..8075fca09d8 100644 --- a/src/gateway/protocol/connect-error-details.ts +++ b/src/gateway/protocol/connect-error-details.ts @@ -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, ) diff --git a/src/gateway/server-close.ts b/src/gateway/server-close.ts index 047d4e515d1..0425f19e3fb 100644 --- a/src/gateway/server-close.ts +++ b/src/gateway/server-close.ts @@ -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) diff --git a/src/gateway/server-methods/approval-shared.ts b/src/gateway/server-methods/approval-shared.ts index c44846235a0..7b74a3f3d49 100644 --- a/src/gateway/server-methods/approval-shared.ts +++ b/src/gateway/server-methods/approval-shared.ts @@ -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(params: { inputId: unknown; respond: RespondFn; }): Promise { - 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; diff --git a/src/gateway/server-methods/nodes.helpers.ts b/src/gateway/server-methods/nodes.helpers.ts index 30ab7f02062..012cddfe7c5 100644 --- a/src/gateway/server-methods/nodes.helpers.ts +++ b/src/gateway/server-methods/nodes.helpers.ts @@ -61,11 +61,8 @@ export function respondUnavailableOnNodeInvokeError 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, diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index c9b15daf514..342e98d6d87 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -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"); } diff --git a/src/gateway/server-methods/push.ts b/src/gateway/server-methods/push.ts index 62f593dd2e9..44ebdf3e4aa 100644 --- a/src/gateway/server-methods/push.ts +++ b/src/gateway/server-methods/push.ts @@ -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; diff --git a/src/gateway/server-methods/system.ts b/src/gateway/server-methods/system.ts index 5588551d67c..084a9debd5a 100644 --- a/src/gateway/server-methods/system.ts +++ b/src/gateway/server-methods/system.ts @@ -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) { diff --git a/src/gateway/server-methods/tools-catalog.ts b/src/gateway/server-methods/tools-catalog.ts index 98ccd13622c..fa86e5ff3f9 100644 --- a/src/gateway/server-methods/tools-catalog.ts +++ b/src/gateway/server-methods/tools-catalog.ts @@ -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) { diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index 83e96083f65..fa8d77a6594 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -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; diff --git a/src/gateway/server-utils.ts b/src/gateway/server-utils.ts index ed812756706..697eb1a5ea4 100644 --- a/src/gateway/server-utils.ts +++ b/src/gateway/server-utils.ts @@ -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(); diff --git a/src/gateway/startup-auth.ts b/src/gateway/startup-auth.ts index 36c1b83525c..b50a8818345 100644 --- a/src/gateway/startup-auth.ts +++ b/src/gateway/startup-auth.ts @@ -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; }