@apollo/client
Version:
A fully-featured caching GraphQL client.
160 lines (159 loc) • 6.72 kB
JavaScript
import { CombinedProtocolErrors, PROTOCOL_ERRORS_SYMBOL, ServerError, ServerParseError, } from "@apollo/client/errors";
import { isNonNullObject } from "@apollo/client/utilities/internal";
import { invariant } from "@apollo/client/utilities/invariant";
const { hasOwnProperty } = Object.prototype;
/**
* This function detects an Apollo payload result before it is transformed
* into a FetchResult via HttpLink; it cannot detect an ApolloPayloadResult
* once it leaves the link chain.
*/
function isApolloPayloadResult(value) {
return isNonNullObject(value) && "payload" in value;
}
async function* consumeMultipartBody(response) {
const decoder = new TextDecoder("utf-8");
const contentType = response.headers?.get("content-type");
// parse boundary value and ignore any subsequent name/value pairs after ;
// https://www.rfc-editor.org/rfc/rfc9110.html#name-parameters
// e.g. multipart/mixed;boundary="graphql";deferSpec=20220824
// if no boundary is specified, default to -
const match = contentType?.match(
/*
;\s*boundary= # Match the boundary parameter
(?: # either
'([^']*)' # a string starting with ' doesn't contain ', ends with '
| # or
"([^"]*)" # a string starting with " doesn't contain ", ends with "
| # or
([^"'].*?) # a string that doesn't start with ' or ", parsed non-greedily
) # end of the group
\s* # optional whitespace
(?:;|$) # match a semicolon or end of string
*/
/;\s*boundary=(?:'([^']+)'|"([^"]+)"|([^"'].+?))\s*(?:;|$)/i);
const boundary = "\r\n--" + (match ? match[1] ?? match[2] ?? match[3] ?? "-" : "-");
let buffer = "";
invariant(response.body && typeof response.body.getReader === "function", 60);
const stream = response.body;
const reader = stream.getReader();
let done = false;
let encounteredBoundary = false;
let value;
// check to see if we received the final boundary, which is a normal boundary followed by "--"
// as described in https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1
const passedFinalBoundary = () => encounteredBoundary && buffer[0] == "-" && buffer[1] == "-";
try {
while (!done) {
({ value, done } = await reader.read());
const chunk = typeof value === "string" ? value : decoder.decode(value);
const searchFrom = buffer.length - boundary.length + 1;
buffer += chunk;
let bi = buffer.indexOf(boundary, searchFrom);
while (bi > -1 && !passedFinalBoundary()) {
encounteredBoundary = true;
let message;
[message, buffer] = [
buffer.slice(0, bi),
buffer.slice(bi + boundary.length),
];
const i = message.indexOf("\r\n\r\n");
const headers = parseHeaders(message.slice(0, i));
const contentType = headers["content-type"];
if (contentType &&
contentType.toLowerCase().indexOf("application/json") === -1) {
throw new Error("Unsupported patch content type: application/json is required.");
}
// nb: Technically you'd want to slice off the beginning "\r\n" but since
// this is going to be `JSON.parse`d there is no need.
const body = message.slice(i);
if (body) {
yield body;
}
bi = buffer.indexOf(boundary);
}
if (passedFinalBoundary()) {
return;
}
}
throw new Error("premature end of multipart body");
}
finally {
reader.cancel();
}
}
export async function readMultipartBody(response, nextValue) {
for await (const body of consumeMultipartBody(response)) {
const result = parseJsonEncoding(response, body);
if (Object.keys(result).length == 0)
continue;
if (isApolloPayloadResult(result)) {
if (Object.keys(result).length === 1 && result.payload === null) {
return;
}
let next = { ...result.payload };
if ("errors" in result) {
next.extensions = {
...next.extensions,
[PROTOCOL_ERRORS_SYMBOL]: new CombinedProtocolErrors(result.errors ?? []),
};
}
nextValue(next);
}
else {
nextValue(result);
}
}
}
function parseHeaders(headerText) {
const headersInit = {};
headerText.split("\n").forEach((line) => {
const i = line.indexOf(":");
if (i > -1) {
// normalize headers to lowercase
const name = line.slice(0, i).trim().toLowerCase();
const value = line.slice(i + 1).trim();
headersInit[name] = value;
}
});
return headersInit;
}
function parseJsonEncoding(response, bodyText) {
if (response.status >= 300) {
throw new ServerError(`Response not successful: Received status code ${response.status}`, { response, bodyText });
}
try {
return JSON.parse(bodyText);
}
catch (err) {
throw new ServerParseError(err, { response, bodyText });
}
}
function parseGraphQLResponseJsonEncoding(response, bodyText) {
try {
return JSON.parse(bodyText);
}
catch (err) {
throw new ServerParseError(err, { response, bodyText });
}
}
function parseResponse(response, bodyText) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/graphql-response+json")) {
return parseGraphQLResponseJsonEncoding(response, bodyText);
}
return parseJsonEncoding(response, bodyText);
}
export function parseAndCheckHttpResponse(operations) {
return (response) => response.text().then((bodyText) => {
const result = parseResponse(response, bodyText);
if (!Array.isArray(result) &&
!hasOwnProperty.call(result, "data") &&
!hasOwnProperty.call(result, "errors")) {
throw new ServerError(`Server response was malformed for query '${Array.isArray(operations) ?
operations.map((op) => op.operationName)
: operations.operationName}'.`, { response, bodyText });
}
return result;
});
}
//# sourceMappingURL=parseAndCheckHttpResponse.js.map