Repository: apollographql/apollo-client

Problem statement:
Unhandled promise rejection when error in polling query on SSR
**How to reproduce the issue:**
1. Setup SSR
2. Setup query polling:
```js

export default graphql('query Samples { ... }', {
    options: {
        pollInterval: 5000,
        forceFetch: true,
        notifyOnNetworkStatusChange: true
    }
})(PageLabSamples);
```
3. Throw error in `Samples` query's resolver.

**Intended outcome:**
Error thrown once.

**Actual outcome:**
Error thrown twice: one for SSR and another after polling interval. The problem is second throw is not catchable so it shuts down whole server.

apollo-client 0.9.0


Hints:
The only "fix" for this I've came up is to turn off polling on SSR:
```js
const isBrowser = typeof window !== 'undefined';

export default graphql(SamplesQuery, {
    options: function() {
        return isBrowser
            ? {
                pollInterval: 5000,
                forceFetch: true,
                notifyOnNetworkStatusChange: true
            }
            : {};
    }
})(PageLabSamples);
```

Maybe it's valid fix for whole apollo-client? 😄 
We should not be polling when server-side rendering, especially considering that the React components don’t even mount.

See https://github.com/apollographql/react-apollo/pull/493 which should fix the issue. Tell me if that change solves it for you.
@DimitryDushkin Could you make a PR that checks the `ssrMode` flag before starting to poll, and just not poll when it's `true`? That way this problem could be solved internally to Apollo Client.
@helfer Sure, I'll try to make PR on this week.

Patch:
diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts
--- a/src/ApolloClient.ts
+++ b/src/ApolloClient.ts
@@ -136,6 +136,7 @@ export default class ApolloClient implements DataProxy {
   private devToolsHookCb: Function;
   private proxy: DataProxy | undefined;
   private fragmentMatcher: FragmentMatcherInterface;
+  private ssrMode: boolean;
 
   /**
    * Constructs an instance of {@link ApolloClient}.
@@ -215,6 +216,7 @@ export default class ApolloClient implements DataProxy {
     this.dataId = dataIdFromObject = dataIdFromObject || defaultDataIdFromObject;
     this.fieldWithArgs = storeKeyNameFromFieldNameAndArgs;
     this.queryDeduplication = queryDeduplication;
+    this.ssrMode = ssrMode;
 
     if (ssrForceFetchDelay) {
       setTimeout(() => this.disableNetworkFetches = false, ssrForceFetchDelay);
@@ -522,6 +524,7 @@ export default class ApolloClient implements DataProxy {
       reducerConfig: this.reducerConfig,
       queryDeduplication: this.queryDeduplication,
       fragmentMatcher: this.fragmentMatcher,
+      ssrMode: this.ssrMode,
     });
   }
 
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -132,6 +132,7 @@ export class QueryManager {
   public scheduler: QueryScheduler;
   public store: ApolloStore;
   public networkInterface: NetworkInterface;
+  public ssrMode: boolean;
 
   private addTypename: boolean;
   private deduplicator: Deduplicator;
@@ -177,6 +178,7 @@ export class QueryManager {
     fragmentMatcher,
     addTypename = true,
     queryDeduplication = false,
+    ssrMode = false,
   }: {
     networkInterface: NetworkInterface,
     store: ApolloStore,
@@ -185,6 +187,7 @@ export class QueryManager {
     reducerConfig?: ApolloReducerConfig,
     addTypename?: boolean,
     queryDeduplication?: boolean,
+    ssrMode?: boolean,
   }) {
     // XXX this might be the place to do introspection for inserting the `id` into the query? or
     // is that the network interface?
@@ -198,6 +201,7 @@ export class QueryManager {
     this.queryDocuments = {};
     this.addTypename = addTypename;
     this.queryDeduplication = queryDeduplication;
+    this.ssrMode = ssrMode;
 
     // XXX This logic is duplicated in ApolloClient.ts for two reasons:
     // 1. we need it in ApolloClient.ts for readQuery and readFragment of the data proxy.
diff --git a/src/scheduler/scheduler.ts b/src/scheduler/scheduler.ts
--- a/src/scheduler/scheduler.ts
+++ b/src/scheduler/scheduler.ts
@@ -80,6 +80,11 @@ export class QueryScheduler {
       throw new Error('Attempted to start a polling query without a polling interval.');
     }
 
+    if (this.queryManager.ssrMode) {
+      // Do not poll in SSR mode
+      return queryId;
+    }
+
     this.registeredQueries[queryId] = options;
 
     if (listener) {


Test patch:
diff --git a/test/QueryManager.ts b/test/QueryManager.ts
--- a/test/QueryManager.ts
+++ b/test/QueryManager.ts
@@ -1711,6 +1711,7 @@ describe('QueryManager', () => {
   });
 
   describe('polling queries', () => {
+
     it('allows you to poll queries', () => {
       const query = gql`
         query fetchLuke($id: String) {
@@ -1735,6 +1736,7 @@ describe('QueryManager', () => {
           name: 'Luke Skywalker has a new name',
         },
       };
+
       const queryManager = mockQueryManager(
         {
           request: { query, variables },
@@ -1759,6 +1761,81 @@ describe('QueryManager', () => {
 
     });
 
+    it('does not poll during SSR', (done) => {
+      const query = gql`
+        query fetchLuke($id: String) {
+          people_one(id: $id) {
+            name
+          }
+        }
+      `;
+
+      const variables = {
+        id: '1',
+      };
+
+      const data1 = {
+        people_one: {
+          name: 'Luke Skywalker',
+        },
+      };
+
+      const data2 = {
+        people_one: {
+          name: 'Luke Skywalker has a new name',
+        },
+      };
+
+      const queryManager = new QueryManager({
+        networkInterface: mockNetworkInterface({
+          request: { query, variables },
+          result: { data: data1 },
+        },
+        {
+          request: { query, variables },
+          result: { data: data2 },
+        },
+        {
+          request: { query, variables },
+          result: { data: data2 },
+        }),
+        store: createApolloStore(),
+        reduxRootSelector: defaultReduxRootSelector,
+        addTypename: false,
+        ssrMode: true,
+      });
+
+      const observable = queryManager.watchQuery<any>({
+        query,
+        variables,
+        pollInterval: 10,
+        notifyOnNetworkStatusChange: false,
+      });
+
+      let count = 1;
+      let doneCalled = false;
+      const subHandle = observable.subscribe({
+        next: (result: any) => {
+          switch (count) {
+            case 1:
+              assert.deepEqual(result.data, data1);
+              setTimeout(() => {
+                subHandle.unsubscribe();
+                if (!doneCalled) {
+                  done();
+                }
+              }, 15);
+              count++;
+              break;
+            case 2:
+            default:
+              doneCalled = true;
+              done(new Error('Only expected one result, not multiple'));
+          }
+        },
+      });
+    });
+
     it('should let you handle multiple polled queries and unsubscribe from one of them', (done) => {
       const query1 = gql`
         query {