@graphql-tools/executor-http
Version:
A set of utils for faster development of GraphQL tools
243 lines (242 loc) • 10.2 kB
JavaScript
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 };