UNPKG

@graphql-tools/executor-http

Version:

A set of utils for faster development of GraphQL tools

243 lines (242 loc) • 10.2 kB
import { createGraphQLError, getOperationASTFromRequest, } from '@graphql-tools/utils'; import { print } from 'graphql'; import { isLiveQueryOperationDefinitionNode } from './isLiveQueryOperationDefinitionNode.js'; import { prepareGETUrl } from './prepareGETUrl.js'; import { ValueOrPromise } from 'value-or-promise'; import { createFormDataFromVariables } from './createFormDataFromVariables.js'; import { handleEventStreamResponse } from './handleEventStreamResponse.js'; import { handleMultipartMixedResponse } from './handleMultipartMixedResponse.js'; import { fetch as defaultFetch } from '@whatwg-node/fetch'; export function buildHTTPExecutor(options) { const executor = (request) => { const fetchFn = request.extensions?.fetch ?? options?.fetch ?? defaultFetch; let controller; let method = request.extensions?.method || options?.method || 'POST'; const operationAst = getOperationASTFromRequest(request); const operationType = operationAst.operation; if ((options?.useGETForQueries || request.extensions?.useGETForQueries) && operationType === 'query') { method = 'GET'; } let accept = 'application/graphql-response+json, application/json, multipart/mixed'; if (operationType === 'subscription' || isLiveQueryOperationDefinitionNode(operationAst)) { method = 'GET'; accept = 'text/event-stream'; } const endpoint = request.extensions?.endpoint || options?.endpoint || '/graphql'; const headers = Object.assign({ accept, }, (typeof options?.headers === 'function' ? options.headers(request) : options?.headers) || {}, request.extensions?.headers || {}); const query = print(request.document); const requestBody = { query, variables: request.variables, operationName: request.operationName, extensions: request.extensions, }; let timeoutId; if (options?.timeout) { controller = new AbortController(); timeoutId = setTimeout(() => { if (!controller?.signal.aborted) { controller?.abort('timeout'); } }, options.timeout); } const responseDetailsForError = {}; return new ValueOrPromise(() => { switch (method) { case 'GET': { const finalUrl = prepareGETUrl({ baseUrl: endpoint, ...requestBody, }); return fetchFn(finalUrl, { method: 'GET', ...(options?.credentials != null ? { credentials: options.credentials } : {}), headers, signal: controller?.signal, }, request.context, request.info); } case 'POST': return new ValueOrPromise(() => createFormDataFromVariables(requestBody, { File: options?.File, FormData: options?.FormData, })) .then(body => fetchFn(endpoint, { method: 'POST', ...(options?.credentials != null ? { credentials: options.credentials } : {}), body, headers: { ...headers, ...(typeof body === 'string' ? { 'content-type': 'application/json' } : {}), }, signal: controller?.signal, }, request.context, request.info)) .resolve(); } }) .then((fetchResult) => { responseDetailsForError.status = fetchResult.status; responseDetailsForError.statusText = fetchResult.statusText; if (timeoutId != null) { clearTimeout(timeoutId); } // Retry should respect HTTP Errors if (options?.retry != null && !fetchResult.status.toString().startsWith('2')) { throw new Error(fetchResult.statusText || `HTTP Error: ${fetchResult.status}`); } const contentType = fetchResult.headers.get('content-type'); if (contentType?.includes('text/event-stream')) { return handleEventStreamResponse(fetchResult, controller); } else if (contentType?.includes('multipart/mixed')) { return handleMultipartMixedResponse(fetchResult, controller); } return fetchResult.text(); }) .then(result => { if (typeof result === 'string') { if (result) { try { return JSON.parse(result); } catch (e) { return { errors: [ createGraphQLError(`Unexpected response: ${JSON.stringify(result)}`, { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, originalError: e, }), ], }; } } } else { return result; } }) .catch((e) => { if (typeof e === 'string') { return { errors: [ createGraphQLError(e, { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, }), ], }; } else if (e.name === 'GraphQLError') { return { errors: [e], }; } else if (e.name === 'TypeError' && e.message === 'fetch failed') { return { errors: [ createGraphQLError(`fetch failed to ${endpoint}`, { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, originalError: e, }), ], }; } else if (e.name === 'AbortError' && controller?.signal?.reason) { return { errors: [ createGraphQLError('The operation was aborted. reason: ' + controller.signal.reason, { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, originalError: e, }), ], }; } else if (e.message) { return { errors: [ createGraphQLError(e.message, { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, originalError: e, }), ], }; } else { return { errors: [ createGraphQLError('Unknown error', { extensions: { requestBody: { query, operationName: request.operationName, }, responseDetails: responseDetailsForError, }, originalError: e, }), ], }; } }) .resolve(); }; if (options?.retry != null) { return function retryExecutor(request) { let result; let attempt = 0; function retryAttempt() { attempt++; if (attempt > options.retry) { if (result != null) { return result; } return { errors: [createGraphQLError('No response returned from fetch')], }; } return new ValueOrPromise(() => executor(request)) .then(res => { result = res; if (result?.errors?.length) { return retryAttempt(); } return result; }) .resolve(); } return retryAttempt(); }; } return executor; } export { isLiveQueryOperationDefinitionNode };