runHttpQuery.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { chooseContentTypeForSingleResultResponse, internalExecuteOperation, MEDIA_TYPES, } from './ApolloServer.js';
  2. import { Kind } from 'graphql';
  3. import { BadRequestError } from './internalErrorClasses.js';
  4. import Negotiator from 'negotiator';
  5. import { HeaderMap } from './utils/HeaderMap.js';
  6. function fieldIfString(o, fieldName) {
  7. const value = o[fieldName];
  8. if (typeof value === 'string') {
  9. return value;
  10. }
  11. return undefined;
  12. }
  13. function searchParamIfSpecifiedOnce(searchParams, paramName) {
  14. const values = searchParams.getAll(paramName);
  15. switch (values.length) {
  16. case 0:
  17. return undefined;
  18. case 1:
  19. return values[0];
  20. default:
  21. throw new BadRequestError(`The '${paramName}' search parameter may only be specified once.`);
  22. }
  23. }
  24. function jsonParsedSearchParamIfSpecifiedOnce(searchParams, fieldName) {
  25. const value = searchParamIfSpecifiedOnce(searchParams, fieldName);
  26. if (value === undefined) {
  27. return undefined;
  28. }
  29. let hopefullyRecord;
  30. try {
  31. hopefullyRecord = JSON.parse(value);
  32. }
  33. catch {
  34. throw new BadRequestError(`The ${fieldName} search parameter contains invalid JSON.`);
  35. }
  36. if (!isStringRecord(hopefullyRecord)) {
  37. throw new BadRequestError(`The ${fieldName} search parameter should contain a JSON-encoded object.`);
  38. }
  39. return hopefullyRecord;
  40. }
  41. function fieldIfRecord(o, fieldName) {
  42. const value = o[fieldName];
  43. if (isStringRecord(value)) {
  44. return value;
  45. }
  46. return undefined;
  47. }
  48. function isStringRecord(o) {
  49. return (!!o && typeof o === 'object' && !Buffer.isBuffer(o) && !Array.isArray(o));
  50. }
  51. function isNonEmptyStringRecord(o) {
  52. return isStringRecord(o) && Object.keys(o).length > 0;
  53. }
  54. function ensureQueryIsStringOrMissing(query) {
  55. if (!query || typeof query === 'string') {
  56. return;
  57. }
  58. if (query.kind === Kind.DOCUMENT) {
  59. throw new BadRequestError("GraphQL queries must be strings. It looks like you're sending the " +
  60. 'internal graphql-js representation of a parsed query in your ' +
  61. 'request instead of a request in the GraphQL query language. You ' +
  62. 'can convert an AST to a string using the `print` function from ' +
  63. '`graphql`, or use a client like `apollo-client` which converts ' +
  64. 'the internal representation to a string for you.');
  65. }
  66. else {
  67. throw new BadRequestError('GraphQL queries must be strings.');
  68. }
  69. }
  70. export async function runHttpQuery({ server, httpRequest, contextValue, schemaDerivedData, internals, sharedResponseHTTPGraphQLHead, }) {
  71. let graphQLRequest;
  72. switch (httpRequest.method) {
  73. case 'POST': {
  74. if (!isNonEmptyStringRecord(httpRequest.body)) {
  75. throw new BadRequestError('POST body missing, invalid Content-Type, or JSON object has no keys.');
  76. }
  77. ensureQueryIsStringOrMissing(httpRequest.body.query);
  78. if (typeof httpRequest.body.variables === 'string') {
  79. throw new BadRequestError('`variables` in a POST body should be provided as an object, not a recursively JSON-encoded string.');
  80. }
  81. if (typeof httpRequest.body.extensions === 'string') {
  82. throw new BadRequestError('`extensions` in a POST body should be provided as an object, not a recursively JSON-encoded string.');
  83. }
  84. if ('extensions' in httpRequest.body &&
  85. httpRequest.body.extensions !== null &&
  86. !isStringRecord(httpRequest.body.extensions)) {
  87. throw new BadRequestError('`extensions` in a POST body must be an object if provided.');
  88. }
  89. if ('variables' in httpRequest.body &&
  90. httpRequest.body.variables !== null &&
  91. !isStringRecord(httpRequest.body.variables)) {
  92. throw new BadRequestError('`variables` in a POST body must be an object if provided.');
  93. }
  94. if ('operationName' in httpRequest.body &&
  95. httpRequest.body.operationName !== null &&
  96. typeof httpRequest.body.operationName !== 'string') {
  97. throw new BadRequestError('`operationName` in a POST body must be a string if provided.');
  98. }
  99. graphQLRequest = {
  100. query: fieldIfString(httpRequest.body, 'query'),
  101. operationName: fieldIfString(httpRequest.body, 'operationName'),
  102. variables: fieldIfRecord(httpRequest.body, 'variables'),
  103. extensions: fieldIfRecord(httpRequest.body, 'extensions'),
  104. http: httpRequest,
  105. };
  106. break;
  107. }
  108. case 'GET': {
  109. const searchParams = new URLSearchParams(httpRequest.search);
  110. graphQLRequest = {
  111. query: searchParamIfSpecifiedOnce(searchParams, 'query'),
  112. operationName: searchParamIfSpecifiedOnce(searchParams, 'operationName'),
  113. variables: jsonParsedSearchParamIfSpecifiedOnce(searchParams, 'variables'),
  114. extensions: jsonParsedSearchParamIfSpecifiedOnce(searchParams, 'extensions'),
  115. http: httpRequest,
  116. };
  117. break;
  118. }
  119. default:
  120. throw new BadRequestError('Apollo Server supports only GET/POST requests.', {
  121. extensions: {
  122. http: {
  123. status: 405,
  124. headers: new HeaderMap([['allow', 'GET, POST']]),
  125. },
  126. },
  127. });
  128. }
  129. const graphQLResponse = await internalExecuteOperation({
  130. server,
  131. graphQLRequest,
  132. internals,
  133. schemaDerivedData,
  134. sharedResponseHTTPGraphQLHead,
  135. }, { contextValue });
  136. if (graphQLResponse.body.kind === 'single') {
  137. if (!graphQLResponse.http.headers.get('content-type')) {
  138. const contentType = chooseContentTypeForSingleResultResponse(httpRequest);
  139. if (contentType === null) {
  140. throw new BadRequestError(`An 'accept' header was provided for this request which does not accept ` +
  141. `${MEDIA_TYPES.APPLICATION_JSON} or ${MEDIA_TYPES.APPLICATION_GRAPHQL_RESPONSE_JSON}`, { extensions: { http: { status: 406 } } });
  142. }
  143. graphQLResponse.http.headers.set('content-type', contentType);
  144. }
  145. return {
  146. ...graphQLResponse.http,
  147. body: {
  148. kind: 'complete',
  149. string: await internals.stringifyResult(orderExecutionResultFields(graphQLResponse.body.singleResult)),
  150. },
  151. };
  152. }
  153. const acceptHeader = httpRequest.headers.get('accept');
  154. if (!(acceptHeader &&
  155. new Negotiator({
  156. headers: { accept: httpRequest.headers.get('accept') },
  157. }).mediaType([
  158. MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC,
  159. MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL,
  160. ]) === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL)) {
  161. throw new BadRequestError('Apollo server received an operation that uses incremental delivery ' +
  162. '(@defer or @stream), but the client does not accept multipart/mixed ' +
  163. 'HTTP responses. To enable incremental delivery support, add the HTTP ' +
  164. "header 'Accept: multipart/mixed; deferSpec=20220824'.", { extensions: { http: { status: 406 } } });
  165. }
  166. graphQLResponse.http.headers.set('content-type', 'multipart/mixed; boundary="-"; deferSpec=20220824');
  167. return {
  168. ...graphQLResponse.http,
  169. body: {
  170. kind: 'chunked',
  171. asyncIterator: writeMultipartBody(graphQLResponse.body.initialResult, graphQLResponse.body.subsequentResults),
  172. },
  173. };
  174. }
  175. async function* writeMultipartBody(initialResult, subsequentResults) {
  176. yield `\r\n---\r\ncontent-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify(orderInitialIncrementalExecutionResultFields(initialResult))}\r\n---${initialResult.hasNext ? '' : '--'}\r\n`;
  177. for await (const result of subsequentResults) {
  178. yield `content-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify(orderSubsequentIncrementalExecutionResultFields(result))}\r\n---${result.hasNext ? '' : '--'}\r\n`;
  179. }
  180. }
  181. function orderExecutionResultFields(result) {
  182. return {
  183. errors: result.errors,
  184. data: result.data,
  185. extensions: result.extensions,
  186. };
  187. }
  188. function orderInitialIncrementalExecutionResultFields(result) {
  189. return {
  190. hasNext: result.hasNext,
  191. errors: result.errors,
  192. data: result.data,
  193. incremental: orderIncrementalResultFields(result.incremental),
  194. extensions: result.extensions,
  195. };
  196. }
  197. function orderSubsequentIncrementalExecutionResultFields(result) {
  198. return {
  199. hasNext: result.hasNext,
  200. incremental: orderIncrementalResultFields(result.incremental),
  201. extensions: result.extensions,
  202. };
  203. }
  204. function orderIncrementalResultFields(incremental) {
  205. return incremental?.map((i) => ({
  206. hasNext: i.hasNext,
  207. errors: i.errors,
  208. path: i.path,
  209. label: i.label,
  210. data: i.data,
  211. items: i.items,
  212. extensions: i.extensions,
  213. }));
  214. }
  215. export function prettyJSONStringify(value) {
  216. return JSON.stringify(value) + '\n';
  217. }
  218. export function newHTTPGraphQLHead(status) {
  219. return {
  220. status,
  221. headers: new HeaderMap(),
  222. };
  223. }
  224. export function mergeHTTPGraphQLHead(target, source) {
  225. if (source.status) {
  226. target.status = source.status;
  227. }
  228. if (source.headers) {
  229. for (const [name, value] of source.headers) {
  230. target.headers.set(name, value);
  231. }
  232. }
  233. }
  234. //# sourceMappingURL=runHttpQuery.js.map