Netlify Apollo NextJS SSR 客户端网络套接字在建立安全 TLS 连接之前断开连接

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

我有一个使用 NextJS 构建的应用程序,它托管在 Netlify 上。 API 托管在 Heroku 上(这是一个 NestJS 项目 运行 GraphQL)

在本地开发模式下,我的任何SSR页面都没有问题。但是,在生产中,我不断收到 500 个错误,这些错误在 Netlify 函数面板中生成以下日志:

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
}

我已将 Sentry 附加到应用程序,它正在捕获一些类似的信息:

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

所有其他页面(不使用 SSR,但查询 API)都按预期工作。

我已经查看了其他类似的问题,但 none 的解决方案到目前为止有所帮助。

当我找不到像这样的问题的解决方案时,我倾向于认为我在做一些非常愚蠢的事情并且没有意识到。所以,我完全有可能只是错过了一些我什至都没有考虑过的基本东西。

哇...这花了我几天时间。

所以,事实证明这不是一件容易诊断的事情(至少对我来说不是)。

对我的问题的简短回答是:不要将上下文 headers 从 getServerSideProps 传递给 Apollo 客户端。出于某种原因,那些 headers,即使附加了 authorization header,也会导致某些东西中断。

这是我现在正在做的事情:

// 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,
    },
  };
};

对于我的特定用例,这可能是一个非常具体的问题,但我确实希望有一天,有人会觉得这很有帮助。