@graphql-tools/executor-http
Advanced tools
Comparing version 1.0.3-alpha-20230809181317-aca946d4 to 1.0.3-alpha-20230809204600-c19e0921
@@ -5,3 +5,2 @@ "use strict"; | ||
const extract_files_1 = require("extract-files"); | ||
const value_or_promise_1 = require("value-or-promise"); | ||
const utils_1 = require("@graphql-tools/utils"); | ||
@@ -70,5 +69,4 @@ const fetch_1 = require("@whatwg-node/fetch"); | ||
} | ||
return value_or_promise_1.ValueOrPromise.all(uploads.map((upload, i) => new value_or_promise_1.ValueOrPromise(() => handleUpload(upload, i)))) | ||
.then(() => form) | ||
.resolve(); | ||
return Promise.all(uploads.map((upload, i) => handleUpload(upload, i))) | ||
.then(() => form); | ||
} | ||
@@ -75,0 +73,0 @@ exports.createFormDataFromVariables = createFormDataFromVariables; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.handleEventStreamResponse = exports.isReadableStream = void 0; | ||
exports.isReadableStream = exports.handleEventStreamResponse = void 0; | ||
const utils_1 = require("@graphql-tools/utils"); | ||
const addCancelToResponseStream_js_1 = require("./addCancelToResponseStream.js"); | ||
const handleAsyncIterable_js_1 = require("./handleAsyncIterable.js"); | ||
const handleReadableStream_js_1 = require("./handleReadableStream.js"); | ||
async function* handleEventStreamResponse(response) { | ||
let stream; | ||
const body = response.body; | ||
if (isReadableStream(body)) { | ||
stream = streamToAsyncIterator(body); | ||
} | ||
else if ((0, utils_1.isAsyncIterable)(body)) { | ||
stream = body; | ||
} | ||
else { | ||
throw new Error('Response body is expected to be a readable stream, but got ' + (0, utils_1.inspect)(body)); | ||
} | ||
const decoder = new TextDecoder(); | ||
for await (const rawChunk of stream) { | ||
const chunk = typeof rawChunk === 'string' ? rawChunk : decoder.decode(rawChunk, { stream: true }); | ||
for (const msg of chunk.split('\n\n')) { | ||
const parts = msg.split('\n'); | ||
const eventPart = parts.find(part => part.startsWith('event:')); | ||
const dataPart = parts.find(part => part.startsWith('data:')); | ||
if (!eventPart && !dataPart) { | ||
continue; // ping | ||
} | ||
if (!eventPart) { | ||
throw new Error('Missing stream message event'); | ||
} | ||
const event = eventPart.replace('event:', '').trim(); | ||
if (event === 'complete') { | ||
return; | ||
} | ||
else if (event === 'next') { | ||
if (!dataPart) { | ||
throw new Error(`Missing stream message data for "next" event`); | ||
} | ||
const data = dataPart.replace('data:', '').trim(); | ||
const parsed = JSON.parse(data); | ||
yield parsed.payload || parsed; | ||
} | ||
else { | ||
throw new Error(`Unsupported stream message event "${event}"`); | ||
} | ||
} | ||
} | ||
} | ||
exports.handleEventStreamResponse = handleEventStreamResponse; | ||
function isReadableStream(value) { | ||
return value && typeof value.getReader === 'function'; | ||
return typeof Object(value).getReader === 'function'; | ||
} | ||
exports.isReadableStream = isReadableStream; | ||
function handleEventStreamResponse(response, controller) { | ||
// node-fetch returns body as a promise so we need to resolve it | ||
const body = response.body; | ||
if (body) { | ||
if ((0, utils_1.isAsyncIterable)(body)) { | ||
const resultStream = (0, handleAsyncIterable_js_1.handleAsyncIterable)(body); | ||
return (0, addCancelToResponseStream_js_1.addCancelToResponseStream)(resultStream, controller); | ||
} | ||
if (isReadableStream(body)) { | ||
return (0, handleReadableStream_js_1.handleReadableStream)(body); | ||
} | ||
async function* streamToAsyncIterator(stream) { | ||
const reader = stream.getReader(); | ||
for (;;) { | ||
const { done, value } = await reader.read(); | ||
if (done) | ||
return; | ||
yield value; | ||
} | ||
throw new Error('Response body is expected to be a readable stream but got; ' + (0, utils_1.inspect)(body)); | ||
} | ||
exports.handleEventStreamResponse = handleEventStreamResponse; |
@@ -7,10 +7,7 @@ "use strict"; | ||
const utils_1 = require("@graphql-tools/utils"); | ||
const addCancelToResponseStream_js_1 = require("./addCancelToResponseStream.js"); | ||
function isIncomingMessage(body) { | ||
return body != null && typeof body === 'object' && 'pipe' in body; | ||
} | ||
async function handleMultipartMixedResponse(response, controller) { | ||
// eslint-disable-next-line require-yield -- the returned iterable will yield | ||
async function* handleMultipartMixedResponse(response) { | ||
const body = response.body; | ||
const contentType = response.headers.get('content-type') || ''; | ||
let asyncIterator; | ||
let stream; | ||
if (isIncomingMessage(body)) { | ||
@@ -23,18 +20,16 @@ // Meros/node expects headers as an object map with the content-type prop | ||
const result = await (0, node_1.meros)(body); | ||
if ('next' in result) { | ||
asyncIterator = result; | ||
if (!('next' in result)) { | ||
throw new Error('Multipart mixed response must be iterable'); | ||
} | ||
stream = result; | ||
} | ||
else { | ||
// Nothing is needed for regular `Response`. | ||
const result = await (0, browser_1.meros)(response); | ||
if ('next' in result) { | ||
asyncIterator = result; | ||
if (!('next' in result)) { | ||
throw new Error('Multipart mixed response must be iterable'); | ||
} | ||
stream = result; | ||
} | ||
const executionResult = {}; | ||
if (asyncIterator == null) { | ||
return executionResult; | ||
} | ||
const resultStream = (0, utils_1.mapAsyncIterator)(asyncIterator, (part) => { | ||
const resultStream = (0, utils_1.mapAsyncIterator)(stream, part => { | ||
if (part.json) { | ||
@@ -48,5 +43,11 @@ const incrementalResult = part.body; | ||
} | ||
else { | ||
throw new Error(`Unexpected multipart stream data ${(0, utils_1.inspect)(part)}`); | ||
} | ||
}); | ||
return (0, addCancelToResponseStream_js_1.addCancelToResponseStream)(resultStream, controller); | ||
return resultStream; | ||
} | ||
exports.handleMultipartMixedResponse = handleMultipartMixedResponse; | ||
function isIncomingMessage(body) { | ||
return body != null && typeof body === 'object' && 'pipe' in body; | ||
} |
194
cjs/index.js
@@ -5,3 +5,2 @@ "use strict"; | ||
const graphql_1 = require("graphql"); | ||
const value_or_promise_1 = require("value-or-promise"); | ||
const utils_1 = require("@graphql-tools/utils"); | ||
@@ -16,5 +15,5 @@ const fetch_1 = require("@whatwg-node/fetch"); | ||
function buildHTTPExecutor(options) { | ||
const executor = (request) => { | ||
const executor = async (request) => { | ||
const controller = new AbortController(); | ||
const fetchFn = request.extensions?.fetch ?? options?.fetch ?? fetch_1.fetch; | ||
const controller = new AbortController(); | ||
let method = request.extensions?.method || options?.method || 'POST'; | ||
@@ -37,3 +36,3 @@ const operationAst = (0, utils_1.getOperationASTFromRequest)(request); | ||
const query = (0, graphql_1.print)(request.document); | ||
let timeoutId; | ||
let timeoutId = null; | ||
if (options?.timeout) { | ||
@@ -46,99 +45,97 @@ timeoutId = setTimeout(() => { | ||
} | ||
const responseDetailsForError = {}; | ||
return new value_or_promise_1.ValueOrPromise(() => { | ||
switch (method) { | ||
case 'GET': { | ||
const finalUrl = (0, prepareGETUrl_js_1.prepareGETUrl)({ | ||
baseUrl: endpoint, | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}); | ||
const fetchOptions = { | ||
method: 'GET', | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
return fetchFn(finalUrl, fetchOptions, request.context, request.info); | ||
let fetching; | ||
switch (method) { | ||
case 'GET': { | ||
const finalUrl = (0, prepareGETUrl_js_1.prepareGETUrl)({ | ||
baseUrl: endpoint, | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}); | ||
const fetchOptions = { | ||
method: 'GET', | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
case 'POST': | ||
return new value_or_promise_1.ValueOrPromise(() => (0, createFormDataFromVariables_js_1.createFormDataFromVariables)({ | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}, { | ||
File: options?.File, | ||
FormData: options?.FormData, | ||
})) | ||
.then(body => { | ||
if (typeof body === 'string') { | ||
headers['content-type'] = 'application/json'; | ||
} | ||
const fetchOptions = { | ||
method: 'POST', | ||
body, | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
return fetchFn(endpoint, fetchOptions, request.context, request.info); | ||
}) | ||
.resolve(); | ||
fetching = fetchFn(finalUrl, fetchOptions, request.context, request.info); | ||
break; | ||
} | ||
}) | ||
.then((fetchResult) => { | ||
responseDetailsForError.status = fetchResult.status; | ||
responseDetailsForError.statusText = fetchResult.statusText; | ||
if (timeoutId != null) { | ||
case 'POST': { | ||
const body = await (0, createFormDataFromVariables_js_1.createFormDataFromVariables)({ | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}, { | ||
File: options?.File, | ||
FormData: options?.FormData, | ||
}); | ||
if (typeof body === 'string') { | ||
headers['content-type'] = 'application/json'; | ||
} | ||
const fetchOptions = { | ||
method: 'POST', | ||
body, | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
fetching = fetchFn(endpoint, fetchOptions, request.context, request.info); | ||
break; | ||
} | ||
default: | ||
throw new Error(`Unsupported request method "${method}"`); | ||
} | ||
const responseDetailsForError = {}; | ||
try { | ||
const response = await fetching; | ||
responseDetailsForError.status = response.status; | ||
responseDetailsForError.statusText = response.statusText; | ||
if (timeoutId) { | ||
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}`); | ||
if (options?.retry != null && !response.status.toString().startsWith('2')) { | ||
throw new Error(response.statusText || `HTTP Error: ${response.status}`); | ||
} | ||
const contentType = fetchResult.headers.get('content-type'); | ||
let result; | ||
const contentType = response.headers.get('content-type'); | ||
if (contentType?.includes('text/event-stream')) { | ||
return (0, handleEventStreamResponse_js_1.handleEventStreamResponse)(fetchResult, controller); | ||
result = (0, handleEventStreamResponse_js_1.handleEventStreamResponse)(response); | ||
} | ||
else if (contentType?.includes('multipart/mixed')) { | ||
return (0, handleMultipartMixedResponse_js_1.handleMultipartMixedResponse)(fetchResult, controller); | ||
// TODO: should we assert async response? | ||
result = (0, handleMultipartMixedResponse_js_1.handleMultipartMixedResponse)(response); | ||
} | ||
return fetchResult.text(); | ||
}) | ||
.then(result => { | ||
if (typeof result === 'string') { | ||
if (result) { | ||
try { | ||
return JSON.parse(result); | ||
} | ||
catch (e) { | ||
return { | ||
errors: [ | ||
(0, utils_1.createGraphQLError)(`Unexpected response: ${JSON.stringify(result)}`, { | ||
extensions: { | ||
requestBody: { | ||
query, | ||
operationName: request.operationName, | ||
}, | ||
responseDetails: responseDetailsForError, | ||
else { | ||
const text = await response.text(); | ||
try { | ||
result = JSON.parse(text); | ||
} | ||
catch (e) { | ||
result = { | ||
errors: [ | ||
(0, utils_1.createGraphQLError)(`Unexpected response: "${text}"`, { | ||
extensions: { | ||
requestBody: { | ||
query, | ||
operationName: request.operationName, | ||
}, | ||
originalError: e, | ||
}), | ||
], | ||
}; | ||
} | ||
responseDetails: responseDetailsForError, | ||
}, | ||
originalError: e, | ||
}), | ||
], | ||
}; | ||
} | ||
} | ||
else { | ||
return result; | ||
} | ||
}) | ||
.catch((e) => { | ||
return result; | ||
} | ||
catch (e) { | ||
if (typeof e === 'string') { | ||
@@ -183,3 +180,3 @@ return { | ||
errors: [ | ||
(0, utils_1.createGraphQLError)('The operation was aborted. reason: ' + controller.signal.reason, { | ||
(0, utils_1.createGraphQLError)('The operation was aborted. Reason: ' + controller.signal.reason, { | ||
extensions: { | ||
@@ -229,4 +226,3 @@ requestBody: { | ||
} | ||
}) | ||
.resolve(); | ||
} | ||
}; | ||
@@ -237,3 +233,3 @@ if (options?.retry != null) { | ||
let attempt = 0; | ||
function retryAttempt() { | ||
async function retryAttempt() { | ||
attempt++; | ||
@@ -248,11 +244,7 @@ if (attempt > options.retry) { | ||
} | ||
return new value_or_promise_1.ValueOrPromise(() => executor(request)) | ||
.then(res => { | ||
result = res; | ||
if (result?.errors?.length) { | ||
return retryAttempt(); | ||
} | ||
return result; | ||
}) | ||
.resolve(); | ||
result = await executor(request); | ||
if (Object(result).errors?.length) { | ||
return retryAttempt(); | ||
} | ||
return result; | ||
} | ||
@@ -259,0 +251,0 @@ return retryAttempt(); |
import { extractFiles, isExtractableFile } from 'extract-files'; | ||
import { ValueOrPromise } from 'value-or-promise'; | ||
import { isAsyncIterable, isPromise } from '@graphql-tools/utils'; | ||
@@ -66,5 +65,4 @@ import { File as DefaultFile, FormData as DefaultFormData } from '@whatwg-node/fetch'; | ||
} | ||
return ValueOrPromise.all(uploads.map((upload, i) => new ValueOrPromise(() => handleUpload(upload, i)))) | ||
.then(() => form) | ||
.resolve(); | ||
return Promise.all(uploads.map((upload, i) => handleUpload(upload, i))) | ||
.then(() => form); | ||
} | ||
@@ -71,0 +69,0 @@ function isBlob(obj) { |
import { inspect, isAsyncIterable } from '@graphql-tools/utils'; | ||
import { addCancelToResponseStream } from './addCancelToResponseStream.js'; | ||
import { handleAsyncIterable } from './handleAsyncIterable.js'; | ||
import { handleReadableStream } from './handleReadableStream.js'; | ||
export function isReadableStream(value) { | ||
return value && typeof value.getReader === 'function'; | ||
} | ||
export function handleEventStreamResponse(response, controller) { | ||
// node-fetch returns body as a promise so we need to resolve it | ||
export async function* handleEventStreamResponse(response) { | ||
let stream; | ||
const body = response.body; | ||
if (body) { | ||
if (isAsyncIterable(body)) { | ||
const resultStream = handleAsyncIterable(body); | ||
return addCancelToResponseStream(resultStream, controller); | ||
if (isReadableStream(body)) { | ||
stream = streamToAsyncIterator(body); | ||
} | ||
else if (isAsyncIterable(body)) { | ||
stream = body; | ||
} | ||
else { | ||
throw new Error('Response body is expected to be a readable stream, but got ' + inspect(body)); | ||
} | ||
const decoder = new TextDecoder(); | ||
for await (const rawChunk of stream) { | ||
const chunk = typeof rawChunk === 'string' ? rawChunk : decoder.decode(rawChunk, { stream: true }); | ||
for (const msg of chunk.split('\n\n')) { | ||
const parts = msg.split('\n'); | ||
const eventPart = parts.find(part => part.startsWith('event:')); | ||
const dataPart = parts.find(part => part.startsWith('data:')); | ||
if (!eventPart && !dataPart) { | ||
continue; // ping | ||
} | ||
if (!eventPart) { | ||
throw new Error('Missing stream message event'); | ||
} | ||
const event = eventPart.replace('event:', '').trim(); | ||
if (event === 'complete') { | ||
return; | ||
} | ||
else if (event === 'next') { | ||
if (!dataPart) { | ||
throw new Error(`Missing stream message data for "next" event`); | ||
} | ||
const data = dataPart.replace('data:', '').trim(); | ||
const parsed = JSON.parse(data); | ||
yield parsed.payload || parsed; | ||
} | ||
else { | ||
throw new Error(`Unsupported stream message event "${event}"`); | ||
} | ||
} | ||
if (isReadableStream(body)) { | ||
return handleReadableStream(body); | ||
} | ||
} | ||
throw new Error('Response body is expected to be a readable stream but got; ' + inspect(body)); | ||
} | ||
export function isReadableStream(value) { | ||
return typeof Object(value).getReader === 'function'; | ||
} | ||
async function* streamToAsyncIterator(stream) { | ||
const reader = stream.getReader(); | ||
for (;;) { | ||
const { done, value } = await reader.read(); | ||
if (done) | ||
return; | ||
yield value; | ||
} | ||
} |
import { meros as merosReadableStream } from 'meros/browser'; | ||
import { meros as merosIncomingMessage } from 'meros/node'; | ||
import { mapAsyncIterator, mergeIncrementalResult } from '@graphql-tools/utils'; | ||
import { addCancelToResponseStream } from './addCancelToResponseStream.js'; | ||
function isIncomingMessage(body) { | ||
return body != null && typeof body === 'object' && 'pipe' in body; | ||
} | ||
export async function handleMultipartMixedResponse(response, controller) { | ||
import { inspect, mapAsyncIterator, mergeIncrementalResult, } from '@graphql-tools/utils'; | ||
// eslint-disable-next-line require-yield -- the returned iterable will yield | ||
export async function* handleMultipartMixedResponse(response) { | ||
const body = response.body; | ||
const contentType = response.headers.get('content-type') || ''; | ||
let asyncIterator; | ||
let stream; | ||
if (isIncomingMessage(body)) { | ||
@@ -19,18 +16,16 @@ // Meros/node expects headers as an object map with the content-type prop | ||
const result = await merosIncomingMessage(body); | ||
if ('next' in result) { | ||
asyncIterator = result; | ||
if (!('next' in result)) { | ||
throw new Error('Multipart mixed response must be iterable'); | ||
} | ||
stream = result; | ||
} | ||
else { | ||
// Nothing is needed for regular `Response`. | ||
const result = await merosReadableStream(response); | ||
if ('next' in result) { | ||
asyncIterator = result; | ||
if (!('next' in result)) { | ||
throw new Error('Multipart mixed response must be iterable'); | ||
} | ||
stream = result; | ||
} | ||
const executionResult = {}; | ||
if (asyncIterator == null) { | ||
return executionResult; | ||
} | ||
const resultStream = mapAsyncIterator(asyncIterator, (part) => { | ||
const resultStream = mapAsyncIterator(stream, part => { | ||
if (part.json) { | ||
@@ -44,4 +39,10 @@ const incrementalResult = part.body; | ||
} | ||
else { | ||
throw new Error(`Unexpected multipart stream data ${inspect(part)}`); | ||
} | ||
}); | ||
return addCancelToResponseStream(resultStream, controller); | ||
return resultStream; | ||
} | ||
function isIncomingMessage(body) { | ||
return body != null && typeof body === 'object' && 'pipe' in body; | ||
} |
194
esm/index.js
import { print } from 'graphql'; | ||
import { ValueOrPromise } from 'value-or-promise'; | ||
import { createGraphQLError, getOperationASTFromRequest, } from '@graphql-tools/utils'; | ||
@@ -11,5 +10,5 @@ import { fetch as defaultFetch } from '@whatwg-node/fetch'; | ||
export function buildHTTPExecutor(options) { | ||
const executor = (request) => { | ||
const executor = async (request) => { | ||
const controller = new AbortController(); | ||
const fetchFn = request.extensions?.fetch ?? options?.fetch ?? defaultFetch; | ||
const controller = new AbortController(); | ||
let method = request.extensions?.method || options?.method || 'POST'; | ||
@@ -32,3 +31,3 @@ const operationAst = getOperationASTFromRequest(request); | ||
const query = print(request.document); | ||
let timeoutId; | ||
let timeoutId = null; | ||
if (options?.timeout) { | ||
@@ -41,99 +40,97 @@ timeoutId = setTimeout(() => { | ||
} | ||
const responseDetailsForError = {}; | ||
return new ValueOrPromise(() => { | ||
switch (method) { | ||
case 'GET': { | ||
const finalUrl = prepareGETUrl({ | ||
baseUrl: endpoint, | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}); | ||
const fetchOptions = { | ||
method: 'GET', | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
return fetchFn(finalUrl, fetchOptions, request.context, request.info); | ||
let fetching; | ||
switch (method) { | ||
case 'GET': { | ||
const finalUrl = prepareGETUrl({ | ||
baseUrl: endpoint, | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}); | ||
const fetchOptions = { | ||
method: 'GET', | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
case 'POST': | ||
return new ValueOrPromise(() => createFormDataFromVariables({ | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}, { | ||
File: options?.File, | ||
FormData: options?.FormData, | ||
})) | ||
.then(body => { | ||
if (typeof body === 'string') { | ||
headers['content-type'] = 'application/json'; | ||
} | ||
const fetchOptions = { | ||
method: 'POST', | ||
body, | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
return fetchFn(endpoint, fetchOptions, request.context, request.info); | ||
}) | ||
.resolve(); | ||
fetching = fetchFn(finalUrl, fetchOptions, request.context, request.info); | ||
break; | ||
} | ||
}) | ||
.then((fetchResult) => { | ||
responseDetailsForError.status = fetchResult.status; | ||
responseDetailsForError.statusText = fetchResult.statusText; | ||
if (timeoutId != null) { | ||
case 'POST': { | ||
const body = await createFormDataFromVariables({ | ||
query, | ||
variables: request.variables, | ||
operationName: request.operationName, | ||
extensions: request.extensions, | ||
}, { | ||
File: options?.File, | ||
FormData: options?.FormData, | ||
}); | ||
if (typeof body === 'string') { | ||
headers['content-type'] = 'application/json'; | ||
} | ||
const fetchOptions = { | ||
method: 'POST', | ||
body, | ||
headers, | ||
signal: controller.signal, | ||
}; | ||
if (options?.credentials != null) { | ||
fetchOptions.credentials = options.credentials; | ||
} | ||
fetching = fetchFn(endpoint, fetchOptions, request.context, request.info); | ||
break; | ||
} | ||
default: | ||
throw new Error(`Unsupported request method "${method}"`); | ||
} | ||
const responseDetailsForError = {}; | ||
try { | ||
const response = await fetching; | ||
responseDetailsForError.status = response.status; | ||
responseDetailsForError.statusText = response.statusText; | ||
if (timeoutId) { | ||
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}`); | ||
if (options?.retry != null && !response.status.toString().startsWith('2')) { | ||
throw new Error(response.statusText || `HTTP Error: ${response.status}`); | ||
} | ||
const contentType = fetchResult.headers.get('content-type'); | ||
let result; | ||
const contentType = response.headers.get('content-type'); | ||
if (contentType?.includes('text/event-stream')) { | ||
return handleEventStreamResponse(fetchResult, controller); | ||
result = handleEventStreamResponse(response); | ||
} | ||
else if (contentType?.includes('multipart/mixed')) { | ||
return handleMultipartMixedResponse(fetchResult, controller); | ||
// TODO: should we assert async response? | ||
result = handleMultipartMixedResponse(response); | ||
} | ||
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, | ||
else { | ||
const text = await response.text(); | ||
try { | ||
result = JSON.parse(text); | ||
} | ||
catch (e) { | ||
result = { | ||
errors: [ | ||
createGraphQLError(`Unexpected response: "${text}"`, { | ||
extensions: { | ||
requestBody: { | ||
query, | ||
operationName: request.operationName, | ||
}, | ||
originalError: e, | ||
}), | ||
], | ||
}; | ||
} | ||
responseDetails: responseDetailsForError, | ||
}, | ||
originalError: e, | ||
}), | ||
], | ||
}; | ||
} | ||
} | ||
else { | ||
return result; | ||
} | ||
}) | ||
.catch((e) => { | ||
return result; | ||
} | ||
catch (e) { | ||
if (typeof e === 'string') { | ||
@@ -178,3 +175,3 @@ return { | ||
errors: [ | ||
createGraphQLError('The operation was aborted. reason: ' + controller.signal.reason, { | ||
createGraphQLError('The operation was aborted. Reason: ' + controller.signal.reason, { | ||
extensions: { | ||
@@ -224,4 +221,3 @@ requestBody: { | ||
} | ||
}) | ||
.resolve(); | ||
} | ||
}; | ||
@@ -232,3 +228,3 @@ if (options?.retry != null) { | ||
let attempt = 0; | ||
function retryAttempt() { | ||
async function retryAttempt() { | ||
attempt++; | ||
@@ -243,11 +239,7 @@ if (attempt > options.retry) { | ||
} | ||
return new ValueOrPromise(() => executor(request)) | ||
.then(res => { | ||
result = res; | ||
if (result?.errors?.length) { | ||
return retryAttempt(); | ||
} | ||
return result; | ||
}) | ||
.resolve(); | ||
result = await executor(request); | ||
if (Object(result).errors?.length) { | ||
return retryAttempt(); | ||
} | ||
return result; | ||
} | ||
@@ -254,0 +246,0 @@ return retryAttempt(); |
{ | ||
"name": "@graphql-tools/executor-http", | ||
"version": "1.0.3-alpha-20230809181317-aca946d4", | ||
"version": "1.0.3-alpha-20230809204600-c19e0921", | ||
"description": "A set of utils for faster development of GraphQL tools", | ||
@@ -15,4 +15,3 @@ "sideEffects": false, | ||
"meros": "^1.2.1", | ||
"tslib": "^2.4.0", | ||
"value-or-promise": "^1.0.12" | ||
"tslib": "^2.4.0" | ||
}, | ||
@@ -19,0 +18,0 @@ "repository": { |
@@ -10,2 +10,2 @@ import { FormData as DefaultFormData } from '@whatwg-node/fetch'; | ||
FormData?: typeof DefaultFormData; | ||
}): string | FormData | Promise<FormData>; | ||
}): string | Promise<FormData>; |
import { ExecutionResult } from '@graphql-tools/utils'; | ||
export declare function isReadableStream(value: any): value is ReadableStream; | ||
export declare function handleEventStreamResponse(response: Response, controller: AbortController): AsyncIterable<ExecutionResult>; | ||
import { SyncResponse } from './index.js'; | ||
export declare function handleEventStreamResponse(response: Response | SyncResponse): AsyncIterableIterator<ExecutionResult>; | ||
export declare function isReadableStream<T>(value: unknown): value is ReadableStream<T>; |
import { ExecutionResult } from '@graphql-tools/utils'; | ||
export declare function handleMultipartMixedResponse(response: Response, controller: AbortController): Promise<ExecutionResult<any, any> | AsyncIterable<ExecutionResult<any, any> | undefined>>; | ||
export declare function handleMultipartMixedResponse(response: Response): AsyncIterableIterator<ExecutionResult>; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
7
47641
30
1018
- Removedvalue-or-promise@^1.0.12
- Removedvalue-or-promise@1.0.12(transitive)