From 87a20c10cd4c2b42d907e873cfe562fbff946846 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Apr 2026 00:19:39 +0000 Subject: [PATCH] fix(engine): suppress font-load 404s by checking console location URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome's "Failed to load resource" message text does not include the failing URL — it's only on msg.location().url. The previous filter in frameCapture.ts only checked msg.text(), so every font 404 (e.g. Google Fonts tags in sandboxed render environments) fell through to the "[non-blocking]" prefix instead of being suppressed. Extract the classifier into isFontResourceError() and match against both text and location.url, and extend the extension match to .ttf/.otf. Adds a unit test covering the URL-in-location, URL-in-text, and non-font cases. This is a targeted fix for the render-output noise that PR #311 attempted to address by adding a ~120-entry SYSTEM_FONTS skip list. That approach silently shadowed existing FONT_ALIASES (arial→inter, helvetica→inter, courier new→jetbrains-mono, segoe ui→roboto, etc.) and changed render output on Linux fleets that don't have those fonts installed. Fixing the console-log filter here suppresses the noise without changing any font resolution behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../engine/src/services/frameCapture.test.ts | 112 ++++++++++++++++++ packages/engine/src/services/frameCapture.ts | 30 +++-- 2 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 packages/engine/src/services/frameCapture.test.ts diff --git a/packages/engine/src/services/frameCapture.test.ts b/packages/engine/src/services/frameCapture.test.ts new file mode 100644 index 000000000..1b280c8b4 --- /dev/null +++ b/packages/engine/src/services/frameCapture.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from "vitest"; +import { isFontResourceError } from "./frameCapture.js"; + +describe("isFontResourceError", () => { + it("matches Google Fonts CSS load failures via location.url", () => { + expect( + isFontResourceError( + "error", + "Failed to load resource: net::ERR_FAILED", + "https://fonts.googleapis.com/css2?family=Inter", + ), + ).toBe(true); + }); + + it("matches gstatic font binaries via location.url", () => { + expect( + isFontResourceError( + "error", + "Failed to load resource: the server responded with a status of 404 (Not Found)", + "https://fonts.gstatic.com/s/inter/v12/foo.woff2", + ), + ).toBe(true); + }); + + it("matches self-hosted woff2 failures", () => { + expect( + isFontResourceError( + "error", + "Failed to load resource: net::ERR_CONNECTION_REFUSED", + "http://localhost:9999/font.woff2", + ), + ).toBe(true); + }); + + it("matches .ttf and .otf URLs", () => { + expect( + isFontResourceError("error", "Failed to load resource: 404", "http://example.com/a.ttf"), + ).toBe(true); + expect( + isFontResourceError("error", "Failed to load resource: 404", "http://example.com/b.otf"), + ).toBe(true); + }); + + it("does NOT match non-font resources (images, scripts, videos)", () => { + expect( + isFontResourceError("error", "Failed to load resource: 404", "https://example.com/img.png"), + ).toBe(false); + expect( + isFontResourceError( + "error", + "Failed to load resource: 404", + "https://cdn.example.com/bundle.js", + ), + ).toBe(false); + expect( + isFontResourceError("error", "Failed to load resource: 404", "https://example.com/video.mp4"), + ).toBe(false); + }); + + it("does NOT match when location.url is missing and text has no URL (safe default)", () => { + expect(isFontResourceError("error", "Failed to load resource: 404", "")).toBe(false); + }); + + it("still matches when URL appears in text (older Chrome formats)", () => { + expect( + isFontResourceError( + "error", + "Failed to load resource: https://fonts.googleapis.com/... 404", + "", + ), + ).toBe(true); + }); + + it("does NOT match non-error console messages", () => { + expect( + isFontResourceError( + "warn", + "Failed to load resource: 404", + "https://fonts.googleapis.com/css2", + ), + ).toBe(false); + expect( + isFontResourceError( + "info", + "Failed to load resource: 404", + "https://fonts.googleapis.com/css2", + ), + ).toBe(false); + }); + + it("does NOT match unrelated error messages", () => { + expect(isFontResourceError("error", "Uncaught ReferenceError: x is not defined", "")).toBe( + false, + ); + expect( + isFontResourceError("error", "Some other error", "https://fonts.googleapis.com/css2"), + ).toBe(false); + }); + + it("is case-insensitive for URL matching", () => { + expect( + isFontResourceError( + "error", + "Failed to load resource: 404", + "https://FONTS.GOOGLEAPIS.COM/css2", + ), + ).toBe(true); + expect( + isFontResourceError("error", "Failed to load resource: 404", "http://example.com/FONT.WOFF2"), + ).toBe(true); + }); +}); diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index d6f6ef512..d49c2735f 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -142,6 +142,27 @@ export async function createCaptureSession( }; } +/** + * Classify a console "Failed to load resource" error as a font-load failure. + * + * These are expected when deterministic font injection replaces Google Fonts + * @import URLs with embedded base64 — or when the render environment has no + * network access to Google Fonts. Suppressing them reduces noise in render + * output without hiding real asset failures (images, videos, scripts, etc.). + * + * Chrome's `msg.text()` for a failed resource is typically just + * `"Failed to load resource: net::ERR_FAILED"` — the URL is only on + * `msg.location().url`. We match against both so the filter works regardless + * of which form Chrome emits. + */ +export function isFontResourceError(type: string, text: string, locationUrl: string): boolean { + if (type !== "error") return false; + if (!text.startsWith("Failed to load resource")) return false; + return /fonts\.googleapis|fonts\.gstatic|\.(woff2?|ttf|otf)(\b|$)/i.test( + `${locationUrl} ${text}`, + ); +} + export async function initializeSession(session: CaptureSession): Promise { const { page, serverUrl } = session; @@ -149,13 +170,8 @@ export async function initializeSession(session: CaptureSession): Promise page.on("console", (msg: ConsoleMessage) => { const type = msg.type(); const text = msg.text(); - - // Suppress font-loading 404s entirely. These are expected when deterministic - // font injection replaces Google Fonts @import URLs with embedded base64. - const isFontLoadError = - type === "error" && - text.startsWith("Failed to load resource") && - /fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text); + const locationUrl = msg.location()?.url ?? ""; + const isFontLoadError = isFontResourceError(type, text, locationUrl); // Other "Failed to load resource" 404s are typically non-blocking (e.g. // favicon, sourcemaps, optional assets). Prefix them so users know they