requestPipeline.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { createHash } from '@apollo/utils.createhash';
  2. import { specifiedRules, getOperationAST, GraphQLError, validate, parse, Kind, } from 'graphql';
  3. import { symbolExecutionDispatcherWillResolveField, enablePluginsForSchemaResolvers, symbolUserFieldResolver, } from './utils/schemaInstrumentation.js';
  4. import { PersistedQueryNotSupportedError, PersistedQueryNotFoundError, UserInputError, BadRequestError, ValidationError, SyntaxError, OperationResolutionError, } from './internalErrorClasses.js';
  5. import { ensureError, normalizeAndFormatErrors, ensureGraphQLError, } from './errorNormalize.js';
  6. import { invokeDidStartHook, invokeHooksUntilDefinedAndNonNull, invokeSyncDidStartHook, } from './utils/invokeHooks.js';
  7. import { makeGatewayGraphQLRequestContext } from './utils/makeGatewayGraphQLRequestContext.js';
  8. import { mergeHTTPGraphQLHead, newHTTPGraphQLHead } from './runHttpQuery.js';
  9. import { isDefined } from './utils/isDefined.js';
  10. import { executeIncrementally, } from './incrementalDeliveryPolyfill.js';
  11. import { HeaderMap } from './utils/HeaderMap.js';
  12. export const APQ_CACHE_PREFIX = 'apq:';
  13. function computeQueryHash(query) {
  14. return createHash('sha256').update(query).digest('hex');
  15. }
  16. function isBadUserInputGraphQLError(error) {
  17. return (error.nodes?.length === 1 &&
  18. error.nodes[0].kind === Kind.VARIABLE_DEFINITION &&
  19. (error.message.startsWith(`Variable "$${error.nodes[0].variable.name.value}" got invalid value `) ||
  20. error.message.startsWith(`Variable "$${error.nodes[0].variable.name.value}" of required type `) ||
  21. error.message.startsWith(`Variable "$${error.nodes[0].variable.name.value}" of non-null type `)));
  22. }
  23. export async function processGraphQLRequest(schemaDerivedData, server, internals, requestContext) {
  24. const requestListeners = (await Promise.all(internals.plugins.map((p) => p.requestDidStart?.(requestContext)))).filter(isDefined);
  25. const request = requestContext.request;
  26. let { query, extensions } = request;
  27. let queryHash;
  28. requestContext.metrics.persistedQueryHit = false;
  29. requestContext.metrics.persistedQueryRegister = false;
  30. if (extensions?.persistedQuery) {
  31. if (!internals.persistedQueries) {
  32. return await sendErrorResponse([new PersistedQueryNotSupportedError()]);
  33. }
  34. else if (extensions.persistedQuery.version !== 1) {
  35. return await sendErrorResponse([
  36. new GraphQLError('Unsupported persisted query version', {
  37. extensions: { http: newHTTPGraphQLHead(400) },
  38. }),
  39. ]);
  40. }
  41. queryHash = extensions.persistedQuery.sha256Hash;
  42. if (query === undefined) {
  43. query = await internals.persistedQueries.cache.get(queryHash);
  44. if (query) {
  45. requestContext.metrics.persistedQueryHit = true;
  46. }
  47. else {
  48. return await sendErrorResponse([new PersistedQueryNotFoundError()]);
  49. }
  50. }
  51. else {
  52. const computedQueryHash = computeQueryHash(query);
  53. if (queryHash !== computedQueryHash) {
  54. return await sendErrorResponse([
  55. new GraphQLError('provided sha does not match query', {
  56. extensions: { http: newHTTPGraphQLHead(400) },
  57. }),
  58. ]);
  59. }
  60. requestContext.metrics.persistedQueryRegister = true;
  61. }
  62. }
  63. else if (query) {
  64. queryHash = computeQueryHash(query);
  65. }
  66. else {
  67. return await sendErrorResponse([
  68. new BadRequestError('GraphQL operations must contain a non-empty `query` or a `persistedQuery` extension.'),
  69. ]);
  70. }
  71. requestContext.queryHash = queryHash;
  72. requestContext.source = query;
  73. await Promise.all(requestListeners.map((l) => l.didResolveSource?.(requestContext)));
  74. if (schemaDerivedData.documentStore) {
  75. try {
  76. requestContext.document = await schemaDerivedData.documentStore.get(schemaDerivedData.documentStoreKeyPrefix + queryHash);
  77. }
  78. catch (err) {
  79. server.logger.warn('An error occurred while attempting to read from the documentStore. ' +
  80. ensureError(err).message);
  81. }
  82. }
  83. if (!requestContext.document) {
  84. const parsingDidEnd = await invokeDidStartHook(requestListeners, async (l) => l.parsingDidStart?.(requestContext));
  85. try {
  86. requestContext.document = parse(query, internals.parseOptions);
  87. }
  88. catch (syntaxMaybeError) {
  89. const error = ensureError(syntaxMaybeError);
  90. await parsingDidEnd(error);
  91. return await sendErrorResponse([
  92. new SyntaxError(ensureGraphQLError(error)),
  93. ]);
  94. }
  95. await parsingDidEnd();
  96. if (internals.dangerouslyDisableValidation !== true) {
  97. const validationDidEnd = await invokeDidStartHook(requestListeners, async (l) => l.validationDidStart?.(requestContext));
  98. const validationErrors = validate(schemaDerivedData.schema, requestContext.document, [...specifiedRules, ...internals.validationRules]);
  99. if (validationErrors.length === 0) {
  100. await validationDidEnd();
  101. }
  102. else {
  103. await validationDidEnd(validationErrors);
  104. return await sendErrorResponse(validationErrors.map((error) => new ValidationError(error)));
  105. }
  106. }
  107. if (schemaDerivedData.documentStore) {
  108. Promise.resolve(schemaDerivedData.documentStore.set(schemaDerivedData.documentStoreKeyPrefix + queryHash, requestContext.document)).catch((err) => server.logger.warn('Could not store validated document. ' + err?.message || err));
  109. }
  110. }
  111. const operation = getOperationAST(requestContext.document, request.operationName);
  112. requestContext.operation = operation || undefined;
  113. requestContext.operationName = operation?.name?.value || null;
  114. if (request.http?.method === 'GET' &&
  115. operation?.operation &&
  116. operation.operation !== 'query') {
  117. return await sendErrorResponse([
  118. new BadRequestError(`GET requests only support query operations, not ${operation.operation} operations`, {
  119. extensions: {
  120. http: { status: 405, headers: new HeaderMap([['allow', 'POST']]) },
  121. },
  122. }),
  123. ]);
  124. }
  125. try {
  126. await Promise.all(requestListeners.map((l) => l.didResolveOperation?.(requestContext)));
  127. }
  128. catch (err) {
  129. return await sendErrorResponse([ensureGraphQLError(err)]);
  130. }
  131. if (requestContext.metrics.persistedQueryRegister &&
  132. internals.persistedQueries) {
  133. const ttl = internals.persistedQueries?.ttl;
  134. Promise.resolve(internals.persistedQueries.cache.set(queryHash, query, ttl !== undefined
  135. ? { ttl: internals.persistedQueries?.ttl }
  136. : undefined)).catch(server.logger.warn);
  137. }
  138. const responseFromPlugin = await invokeHooksUntilDefinedAndNonNull(requestListeners, async (l) => await l.responseForOperation?.(requestContext));
  139. if (responseFromPlugin !== null) {
  140. requestContext.response.body = responseFromPlugin.body;
  141. mergeHTTPGraphQLHead(requestContext.response.http, responseFromPlugin.http);
  142. }
  143. else {
  144. const executionListeners = (await Promise.all(requestListeners.map((l) => l.executionDidStart?.(requestContext)))).filter(isDefined);
  145. executionListeners.reverse();
  146. if (executionListeners.some((l) => l.willResolveField)) {
  147. const invokeWillResolveField = (...args) => invokeSyncDidStartHook(executionListeners, (l) => l.willResolveField?.(...args));
  148. Object.defineProperty(requestContext.contextValue, symbolExecutionDispatcherWillResolveField, { value: invokeWillResolveField });
  149. if (internals.fieldResolver) {
  150. Object.defineProperty(requestContext.contextValue, symbolUserFieldResolver, {
  151. value: internals.fieldResolver,
  152. });
  153. }
  154. enablePluginsForSchemaResolvers(schemaDerivedData.schema);
  155. }
  156. try {
  157. const fullResult = await execute(requestContext);
  158. const result = 'singleResult' in fullResult
  159. ? fullResult.singleResult
  160. : fullResult.initialResult;
  161. if (!requestContext.operation) {
  162. if (!result.errors?.length) {
  163. throw new Error('Unexpected error: Apollo Server did not resolve an operation but execute did not return errors');
  164. }
  165. throw new OperationResolutionError(result.errors[0]);
  166. }
  167. const resultErrors = result.errors?.map((e) => {
  168. if (isBadUserInputGraphQLError(e) && e.extensions?.code == null) {
  169. return new UserInputError(e);
  170. }
  171. return e;
  172. });
  173. if (resultErrors) {
  174. await didEncounterErrors(resultErrors);
  175. }
  176. const { formattedErrors, httpFromErrors } = resultErrors
  177. ? formatErrors(resultErrors)
  178. : { formattedErrors: undefined, httpFromErrors: newHTTPGraphQLHead() };
  179. if (internals.status400ForVariableCoercionErrors &&
  180. resultErrors?.length &&
  181. result.data === undefined &&
  182. !httpFromErrors.status) {
  183. httpFromErrors.status = 400;
  184. }
  185. mergeHTTPGraphQLHead(requestContext.response.http, httpFromErrors);
  186. if ('singleResult' in fullResult) {
  187. requestContext.response.body = {
  188. kind: 'single',
  189. singleResult: {
  190. ...result,
  191. errors: formattedErrors,
  192. },
  193. };
  194. }
  195. else {
  196. requestContext.response.body = {
  197. kind: 'incremental',
  198. initialResult: {
  199. ...fullResult.initialResult,
  200. errors: formattedErrors,
  201. },
  202. subsequentResults: fullResult.subsequentResults,
  203. };
  204. }
  205. }
  206. catch (executionMaybeError) {
  207. const executionError = ensureError(executionMaybeError);
  208. await Promise.all(executionListeners.map((l) => l.executionDidEnd?.(executionError)));
  209. return await sendErrorResponse([ensureGraphQLError(executionError)]);
  210. }
  211. await Promise.all(executionListeners.map((l) => l.executionDidEnd?.()));
  212. }
  213. await invokeWillSendResponse();
  214. if (!requestContext.response.body) {
  215. throw Error('got to end of processGraphQLRequest without setting body?');
  216. }
  217. return requestContext.response;
  218. async function execute(requestContext) {
  219. const { request, document } = requestContext;
  220. if (internals.__testing_incrementalExecutionResults) {
  221. return internals.__testing_incrementalExecutionResults;
  222. }
  223. else if (internals.gatewayExecutor) {
  224. const result = await internals.gatewayExecutor(makeGatewayGraphQLRequestContext(requestContext, server, internals));
  225. return { singleResult: result };
  226. }
  227. else {
  228. const resultOrResults = await executeIncrementally({
  229. schema: schemaDerivedData.schema,
  230. document,
  231. rootValue: typeof internals.rootValue === 'function'
  232. ? internals.rootValue(document)
  233. : internals.rootValue,
  234. contextValue: requestContext.contextValue,
  235. variableValues: request.variables,
  236. operationName: request.operationName,
  237. fieldResolver: internals.fieldResolver,
  238. });
  239. if ('initialResult' in resultOrResults) {
  240. return {
  241. initialResult: resultOrResults.initialResult,
  242. subsequentResults: formatErrorsInSubsequentResults(resultOrResults.subsequentResults),
  243. };
  244. }
  245. else {
  246. return { singleResult: resultOrResults };
  247. }
  248. }
  249. }
  250. async function* formatErrorsInSubsequentResults(results) {
  251. for await (const result of results) {
  252. const payload = result.incremental
  253. ? {
  254. ...result,
  255. incremental: await seriesAsyncMap(result.incremental, async (incrementalResult) => {
  256. const { errors } = incrementalResult;
  257. if (errors) {
  258. await Promise.all(requestListeners.map((l) => l.didEncounterSubsequentErrors?.(requestContext, errors)));
  259. return {
  260. ...incrementalResult,
  261. errors: formatErrors(errors).formattedErrors,
  262. };
  263. }
  264. return incrementalResult;
  265. }),
  266. }
  267. : result;
  268. await Promise.all(requestListeners.map((l) => l.willSendSubsequentPayload?.(requestContext, payload)));
  269. yield payload;
  270. }
  271. }
  272. async function invokeWillSendResponse() {
  273. await Promise.all(requestListeners.map((l) => l.willSendResponse?.(requestContext)));
  274. }
  275. async function didEncounterErrors(errors) {
  276. requestContext.errors = errors;
  277. return await Promise.all(requestListeners.map((l) => l.didEncounterErrors?.(requestContext)));
  278. }
  279. async function sendErrorResponse(errors) {
  280. await didEncounterErrors(errors);
  281. const { formattedErrors, httpFromErrors } = formatErrors(errors);
  282. requestContext.response.body = {
  283. kind: 'single',
  284. singleResult: {
  285. errors: formattedErrors,
  286. },
  287. };
  288. mergeHTTPGraphQLHead(requestContext.response.http, httpFromErrors);
  289. if (!requestContext.response.http.status) {
  290. requestContext.response.http.status = 500;
  291. }
  292. await invokeWillSendResponse();
  293. return requestContext.response;
  294. }
  295. function formatErrors(errors) {
  296. return normalizeAndFormatErrors(errors, {
  297. formatError: internals.formatError,
  298. includeStacktraceInErrorResponses: internals.includeStacktraceInErrorResponses,
  299. });
  300. }
  301. }
  302. async function seriesAsyncMap(ts, fn) {
  303. const us = [];
  304. for (const t of ts) {
  305. const u = await fn(t);
  306. us.push(u);
  307. }
  308. return us;
  309. }
  310. //# sourceMappingURL=requestPipeline.js.map