我有一个使用NextJS构建的应用程序,它托管在Netlify上。API托管在Heroku上(这是一个运行GraphQL的NestJS项目(
在本地开发模式下,我对我的任何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(都按预期工作。
我也研究过其他类似的问题,但到目前为止,没有一个解决方案有帮助。
当我无法找到这样一个问题的解决方案时,我倾向于认为我在做一些非常愚蠢的事情,却没有意识到。所以,完全有可能我只是错过了一些基本的东西,甚至没有想过。
Whew。。。那花了我几天时间。
所以,事实证明这不是一件简单的诊断(至少对我来说不是(。
我的问题的简短答案是:不要将上下文标头从getServerSideProps
传递给Apollo客户端。出于某种原因,即使附加了authorization
标头,这些标头也会导致某些内容中断。
以下是我现在正在做的事情:
// 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,
},
};
};
对于我的特定用例来说,这可能是一个非常具体的问题,但我确实希望有一天有人能发现这一点。