UNPKG

reactotron-react-native

Version:

A development tool to explore, inspect, and diagnose your React Native apps.

198 lines (191 loc) 7.2 kB
"use strict"; /** * Intercepts the global `fetch` when it is Expo's expo/fetch implementation * (the default `globalThis.fetch` on Expo SDK 56+). expo/fetch is backed by a * native module and bypasses `XMLHttpRequest`, so it is invisible to * `XHRInterceptor`. This wraps it so the networking plugin can report * expo/fetch traffic the same way it reports XHR traffic. * * The shape mirrors ./xhr-interceptor (set*Callback / enableInterception / * disableInterception) so `networking.ts` can wire Reactotron into it the same * way. On platforms/SDKs where the global fetch is not expo/fetch (e.g. RN's * XHR-backed fetch, which is already covered by `XHRInterceptor`), this is a * no-op. */ // Expo stamps its installed globals (see expo's `installGlobal`) with this symbol. const EXPO_BUILTIN = Symbol.for("expo.builtin"); /** * Invoked synchronously as soon as the response resolves, BEFORE the response * is returned to the caller. To read the body, clone `response` synchronously * inside the callback (do not `await` before cloning) so the caller's copy is * left intact and streaming responses are not blocked. `response` is null when * the request rejected (network error); `error` then holds the rejection. */ let openCallback; let responseCallback; let originalFetch = null; let wrappedFetch = null; let previousGlobalFetch = null; let wrapperState = null; let requestId = 0; function isExpoFetch(fn) { return typeof fn === "function" && fn[EXPO_BUILTIN] === true; } function isRequest(value) { return typeof Request !== "undefined" && value instanceof Request; } function getUrl(input) { if (typeof input === "string") return input; if (isRequest(input)) return input.url; return String(input); } function getMethod(input, init) { if (isRequest(input) && input.method) return input.method.toUpperCase(); if (init && init.method) return String(init.method).toUpperCase(); return "GET"; } /** * Normalizes a fetch `HeadersInit` / `Headers` into a plain object. */ function headersToObject(headers) { if (!headers) return null; const anyHeaders = headers; if (typeof anyHeaders.forEach === "function" && typeof anyHeaders.get === "function") { const out = {}; anyHeaders.forEach((value, key) => { out[key] = value; }); return out; } if (Array.isArray(headers)) { return headers.reduce((acc, pair) => { if (pair && pair.length === 2) acc[pair[0]] = pair[1]; return acc; }, {}); } if (typeof headers === "object") return { ...headers }; return null; } /** * A network interceptor for Expo's expo/fetch. Mirrors `XHRInterceptor` so the * networking plugin can register callbacks and enable/disable it identically. */ export const FetchInterceptor = { /** * Invoked synchronously before the wrapped fetch is sent. */ setOpenCallback(callback) { openCallback = callback; }, /** * Invoked synchronously when the response resolves (or rejects). See the * callback type for the body-cloning contract. */ setResponseCallback(callback) { responseCallback = callback; }, isInterceptorEnabled() { return originalFetch !== null; }, /** * Wraps the global fetch and reassigns `globalThis.fetch`. * * With no argument, only expo/fetch is wrapped (detected by the * `expo.builtin` symbol). RN's XHR-backed fetch is already covered by * `XHRInterceptor`, and wrapping it here too would double-report. * * Pass `fetchToWrap` to skip the detection and wrap that function instead — * for runtimes where the expo/fetch global has been re-wrapped and lost the * symbol (e.g. expo-router's window.location polyfill). The caller is * asserting the function does not go through XMLHttpRequest. */ enableInterception(fetchToWrap) { if (originalFetch) { return; } const current = fetchToWrap ?? globalThis.fetch; if (!current || current.__reactotronWrapped || !fetchToWrap && !isExpoFetch(current)) { return; } originalFetch = current; previousGlobalFetch = globalThis.fetch; // Closed over (rather than reading module state) so the wrapper keeps // working as a plain pass-through even after disableInterception, when a // third party has wrapped fetch on top of us and we can't restore the global. const original = current; const state = { stopped: false }; const wrapped = function (input, init) { if (state.stopped) { return original(input, init); } const id = requestId += 1; // Reactotron-internal failures must never alter the app's fetch — parse // and report inside try/catch, and always defer to the real fetch. try { if (openCallback) { const requestHeaders = headersToObject(init && init.headers || (isRequest(input) ? input.headers : null)); const data = init && typeof init.body === "string" ? init.body : init && init.body ? "[non-string body]" : null; openCallback(getMethod(input, init), getUrl(input), requestHeaders, data, id); } } catch (instrumentationError) { // swallow: reporting is best-effort, the request itself must proceed } return original(input, init).then(response => { // Fire synchronously and return the original response untouched, so // the caller is never blocked and streaming bodies stay intact. try { if (responseCallback) { responseCallback(id, response.status, headersToObject(response.headers), response, null); } } catch (instrumentationError) { // swallow: a reporting failure must not reject a successful response } return response; }, error => { try { if (responseCallback) { responseCallback(id, -1, null, null, error); } } catch (instrumentationError) { // swallow: the app's own network error must propagate unchanged } throw error; }); }; wrapped.__reactotronWrapped = true; // Keep it detectable as the expo builtin fetch for anything else that checks. if (isExpoFetch(current)) { wrapped[EXPO_BUILTIN] = true; } wrappedFetch = wrapped; wrapperState = state; globalThis.fetch = wrapped; }, // Unpatch the global fetch and remove the callbacks. If something else has // wrapped fetch on top of us since, the global is left alone and our wrapper // just goes inert (pass-through) — ripping out a later wrapper isn't ours to do. disableInterception() { if (!originalFetch) { return; } if (wrapperState) { wrapperState.stopped = true; } if (globalThis.fetch === wrappedFetch && previousGlobalFetch) { // Restore what was global when we wrapped — not necessarily the wrapped // function itself (an explicitly passed fetch may never have been global). globalThis.fetch = previousGlobalFetch; } originalFetch = null; wrappedFetch = null; previousGlobalFetch = null; wrapperState = null; openCallback = null; responseCallback = null; } }; //# sourceMappingURL=fetch-interceptor.js.map