fix: ChatGPT interception — synchronous early fetch hook

The fetch interceptor was loaded via <script src="content.js"> which
is asynchronous. ChatGPT's Next.js framework stores a reference to
the original fetch() during module initialization — before our script
finishes downloading. By the time content.js patches window.fetch,
ChatGPT is already using its stored copy of the original.

Fixed with a two-step injection:

1. Injector injects a tiny INLINE <script> (synchronous, instant)
   that stores the real fetch/XHR references and installs a thin
   proxy. This runs before ANY page JavaScript.

2. Content.js loads normally, uses the stored __ssOriginalFetch
   reference, and registers __ssInterceptFetch so the proxy can
   route future calls through the full substitution engine.

This ensures the fetch hook is in place before frameworks like
Next.js, React, or any SPA framework can save a reference to
the original fetch().

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-27 04:27:29 +00:00
parent 02c635c4d7
commit 271f036749
2 changed files with 36 additions and 4 deletions
+10 -3
View File
@@ -891,8 +891,11 @@
// ============================================================
// Fetch Interception — scans ALL POST requests with a body.
// Service-agnostic: doesn't depend on URL patterns.
// Uses __ssOriginalFetch from the early hook (injected synchronously
// before any page JS) to ensure we have the real fetch, even if
// frameworks like Next.js (ChatGPT) store a reference early.
// ============================================================
const originalFetch = window.fetch;
const originalFetch = window.__ssOriginalFetch || window.fetch;
// URLs to never touch (static assets, analytics, etc.)
const SKIP_URL_PATTERNS = [
@@ -1012,6 +1015,10 @@
return originalFetch.call(this, url, options);
};
// Register our fetch interceptor so the early hook proxy can use it
window.__ssInterceptFetch = window.fetch;
window.__ssReady = true;
// ============================================================
// Document Upload Processing
//
@@ -1376,8 +1383,8 @@
// ============================================================
// XMLHttpRequest Interception — same aggressive approach
// ============================================================
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
const origOpen = window.__ssOriginalXHROpen || XMLHttpRequest.prototype.open;
const origSend = window.__ssOriginalXHRSend || XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this._ssUrl = url;
+26 -1
View File
@@ -70,7 +70,32 @@
// Merge active profiles into a flat identity object for the content script
const identity = mergeProfiles(identityData);
// Inject the main interception script into the page's world
// STEP 1: Inject a synchronous inline script that patches fetch/XHR
// IMMEDIATELY, before any page JS can store a reference to the originals.
// This thin proxy queues calls until the full content.js loads.
const earlyHook = document.createElement('script');
earlyHook.textContent = `(function(){
// Store the real fetch/XHR before any page script can
window.__ssOriginalFetch = window.fetch;
window.__ssOriginalXHROpen = XMLHttpRequest.prototype.open;
window.__ssOriginalXHRSend = XMLHttpRequest.prototype.send;
window.__ssReady = false;
window.__ssQueue = [];
// Replace fetch with a proxy that queues until content.js is ready
window.fetch = function() {
if (window.__ssReady && window.__ssInterceptFetch) {
return window.__ssInterceptFetch.apply(this, arguments);
}
// If not ready yet, call original (no substitution possible)
return window.__ssOriginalFetch.apply(this, arguments);
};
})();`;
(document.head || document.documentElement).appendChild(earlyHook);
earlyHook.remove();
// STEP 2: Load the full content.js which will use __ssOriginalFetch
// and set __ssReady = true when it's done hooking
const script = document.createElement('script');
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
script.src = api.runtime.getURL('src/content/content.js');