From dc3101b9bce27d2d4939d250345e30b58faa4c76 Mon Sep 17 00:00:00 2001 From: "Ikenga Ifeanyi .M." Date: Sun, 28 Jun 2026 12:46:19 +0100 Subject: [PATCH 1/2] Harden provider failure logging --- apps/api/src/services/query-service.test.ts | 59 ++++++++++++++++++++- apps/api/src/services/query-service.ts | 45 +++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/query-service.test.ts b/apps/api/src/services/query-service.test.ts index 201456e..76a5f07 100644 --- a/apps/api/src/services/query-service.test.ts +++ b/apps/api/src/services/query-service.test.ts @@ -1,7 +1,28 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { UnsafeScrapeUrlError } from "../lib/scrape-url-safety.js"; +const registryExecuteMock = vi.fn(); +const loggerErrorMock = vi.fn(); + +vi.mock("../providers/index.js", () => ({ + registry: { + execute: (...args: unknown[]) => registryExecuteMock(...args) + } +})); + +vi.mock("../lib/logger.js", () => ({ + logger: { + error: (...args: unknown[]) => loggerErrorMock(...args) + } +})); + describe("executeQuery", () => { + beforeEach(() => { + registryExecuteMock.mockReset(); + loggerErrorMock.mockReset(); + vi.resetModules(); + }); + it("rejects unsafe scrape URLs at the service boundary", async () => { process.env.X402_PAY_TO_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; const { executeQuery } = await import("./query-service.js"); @@ -14,4 +35,40 @@ describe("executeQuery", () => { }) ).rejects.toBeInstanceOf(UnsafeScrapeUrlError); }); + + it("logs provider failures with safe metadata only", async () => { + process.env.X402_PAY_TO_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; + const providerError = new Error( + 'upstream failed query="super secret question" url=https://secret.example.test/search payment-response=proof_123 Authorization:Bearer token_abc privateKey=wallet_secret' + ); + registryExecuteMock.mockRejectedValueOnce(providerError); + + const { executeQuery } = await import("./query-service.js"); + + await expect( + executeQuery({ + mode: "search", + provider: "search.live", + q: "super secret question" + }) + ).rejects.toThrow(providerError.message); + + expect(loggerErrorMock).toHaveBeenCalledTimes(1); + + const [payload, message] = loggerErrorMock.mock.calls[0]; + const serializedPayload = JSON.stringify(payload); + + expect(message).toBe("provider execution failed"); + expect(payload).toMatchObject({ + providerId: "search.live", + mode: "search", + errorClass: "Error" + }); + expect(payload.errorMessage).toContain("[redacted-url]"); + expect(serializedPayload).not.toContain("super secret question"); + expect(serializedPayload).not.toContain("https://secret.example.test/search"); + expect(serializedPayload).not.toContain("proof_123"); + expect(serializedPayload).not.toContain("token_abc"); + expect(serializedPayload).not.toContain("wallet_secret"); + }); }); diff --git a/apps/api/src/services/query-service.ts b/apps/api/src/services/query-service.ts index 43264e1..bd1a214 100644 --- a/apps/api/src/services/query-service.ts +++ b/apps/api/src/services/query-service.ts @@ -3,6 +3,33 @@ import { registry } from "../providers/index.js"; import { nanoid } from "nanoid"; import { QueryResult } from "@query402/shared"; import { validateScrapeUrl } from "../lib/scrape-url-safety.js"; +import { logger } from "../lib/logger.js"; + +function getErrorClass(error: unknown): string { + if (error instanceof Error && error.name) { + return error.name; + } + + return typeof error; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return String(error); +} + +function sanitizeErrorMessage(message: string): string { + return message + .replace(/https?:\/\/\S+/gi, "[redacted-url]") + .replace(/\b(payment-response|x-payment-response|authorization)\b\s*[:=]\s*([^\s,;]+)/gi, "$1=[redacted]") + .replace( + /\b(query|queryOrUrl|q|url|targetUrl|secret|api[_ -]?key|token|private[_ -]?key|privateKey|seed)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^,;]+)/gi, + "$1=[redacted]" + ); +} export async function executeQuery(params: { mode: "search" | "news" | "scrape"; @@ -24,7 +51,23 @@ export async function executeQuery(params: { // Registry handles provider matching, circuit breaking, timeouts, and fallbacks const start = Date.now(); - const execution = await registry.execute(params.mode, params.provider, safeInput); + + let execution; + try { + execution = await registry.execute(params.mode, params.provider, safeInput); + } catch (error) { + logger.error( + { + providerId: params.provider, + mode: params.mode, + errorClass: getErrorClass(error), + errorMessage: sanitizeErrorMessage(getErrorMessage(error)) + }, + "provider execution failed" + ); + throw error; + } + const latencyMs = Date.now() - start; return { From a415ae7b5bcb62d786089260b7187a0919f6d90f Mon Sep 17 00:00:00 2001 From: "Ikenga Ifeanyi .M." Date: Mon, 29 Jun 2026 06:21:02 +0100 Subject: [PATCH 2/2] Fix provider failure logging test failures. Import UnsafeScrapeUrlError after module reset to avoid instanceof mismatch, and preserve [redacted-url] tokens by redacting url/targetUrl keys before the generic key-value pass. Co-authored-by: Cursor --- apps/api/src/services/query-service.test.ts | 6 ++++-- apps/api/src/services/query-service.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/api/src/services/query-service.test.ts b/apps/api/src/services/query-service.test.ts index 76a5f07..20acda2 100644 --- a/apps/api/src/services/query-service.test.ts +++ b/apps/api/src/services/query-service.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { UnsafeScrapeUrlError } from "../lib/scrape-url-safety.js"; const registryExecuteMock = vi.fn(); const loggerErrorMock = vi.fn(); @@ -25,7 +24,10 @@ describe("executeQuery", () => { it("rejects unsafe scrape URLs at the service boundary", async () => { process.env.X402_PAY_TO_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; - const { executeQuery } = await import("./query-service.js"); + const [{ executeQuery }, { UnsafeScrapeUrlError }] = await Promise.all([ + import("./query-service.js"), + import("../lib/scrape-url-safety.js") + ]); await expect( executeQuery({ diff --git a/apps/api/src/services/query-service.ts b/apps/api/src/services/query-service.ts index bd1a214..df24cfc 100644 --- a/apps/api/src/services/query-service.ts +++ b/apps/api/src/services/query-service.ts @@ -23,12 +23,16 @@ function getErrorMessage(error: unknown): string { function sanitizeErrorMessage(message: string): string { return message - .replace(/https?:\/\/\S+/gi, "[redacted-url]") + .replace( + /\b(url|targetUrl)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^,;]+)/gi, + "$1=[redacted-url]" + ) .replace(/\b(payment-response|x-payment-response|authorization)\b\s*[:=]\s*([^\s,;]+)/gi, "$1=[redacted]") .replace( - /\b(query|queryOrUrl|q|url|targetUrl|secret|api[_ -]?key|token|private[_ -]?key|privateKey|seed)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^,;]+)/gi, + /\b(query|queryOrUrl|q|secret|api[_ -]?key|token|private[_ -]?key|privateKey|seed)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^,;]+)/gi, "$1=[redacted]" - ); + ) + .replace(/https?:\/\/\S+/gi, "[redacted-url]"); } export async function executeQuery(params: {