Search code examples
sslherokugraphqlnext.jsapollo-client

Netlify Apollo NextJS SSR Client network socket disconnected before secure TLS connection was established


I have an application I've built using NextJS that is hosted on Netlify. The API is hosted on Heroku (it's a NestJS project running GraphQL)

In local development mode, I have no problem with any of my SSR pages. However, in production, I continually get 500 errors that produce the following logs in the Netlify functions panel:

ERROR  ApolloError: request to https://api.paladindeck.com/graphql failed, reason: Client network socket disconnected before secure TLS connection was established
    at new ApolloError (/var/task/node_modules/@apollo/client/errors/errors.cjs:34:28)
    at /var/task/node_modules/@apollo/client/core/core.cjs:1598:19
    at both (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:986:53)
    at /var/task/node_modules/@apollo/client/utilities/utilities.cjs:979:72
    at new Promise (<anonymous>)
    at Object.then (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:979:24)
    at Object.error (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:987:49)
    at notifySubscription (/var/task/node_modules/zen-observable/lib/Observable.js:140:18)
    at onNotify (/var/task/node_modules/zen-observable/lib/Observable.js:179:3)
    at SubscriptionObserver.error (/var/task/node_modules/zen-observable/lib/Observable.js:240:7) {
  graphQLErrors: [],
  clientErrors: [],
  networkError: FetchError: request to https://api.paladindeck.com/graphql failed, reason: Client network socket disconnected before secure TLS connection was established
      at ClientRequest.<anonymous> (/var/task/node_modules/next/dist/compiled/node-fetch/index.js:1:64142)
      at ClientRequest.emit (events.js:412:35)
      at ClientRequest.emit (domain.js:475:12)
      at TLSSocket.socketErrorListener (_http_client.js:475:9)
      at TLSSocket.emit (events.js:400:28)
      at TLSSocket.emit (domain.js:475:12)
      at emitErrorNT (internal/streams/destroy.js:106:8)
      at emitErrorCloseNT (internal/streams/destroy.js:74:3)
      at processTicksAndRejections (internal/process/task_queues.js:82:21) {
    type: 'system',
    errno: 'ECONNRESET',
    code: 'ECONNRESET'
  },
  extraInfo: undefined
}

I have attached Sentry to the application and it's capturing some similar information:

http
POST https://api.paladindeck.com/graphql [[undefined]]
Info
09:15:05
console
ApolloError: request to https://api.paladindeck.com/graphql failed, reason: Client network socket disconnected before secure TLS connection was established
    at new ApolloError (/var/task/node_modules/@apollo/client/errors/errors.cjs:34:28)
    at /var/task/node_modules/@apollo/client/core/core.cjs:1598:19
    at both (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:986:53)
    at /var/task/node_modules/@apollo/client/utilities/utilities.cjs:979:72
    at new Promise (<anonymous>)
    at Object.then (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:979:24)
    at Object.error (/var/task/node_modules/@apollo/client/utilities/utilities.cjs:987:49)
    at notifySubscription (/var/task/node_modules/zen-observable/lib/Observable.js:140:18)
    at onNotify (/var/task/node_modules/zen-observable/lib/Observable.js:179:3)
    at SubscriptionObserver.error (/var/task/node_modules/zen-observable/lib/Observable.js:240:7) {
  graphQLErrors: [],
  clientErrors: [],
  networkError: FetchError: request to https://api.paladindeck.com/graphql failed, reason: Client network socket disconnected before secure TLS connection was established
      at ClientRequest.<anonymous> (/var/task/node_modules/next/dist/compiled/node-fetch/index.js:1:64142)
      at ClientRequest.emit (events.js:412:35)
      at ClientRequest.emit (domain.js:475:12)
      at TLSSocket.socketErrorListener (_http_client.js:475:9)
      at TLSSocket.emit (events.js:400:28)
      at TLSSocket.emit (domain.js:475:12)
      at emitErrorNT (internal/streams/destroy.js:106:8)
      at emitErrorCloseNT (internal/streams/destroy.js:74:3)
      at processTicksAndRejections (internal/process/task_queues.js:82:21) {
    type: 'system',
    errno: 'ECONNRESET',
    code: 'ECONNRESET'
  },
  extraInfo: undefined
}
Error
09:15:06
console
[GET] /_next/data/hHiW6IT3wpykwmCV9Cdhe/collections/d45ebedf-d7f1-4208-bfbf-e7aa1af43bd3/e54b8945-6ed0-4094-8c54-fbd42e755e97.json?cardInCollectionId=e54b8945-6ed0-4094-8c54-fbd42e755e97&collectionId=d45ebedf-d7f1-4208-bfbf-e7aa1af43bd3 (SSR)
Info
09:15:06

All of the other pages (which do not use SSR, but query the API) work as expected.

I have looked at other similar issues but none of the solutions have helped thus far.

When I'm unable to find a solution to an issue like this I tend to think I'm doing something very stupid and not realizing it. So, it's entirely possible I'm just missing something so basic I'm not even thinking about it.


Solution

  • Whew... that took me a few days.

    So, it turns out this wasn't a simple thing to diagnose (at least, not for me).

    The short answer to my problem was: Don't pass the context headers from getServerSideProps to the Apollo client. For some reason, those headers, even with the authorization header being appended, were causing something to break.

    Here's what I'm doing now:

    // graphql-client.ts
    export class GraphQLClient {
      private readonly logger = new Logger(GraphQLClient.name);
    
      get value(): ApolloClient<NormalizedCacheObject> {
        if (!this._client) {
          this._client = this.createClient();
        }
        if (this._client === undefined)
          throw new Error(`Error when creating graphql client`);
        return this._client;
      }
    
      constructor(
        private readonly user?: User | null,
        private _client?: ApolloClient<NormalizedCacheObject>,
      ) {}
    
      private createClient(): ApolloClient<NormalizedCacheObject> {
        const isSsrMode = typeof window === 'undefined';
        const httpLink = createHttpLink({ uri: apolloConfig.uri });
        const authLink = setContext(async (_, context) => {
          let token: string | undefined;
          if (context?.headers?.cookie) {
            try {
              token = getCookie(TOKEN_COOKIE_NAME, context.headers.cookie);
            } catch (err) {
              this.logger.error(err);
              token = await this.user?.getIdToken();
            }
          } else {
            token = await this.user?.getIdToken();
          }
    
          const headers = {
            // HERE IS HOW I FIXED THINGS
            // If this is SSR, DO NOT PASS THE REQUEST HEADERS.
            // Just send along the authorization headers.
            // The **correct** headers will be supplied by the `getServerSideProps` invocation of the query.
            ...(!isSsrMode ? context.headers : []),
            authorization: token ? `Bearer ${token}` : ``,
          };
    
          return { headers };
        });
    
        return new ApolloClient({
          link: authLink.concat(httpLink),
          credentials: 'include',
          cache: new InMemoryCache({
            possibleTypes: generatedIntrospection.possibleTypes,
          }),
          ssrMode: isSsrMode,
        });
      }
    }
    
    // mypage.tsx
    ...
    ...
    ...
    
    export const getServerSideProps: GetServerSideProps = async (context) => {
      if (!isCardDetailsPageQueryType(context.query))
        return {
          props: {},
        };
    
      const logger = new Logger(
        `${CardDetailsPage.name}_${getServerSideProps.name}`,
      );
    
      const client = new GraphQLClient();
    
      const GET_CARD_DETAILS_QUERY = gql`
        // query
      `;
    
      const results = await client.value.query({
        query: GET_CARD_DETAILS_QUERY,
        variables: { id: context.query.cardInCollectionId },
        context: {
          headers: {
            ...context.req.headers, // <-- just pass the context headers, the client will automatically append the authorization header
          },
        },
      });
    
      const GET_OTHER_PRINTINGS_BY_NAME_QUERY = gql`
        // query
      `;
      const otherPrintingResults = await client.value.query({
        query: GET_OTHER_PRINTINGS_BY_NAME_QUERY,
        variables: {
          name: results.data.cardsInCollection.card.name,
          collectionId: context.query.collectionId,
        },
        context: {
          headers: {
            ...context.req.headers, // <-- same as above
          },
        },
      });
    
      return {
        props: {
          cardsInCollection: results.data.cardsInCollection,
          otherPrintings: otherPrintingResults.data.otherPrintings,
          allCardsInCollection: otherPrintingResults.data.allCardsInCollection,
        },
      };
    };
    

    This might be a very specific issue for my specific use case, but I do hope someone, someday, finds this helpful.