From 8e76e4cbaa50c8656267fbbfaba87dc492f1b57c Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Tue, 28 Jul 2026 11:28:04 +0100 Subject: [PATCH] fix: stop re-fetching property/Connect JSON that is known to 404 The property-tooltip script caches successful JSON fetches in localStorage for 24 hours but never caches failures, so when the referenced attachment does not exist (version drift between release tags and generated JSON), every page view re-requests a URL that is guaranteed to 404 (~17k requests/day). The Bloblang script has the same problem, plus a hardcoded fallback-version chain that multiplies the misses. Property tooltips now store a failure marker (1 hour TTL, versioned by latest-redpanda-tag) and resolve to an empty lookup while it is fresh. The Bloblang loader tracks per-URL failures for 1 hour and skips URLs that recently returned an error response. Preview mode is unaffected. Co-Authored-By: Claude Fable 5 --- src/js/16-bloblang-interactive.js | 43 +++++++++++++++++++++++++++++++ src/js/19-property-tooltips.js | 22 ++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/js/16-bloblang-interactive.js b/src/js/16-bloblang-interactive.js index 6aceb4fc..81986159 100644 --- a/src/js/16-bloblang-interactive.js +++ b/src/js/16-bloblang-interactive.js @@ -72,6 +72,42 @@ ); } + /** + * Track Connect JSON URLs that recently returned an error response, so a + * URL that is known to 404 (for example, a version whose JSON was never + * generated) is not re-requested on every page view. + */ + const FETCH_FAILURE_KEY = 'connect-json-fetch-failures'; + const FETCH_FAILURE_TTL = 60 * 60 * 1000; // 1 hour + + function readFetchFailures() { + try { + const parsed = JSON.parse(localStorage.getItem(FETCH_FAILURE_KEY) || '{}'); + const now = Date.now(); + const fresh = {}; + Object.keys(parsed).forEach((url) => { + if (now - parsed[url] < FETCH_FAILURE_TTL) fresh[url] = parsed[url]; + }); + return fresh; + } catch (e) { + return {}; + } + } + + function hasRecentFetchFailure(url) { + return url in readFetchFailures(); + } + + function markFetchFailure(url) { + try { + const failures = readFetchFailures(); + failures[url] = Date.now(); + localStorage.setItem(FETCH_FAILURE_KEY, JSON.stringify(failures)); + } catch (e) { + // localStorage not available + } + } + /** * Parse a Bloblang snippet into mapping, input, and metadata sections. * Looks for # In: and # Meta: comment directives. @@ -196,11 +232,18 @@ if (!url) { url = `/redpanda-connect/components/_attachments/connect-${version}.json`; } + + if (hasRecentFetchFailure(url)) { + return null; + } } const response = await fetch(url); if (!response.ok) { + if (!isPreviewMode()) { + markFetchFailure(url); + } return null; } diff --git a/src/js/19-property-tooltips.js b/src/js/19-property-tooltips.js index e447c588..ef24a0ef 100644 --- a/src/js/19-property-tooltips.js +++ b/src/js/19-property-tooltips.js @@ -92,6 +92,7 @@ var CACHE_KEY = 'redpanda-properties-cache' var CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours + var FAILURE_CACHE_TTL = 60 * 60 * 1000 // 1 hour: retry failed fetches sooner so a fix deploy is picked up // Use latest-redpanda-tag meta tag for cache versioning var cacheVersion = getLatestRedpandaTag() || 'unknown' @@ -101,8 +102,11 @@ var cached = localStorage.getItem(CACHE_KEY) if (cached) { var parsed = JSON.parse(cached) - if (parsed.version === cacheVersion && Date.now() - parsed.timestamp < CACHE_TTL) { - propertiesData = parsed.data + var age = Date.now() - parsed.timestamp + if (parsed.version === cacheVersion && (parsed.failed ? age < FAILURE_CACHE_TTL : age < CACHE_TTL)) { + // failed entries resolve to an empty lookup so a URL known to 404 + // is not re-requested on every page view + propertiesData = parsed.failed ? {} : parsed.data propertiesLoading = false propertiesLoadQueue.forEach(function (resolve) { resolve(propertiesData) @@ -182,6 +186,20 @@ } console.warn('Property tooltips: Failed to load properties data:', error) + if (!isPreviewMode()) { + try { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ + version: cacheVersion, + timestamp: Date.now(), + failed: true, + }) + ) + } catch (cacheError) { + // localStorage full or unavailable + } + } propertiesLoading = false propertiesData = {} propertiesLoadQueue.forEach(function (resolve) {