Repository: apollographql/apollo-client (TypeScript)

Issue:
BatchHTTPLink is not monitoring friendly
### Issue Description

We're trying to integrate Datadog RUM, which attaches headers to outgoing requests if they match certain patterns. When using HTTPLink, the headers are always correctly attached. When using BatchHTTPLink, the headers are not attached. This was previously not working for HTTPLink but was fixed in https://github.com/apollographql/apollo-client/pull/8603, but that same fix was not applied to BatchHTTPLink.

BatchHTTPLink is maintaining a local reference to fetch that monitoring tools cannot inject headers into.

### Link to Reproduction

https://github.com/dbasilio/apollo-dd-rum-repro

### Reproduction Steps

See the readme in the repro repo

### `@apollo/client` version

3.8.10


Discussion:
Related Issues:
- https://github.com/apollographql/apollo-client/issues/7130
- https://github.com/apollographql/apollo-client/issues/7832
Hi @dbasilio 👋 Thanks for opening this issue - the reproduction and linked issues are helpful :) I'll get this on our team agenda, thanks!
Quick update, I have a PR open here: https://github.com/apollographql/apollo-client/pull/11860
Do you have any feedback for the maintainers? Please tell us by taking a [one-minute survey](https://docs.google.com/forms/d/e/1FAIpQLSczNDXfJne3ZUOXjk9Ursm9JYvhTh1_nFTDfdq3XBAFWCzplQ/viewform?usp=pp_url&entry.1170701325=Apollo+Client&entry.204965213=GitHub+Issue). Your responses will help us understand Apollo Client usage and allow us to serve you better.

The fix be included in the next patch release, v3.10.5, which will go out later this week 🚀 
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using [StackOverflow](https://stackoverflow.com/questions/tagged/apollo-client) or our [discord server](https://discord.gg/graphos).



Patch:
diff --git a/.changeset/late-days-give.md b/.changeset/late-days-give.md
new file mode 100644
index 00000000000..4fe36c2d495
--- /dev/null
+++ b/.changeset/late-days-give.md
@@ -0,0 +1,5 @@
+---
+"@apollo/client": patch
+---
+
+Fixes [#11849](https://github.com/apollographql/apollo-client/issues/11849) by reevaluating `window.fetch` each time `BatchHttpLink` uses it, if not configured via `options.fetch`. Takes the same approach as PR [#8603](https://github.com/apollographql/apollo-client/pull/8603) which fixed the same issue in `HttpLink`.
diff --git a/src/link/batch-http/batchHttpLink.ts b/src/link/batch-http/batchHttpLink.ts
index cb80e8d3809..d0412f68cf2 100644
--- a/src/link/batch-http/batchHttpLink.ts
+++ b/src/link/batch-http/batchHttpLink.ts
@@ -3,6 +3,7 @@ import { ApolloLink } from "../core/index.js";
 import {
   Observable,
   hasDirectives,
+  maybe,
   removeClientSetsFromDocument,
 } from "../../utilities/index.js";
 import { fromError } from "../utils/index.js";
@@ -27,6 +28,8 @@ export namespace BatchHttpLink {
     Omit<HttpOptions, "useGETForQueries">;
 }
 
+const backupFetch = maybe(() => fetch);
+
 /**
  * Transforms Operation for into HTTP results.
  * context can include the headers property, which will be passed to the fetch function
@@ -43,7 +46,7 @@ export class BatchHttpLink extends ApolloLink {
     let {
       uri = "/graphql",
       // use default global fetch if nothing is passed in
-      fetch: fetcher,
+      fetch: preferredFetch,
       print = defaultPrinter,
       includeExtensions,
       preserveHeaderCase,
@@ -55,14 +58,10 @@ export class BatchHttpLink extends ApolloLink {
       ...requestOptions
     } = fetchParams || ({} as BatchHttpLink.Options);
 
-    // dev warnings to ensure fetch is present
-    checkFetcher(fetcher);
-
-    //fetcher is set here rather than the destructuring to ensure fetch is
-    //declared before referencing it. Reference in the destructuring would cause
-    //a ReferenceError
-    if (!fetcher) {
-      fetcher = fetch;
+    if (__DEV__) {
+      // Make sure at least one of preferredFetch, window.fetch, or backupFetch
+      // is defined, so requests won't fail at runtime.
+      checkFetcher(preferredFetch || backupFetch);
     }
 
     const linkConfig = {
@@ -163,7 +162,16 @@ export class BatchHttpLink extends ApolloLink {
       }
 
       return new Observable<FetchResult[]>((observer) => {
-        fetcher!(chosenURI, options)
+        // Prefer BatchHttpLink.Options.fetch (preferredFetch) if provided, and
+        // otherwise fall back to the *current* global window.fetch function
+        // (see issue #7832), or (if all else fails) the backupFetch function we
+        // saved when this module was first evaluated. This last option protects
+        // against the removal of window.fetch, which is unlikely but not
+        // impossible.
+        const currentFetch =
+          preferredFetch || maybe(() => fetch) || backupFetch;
+
+        currentFetch!(chosenURI, options)
           .then((response) => {
             // Make the raw response available in the context.
             operations.forEach((operation) =>


Test patch:
diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts
index 6dea1805b89..1209b0414c1 100644
--- a/src/link/batch-http/__tests__/batchHttpLink.ts
+++ b/src/link/batch-http/__tests__/batchHttpLink.ts
@@ -6,6 +6,7 @@ import { ApolloLink } from "../../core/ApolloLink";
 import { execute } from "../../core/execute";
 import {
   Observable,
+  ObservableSubscription,
   Observer,
 } from "../../../utilities/observables/Observable";
 import { BatchHttpLink } from "../batchHttpLink";
@@ -271,6 +272,8 @@ const createHttpLink = (httpArgs?: any) => {
   return new BatchHttpLink(args);
 };
 
+const subscriptions = new Set<ObservableSubscription>();
+
 describe("SharedHttpTest", () => {
   const data = { data: { hello: "world" } };
   const data2 = { data: { hello: "everyone" } };
@@ -300,10 +303,16 @@ describe("SharedHttpTest", () => {
       error,
       complete,
     };
+    subscriptions.clear();
   });
 
   afterEach(() => {
     fetchMock.restore();
+    if (subscriptions.size) {
+      // Tests within this describe block can add subscriptions to this Set
+      // that they want to be canceled/unsubscribed after the test finishes.
+      subscriptions.forEach((sub) => sub.unsubscribe());
+    }
   });
 
   it("raises warning if called with concat", () => {
@@ -624,6 +633,61 @@ describe("SharedHttpTest", () => {
     );
   });
 
+  it("uses the latest window.fetch function if options.fetch not configured", (done) => {
+    const httpLink = createHttpLink({ uri: "data" });
+
+    const fetch = window.fetch;
+    expect(typeof fetch).toBe("function");
+
+    const fetchSpy = jest.spyOn(window, "fetch");
+    fetchSpy.mockImplementation(() =>
+      Promise.resolve<Response>({
+        text() {
+          return Promise.resolve(
+            JSON.stringify({
+              data: { hello: "from spy" },
+            })
+          );
+        },
+      } as Response)
+    );
+
+    const spyFn = window.fetch;
+    expect(spyFn).not.toBe(fetch);
+
+    subscriptions.add(
+      execute(httpLink, {
+        query: sampleQuery,
+      }).subscribe({
+        error: done.fail,
+
+        next(result) {
+          expect(fetchSpy).toHaveBeenCalledTimes(1);
+          expect(result).toEqual({
+            data: { hello: "from spy" },
+          });
+
+          fetchSpy.mockRestore();
+          expect(window.fetch).toBe(fetch);
+
+          subscriptions.add(
+            execute(httpLink, {
+              query: sampleQuery,
+            }).subscribe({
+              error: done.fail,
+              next(result) {
+                expect(result).toEqual({
+                  data: { hello: "world" },
+                });
+                done();
+              },
+            })
+          );
+        },
+      })
+    );
+  });
+
   itAsync(
     "prioritizes context headers over setup headers",
     (resolve, reject) => {