kinetex 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +413 -52
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +3134 -787
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +140 -33
- package/dist/cjs/cache.js +58 -7
- package/dist/cjs/circuit-breaker.js +53 -4
- package/dist/cjs/client.js +726 -188
- package/dist/cjs/cookie-parser.js +131 -6
- package/dist/cjs/cookie-store.js +125 -28
- package/dist/cjs/core.js +691 -70
- package/dist/cjs/dedup.js +142 -21
- package/dist/cjs/digest.js +195 -38
- package/dist/cjs/graphql.js +166 -35
- package/dist/cjs/headers.js +327 -46
- package/dist/cjs/interceptors.js +227 -24
- package/dist/cjs/lifecycle.js +92 -59
- package/dist/cjs/logging.js +160 -24
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +247 -22
- package/dist/cjs/progress.js +178 -30
- package/dist/cjs/proxy.js +429 -0
- package/dist/cjs/response.js +506 -110
- package/dist/cjs/socks5.js +136 -15
- package/dist/cjs/sse.js +173 -43
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +335 -73
- package/dist/cjs/ws.js +62 -12
- package/dist/esm/aws-sigv4.js +140 -33
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +58 -7
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +53 -4
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +726 -188
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +131 -6
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +125 -28
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +691 -70
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +142 -21
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +195 -38
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +166 -35
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +327 -46
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +227 -24
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +92 -59
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +160 -24
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/mod.js +3 -2
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/pagination.js +247 -22
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +178 -30
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +430 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +506 -110
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +136 -15
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +173 -43
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js +191 -45
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +335 -73
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/ws.js +62 -12
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +19 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +15 -2
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +69 -11
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts +0 -17
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/cookie-store.d.ts.map +1 -1
- package/dist/types/core.d.ts +216 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts +38 -3
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +17 -37
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +68 -31
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +126 -38
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts +19 -2
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts +22 -3
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mod.d.ts +5 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/pagination.d.ts +0 -25
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts +50 -0
- package/dist/types/proxy.d.ts.map +1 -0
- package/dist/types/response.d.ts +49 -2
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/sse.d.ts.map +1 -1
- package/dist/types/types.d.ts +219 -26
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -25
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts +69 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/ws.d.ts +0 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +10 -4
package/dist/cjs/graphql.js
CHANGED
|
@@ -30,6 +30,8 @@ import { ValidationError } from "./types.js";
|
|
|
30
30
|
// FIX (H6): untrusted GraphQL responses are parsed with raw JSON.parse below —
|
|
31
31
|
// sanitize prototype-pollution keys before they reach user code or merges.
|
|
32
32
|
import { sanitizeParsedJSON } from "./utils.js";
|
|
33
|
+
import { sleep } from "./utils.js";
|
|
34
|
+
import { sha256Hex } from "./utils.js";
|
|
33
35
|
// ============================================================================
|
|
34
36
|
// §2 ERRORS
|
|
35
37
|
// ============================================================================
|
|
@@ -82,9 +84,6 @@ function buildGraphQLError(graphQLErrors, req, res) {
|
|
|
82
84
|
return new GraphQLClientError(message, "EGRAPHQL", graphQLErrors, req, res);
|
|
83
85
|
}
|
|
84
86
|
// Utility function (B-10 fix - moved before usage)
|
|
85
|
-
function sleep(ms) {
|
|
86
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
-
}
|
|
88
87
|
function buildLinkChain(links, terminal) {
|
|
89
88
|
return links.reduceRight((next, link) => (op) => link(op, next), terminal);
|
|
90
89
|
}
|
|
@@ -121,14 +120,6 @@ export function extractOperationName(query) {
|
|
|
121
120
|
// ============================================================================
|
|
122
121
|
// §5 AUTOMATIC PERSISTED QUERIES (APQ)
|
|
123
122
|
// ============================================================================
|
|
124
|
-
async function sha256Hex(str) {
|
|
125
|
-
const encoded = new TextEncoder().encode(str);
|
|
126
|
-
const buf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
|
127
|
-
const hash = await crypto.subtle.digest("SHA-256", buf);
|
|
128
|
-
return Array.from(new Uint8Array(hash))
|
|
129
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
130
|
-
.join("");
|
|
131
|
-
}
|
|
132
123
|
/** Maximum APQ cache entries to prevent memory leaks */
|
|
133
124
|
const MAX_APQ_CACHE_ENTRIES = 1000;
|
|
134
125
|
/** APQ cache entry TTL in ms — entries older than this are evicted */
|
|
@@ -179,6 +170,72 @@ const MALICIOUS_QUERY_PATTERNS = [
|
|
|
179
170
|
/onerror\s*=/i, // Event handler injection (onerror=, onclick=, etc.)
|
|
180
171
|
/eval\s*\(/i, // Code execution via eval()
|
|
181
172
|
];
|
|
173
|
+
/**
|
|
174
|
+
* Blank out the contents of every GraphQL string literal, preserving the
|
|
175
|
+
* document's length so the surrounding structure is unchanged.
|
|
176
|
+
*
|
|
177
|
+
* Both the injection scan and the brace counter used to read string literals
|
|
178
|
+
* as if they were code, so a query the caller was entitled to send was
|
|
179
|
+
* rejected:
|
|
180
|
+
*
|
|
181
|
+
* - `{ search(term: "javascript:void(0)") }` -> "malicious pattern: /javascript:/"
|
|
182
|
+
* - `{ run(code: "eval(x)") }` -> "malicious pattern: /eval\s*\(/"
|
|
183
|
+
* - `{ page(html: "<div onerror=alert(1)>") }` -> "malicious pattern: /onerror\s*=/"
|
|
184
|
+
* - `{ css(v: "<!-- hi -->") }` -> "malicious pattern: /<!--/"
|
|
185
|
+
* - `{ a(s: "}") b }` -> "GraphQL query has unbalanced braces"
|
|
186
|
+
* - `{ f(x: "{{") }` -> "malicious pattern: /\{\s*\{/"
|
|
187
|
+
*
|
|
188
|
+
* A client refusing to transmit a well-formed query is a hard failure for the
|
|
189
|
+
* caller, and none of these is a client-side hazard: what the *server* renders
|
|
190
|
+
* from a string it was sent is the server's responsibility to escape, which is
|
|
191
|
+
* precisely what a scan of the request text cannot check. The patterns stay
|
|
192
|
+
* exactly as strict for text *outside* string literals, which is where a
|
|
193
|
+
* malformed or hostile document actually shows up.
|
|
194
|
+
*
|
|
195
|
+
* Handles `"..."`, the block forms `"""..."""` and `'''...'''`, and `#`
|
|
196
|
+
* comments, and leaves an unterminated quote alone so the structural checks
|
|
197
|
+
* still see it.
|
|
198
|
+
*/
|
|
199
|
+
function maskGraphQLLiterals(query) {
|
|
200
|
+
const out = query.split("");
|
|
201
|
+
let i = 0;
|
|
202
|
+
const n = query.length;
|
|
203
|
+
while (i < n) {
|
|
204
|
+
const ch = query[i];
|
|
205
|
+
// Comment to end of line.
|
|
206
|
+
if (ch === "#") {
|
|
207
|
+
while (i < n && query[i] !== "\n")
|
|
208
|
+
out[i++] = " ";
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Block string: """ or '''.
|
|
212
|
+
if ((ch === '"' || ch === "'") && query[i + 1] === ch && query[i + 2] === ch) {
|
|
213
|
+
const delim = ch.repeat(3);
|
|
214
|
+
const end = query.indexOf(delim, i + 3);
|
|
215
|
+
const stop = end === -1 ? n : end + 3;
|
|
216
|
+
while (i < stop)
|
|
217
|
+
out[i++] = " ";
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Ordinary string.
|
|
221
|
+
if (ch === '"' || ch === "'") {
|
|
222
|
+
const quote = ch;
|
|
223
|
+
out[i] = " ";
|
|
224
|
+
i++;
|
|
225
|
+
while (i < n && query[i] !== quote && query[i] !== "\n") {
|
|
226
|
+
out[i] = " ";
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
if (i < n && query[i] === quote) {
|
|
230
|
+
out[i] = " ";
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
return out.join("");
|
|
238
|
+
}
|
|
182
239
|
/**
|
|
183
240
|
* Validate a GraphQL query for safety.
|
|
184
241
|
* Prevents excessively large queries and detects malicious patterns.
|
|
@@ -192,17 +249,20 @@ function validateGraphQLQuery(query) {
|
|
|
192
249
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
193
250
|
throw new ValidationError(`GraphQL query exceeds maximum length of ${MAX_QUERY_LENGTH} bytes`);
|
|
194
251
|
}
|
|
252
|
+
// Structural checks run on the document with its string literals blanked, so
|
|
253
|
+
// a brace or a keyword inside a value is treated as data, not as syntax.
|
|
254
|
+
const structural = maskGraphQLLiterals(query);
|
|
195
255
|
// Check for malicious patterns
|
|
196
256
|
for (const pattern of MALICIOUS_QUERY_PATTERNS) {
|
|
197
|
-
if (pattern.test(
|
|
257
|
+
if (pattern.test(structural)) {
|
|
198
258
|
throw new ValidationError(`GraphQL query contains potentially malicious pattern: ${pattern}`);
|
|
199
259
|
}
|
|
200
260
|
}
|
|
201
261
|
// Basic depth check by counting open braces
|
|
202
262
|
let depth = 0;
|
|
203
263
|
let maxDepth = 0;
|
|
204
|
-
for (let i = 0; i <
|
|
205
|
-
const char =
|
|
264
|
+
for (let i = 0; i < structural.length; i++) {
|
|
265
|
+
const char = structural[i];
|
|
206
266
|
if (char === "{") {
|
|
207
267
|
depth++;
|
|
208
268
|
maxDepth = Math.max(maxDepth, depth);
|
|
@@ -289,12 +349,25 @@ function buildMultipartBody(req, uploads) {
|
|
|
289
349
|
const form = new FormData();
|
|
290
350
|
// Null out file variables in the operations object
|
|
291
351
|
const operations = JSON.parse(buildJSONBody(req));
|
|
292
|
-
//
|
|
352
|
+
// The multipart spec's `map` paths are rooted at the `operations` object,
|
|
353
|
+
// and its example is `{"0": ["variables.file"]}` — so the path a caller
|
|
354
|
+
// passes has to name `variables` for the nulling below to reach the variable
|
|
355
|
+
// the mutation actually declares. The documented examples here did the
|
|
356
|
+
// opposite (`path: "file"`, `path: "input.file"`), and they produced
|
|
357
|
+
// `{"variables": {"file": {}}, "file": null}`: the file was nulled at the
|
|
358
|
+
// top level of the request object where no variable lives, and the real
|
|
359
|
+
// variable kept its original value. The upload never bound.
|
|
360
|
+
//
|
|
361
|
+
// Both spellings are now accepted, and the map always carries the rooted
|
|
362
|
+
// form so the request stays spec-conformant.
|
|
293
363
|
const map = {};
|
|
294
364
|
for (let i = 0; i < uploads.length; i++) {
|
|
295
365
|
const upload = uploads[i];
|
|
296
|
-
|
|
297
|
-
|
|
366
|
+
const rooted = upload.path === "variables" || upload.path.startsWith("variables.")
|
|
367
|
+
? upload.path
|
|
368
|
+
: `variables.${upload.path}`;
|
|
369
|
+
map[String(i)] = [rooted];
|
|
370
|
+
setNestedValue(operations, rooted, null);
|
|
298
371
|
}
|
|
299
372
|
form.append("operations", JSON.stringify(operations));
|
|
300
373
|
form.append("map", JSON.stringify(map));
|
|
@@ -315,8 +388,15 @@ function setNestedValue(obj, path, value) {
|
|
|
315
388
|
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
316
389
|
throw new ValidationError(`Invalid upload path: "${path}" — reserved key "${key}"`);
|
|
317
390
|
}
|
|
391
|
+
// Whether this segment is an array or an object is decided by the segment
|
|
392
|
+
// *after* it: in `files.0` the container `files` holds an index, so it must
|
|
393
|
+
// be created as an array. Deciding by `key` instead produced `{}`, and the
|
|
394
|
+
// server then saw `variables.files` as `{"0": null}` — an object, not the
|
|
395
|
+
// list the mutation declared.
|
|
396
|
+
const nextIsIndex = /^(0|[1-9][0-9]*)$/.test(parts[i + 1]);
|
|
397
|
+
const next = nextIsIndex ? [] : {};
|
|
318
398
|
if (!current[key] || typeof current[key] !== "object")
|
|
319
|
-
current[key] =
|
|
399
|
+
current[key] = next;
|
|
320
400
|
current = current[key];
|
|
321
401
|
}
|
|
322
402
|
const leaf = parts[parts.length - 1];
|
|
@@ -348,7 +428,9 @@ async function parseGraphQLResponse(response, req) {
|
|
|
348
428
|
// §9 CORE EXECUTE FUNCTION
|
|
349
429
|
// ============================================================================
|
|
350
430
|
async function executeHTTP(req, config, signal, apqMode = "none", getAPQHashFn) {
|
|
351
|
-
|
|
431
|
+
// `req.headers` is last so a link (or a caller using `raw`) can override a
|
|
432
|
+
// default. It used to be ignored entirely.
|
|
433
|
+
const headers = await buildHeaders(config, req.headers ?? {});
|
|
352
434
|
const opType = detectOperationType(req.query);
|
|
353
435
|
const useGET = config.useGETForQueries && opType === "query";
|
|
354
436
|
let fetchUrl = config.url;
|
|
@@ -469,8 +551,12 @@ export class GraphQLClient {
|
|
|
469
551
|
onError: () => { },
|
|
470
552
|
...config,
|
|
471
553
|
};
|
|
472
|
-
// Terminal link — does the actual HTTP fetch
|
|
473
|
-
|
|
554
|
+
// Terminal link — does the actual HTTP fetch.
|
|
555
|
+
// `op.config` is honoured, not just `this.config`: links such as
|
|
556
|
+
// `authLink` replace it to inject headers, and the terminal previously
|
|
557
|
+
// discarded the replacement and read the constructor-time object, so
|
|
558
|
+
// nothing a link put there was ever sent.
|
|
559
|
+
const terminal = (op) => this._executeWithAPQ(op.request, op.signal ?? null, op.config ?? this.config);
|
|
474
560
|
this.executeLink = buildLinkChain(this.config.links, terminal);
|
|
475
561
|
}
|
|
476
562
|
// ── APQ (Automatic Persisted Queries) ───────────────────────────────────
|
|
@@ -570,7 +656,7 @@ export class GraphQLClient {
|
|
|
570
656
|
...(variables !== undefined ? { variables } : {}),
|
|
571
657
|
...(options.operationName !== undefined
|
|
572
658
|
? { operationName: options.operationName }
|
|
573
|
-
: extractOperationName(query) !==
|
|
659
|
+
: extractOperationName(query) !== null
|
|
574
660
|
? { operationName: extractOperationName(query) }
|
|
575
661
|
: {}),
|
|
576
662
|
...(options.extensions !== undefined ? { extensions: options.extensions } : {}),
|
|
@@ -627,13 +713,11 @@ export class GraphQLClient {
|
|
|
627
713
|
variables,
|
|
628
714
|
...(options.operationName !== undefined
|
|
629
715
|
? { operationName: options.operationName }
|
|
630
|
-
: extractOperationName(query) !==
|
|
716
|
+
: extractOperationName(query) !== null
|
|
631
717
|
? { operationName: extractOperationName(query) }
|
|
632
718
|
: {}),
|
|
633
719
|
};
|
|
634
|
-
const headers = await buildHeaders(this.config, {
|
|
635
|
-
/* drop content-type for multipart */
|
|
636
|
-
});
|
|
720
|
+
const headers = await buildHeaders(this.config, { /* drop content-type for multipart */});
|
|
637
721
|
delete headers["content-type"]; // FormData sets it with boundary
|
|
638
722
|
const form = buildMultipartBody(req, uploads);
|
|
639
723
|
const controller = new AbortController();
|
|
@@ -686,6 +770,12 @@ export class GraphQLClient {
|
|
|
686
770
|
* contains GraphQL errors, or is missing data for any request
|
|
687
771
|
*/
|
|
688
772
|
async batch(requests, options = {}) {
|
|
773
|
+
// `query()` and `upload()` both run the query through the validator before
|
|
774
|
+
// sending; `batch()` did not, so a batch was the one way to bypass the
|
|
775
|
+
// length, depth and injection limits the module applies everywhere else.
|
|
776
|
+
for (const req of requests) {
|
|
777
|
+
validateGraphQLQuery(req.query);
|
|
778
|
+
}
|
|
689
779
|
const headers = await buildHeaders(this.config);
|
|
690
780
|
const controller = new AbortController();
|
|
691
781
|
// Listener removed when the fetch settles — avoids accumulating on the
|
|
@@ -707,13 +797,38 @@ export class GraphQLClient {
|
|
|
707
797
|
finally {
|
|
708
798
|
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
709
799
|
}
|
|
710
|
-
//
|
|
711
|
-
|
|
800
|
+
// Every other path routes a non-JSON body through `parseGraphQLResponse`,
|
|
801
|
+
// which turns it into a typed `EPARSE` / `ENETWORK` GraphQLClientError.
|
|
802
|
+
// `batch()` called `response.json()` bare, so a 502 HTML error page threw a
|
|
803
|
+
// raw `SyntaxError` with no `code` at all — a caller switching on
|
|
804
|
+
// `err.code` got `undefined` and had no way to tell this from a bug.
|
|
805
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
806
|
+
if (!response.ok && !ct.includes("application/json")) {
|
|
807
|
+
const text = await response.text().catch(() => "(unreadable)");
|
|
808
|
+
throw new GraphQLClientError(`HTTP ${response.status}: ${response.statusText}\n${text}`, "ENETWORK", undefined, requests[0], undefined, new Error(`HTTP ${response.status}`));
|
|
809
|
+
}
|
|
810
|
+
let rawResults;
|
|
811
|
+
try {
|
|
812
|
+
// FIX (H6): sanitize untrusted batch response JSON before validation.
|
|
813
|
+
rawResults = sanitizeParsedJSON((await response.json()));
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
throw new GraphQLClientError("Failed to parse batch response as JSON", "EPARSE", undefined, requests[0], undefined, err);
|
|
817
|
+
}
|
|
712
818
|
// Validate response is array (B-5 fix)
|
|
713
819
|
if (!Array.isArray(rawResults)) {
|
|
714
820
|
throw new GraphQLClientError("Batch response must be an array of GraphQL responses", "EINVALIDRESPONSE", undefined, requests[0], rawResults);
|
|
715
821
|
}
|
|
716
822
|
const results = rawResults;
|
|
823
|
+
// The contract is "one `data` per request, in order". A server returning a
|
|
824
|
+
// different number was mapped over anyway: two requests answered with one
|
|
825
|
+
// result silently returned one item, and one request answered with three
|
|
826
|
+
// returned three — with `requests[i]` `undefined` for the extra ones, so an
|
|
827
|
+
// `errors` entry there was attributed to no request at all. Positional
|
|
828
|
+
// alignment is the whole point of a batch, so a mismatch is an error.
|
|
829
|
+
if (results.length !== requests.length) {
|
|
830
|
+
throw new GraphQLClientError(`Batch response has ${results.length} result(s) but ${requests.length} request(s) were sent`, "EINVALIDRESPONSE", undefined, requests[0], rawResults);
|
|
831
|
+
}
|
|
717
832
|
return results.map((res, i) => {
|
|
718
833
|
if (res.errors?.length)
|
|
719
834
|
throw buildGraphQLError(res.errors, requests[i], res);
|
|
@@ -776,7 +891,7 @@ export class GraphQLClient {
|
|
|
776
891
|
...(variables !== undefined ? { variables } : {}),
|
|
777
892
|
...(options.operationName !== undefined
|
|
778
893
|
? { operationName: options.operationName }
|
|
779
|
-
: extractOperationName(query) !==
|
|
894
|
+
: extractOperationName(query) !== null
|
|
780
895
|
? { operationName: extractOperationName(query) }
|
|
781
896
|
: {}),
|
|
782
897
|
};
|
|
@@ -861,6 +976,13 @@ export class GraphQLClient {
|
|
|
861
976
|
});
|
|
862
977
|
}
|
|
863
978
|
async _executeWithRetry(req, signal) {
|
|
979
|
+
// A negative count made the loop condition false on the first evaluation,
|
|
980
|
+
// so the body never ran, `lastErr` was never assigned, and the method
|
|
981
|
+
// ended in `throw undefined` — a rejection carrying no value at all, so
|
|
982
|
+
// `err.message` threw a TypeError inside the caller's own error handler.
|
|
983
|
+
if (!Number.isInteger(this.config.retries) || this.config.retries < 0) {
|
|
984
|
+
throw new RangeError(`retries must be a non-negative integer, got ${this.config.retries}`);
|
|
985
|
+
}
|
|
864
986
|
let lastErr;
|
|
865
987
|
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
866
988
|
if (attempt > 0)
|
|
@@ -878,18 +1000,18 @@ export class GraphQLClient {
|
|
|
878
1000
|
}
|
|
879
1001
|
throw lastErr;
|
|
880
1002
|
}
|
|
881
|
-
async _executeWithAPQ(req, signal) {
|
|
882
|
-
if (!
|
|
883
|
-
return executeHTTP(req,
|
|
1003
|
+
async _executeWithAPQ(req, signal, config = this.config) {
|
|
1004
|
+
if (!config.enableAPQ) {
|
|
1005
|
+
return executeHTTP(req, config, signal, "none", undefined);
|
|
884
1006
|
}
|
|
885
1007
|
// APQ: first try without query string
|
|
886
|
-
const res1 = await executeHTTP(req,
|
|
1008
|
+
const res1 = await executeHTTP(req, config, signal, "omitQuery", (q) => this._getAPQHash(q));
|
|
887
1009
|
// Check if server responded with PersistedQueryNotFound
|
|
888
1010
|
const notFound = res1.errors?.some((e) => e.extensions?.["code"] === "PERSISTED_QUERY_NOT_FOUND");
|
|
889
1011
|
if (!notFound)
|
|
890
1012
|
return res1;
|
|
891
1013
|
// Retry with full query
|
|
892
|
-
return executeHTTP(req,
|
|
1014
|
+
return executeHTTP(req, config, signal, "full", (q) => this._getAPQHash(q));
|
|
893
1015
|
}
|
|
894
1016
|
}
|
|
895
1017
|
// ============================================================================
|
|
@@ -945,7 +1067,9 @@ export function errorLink(handler) {
|
|
|
945
1067
|
catch (err) {
|
|
946
1068
|
if (!(err instanceof GraphQLClientError))
|
|
947
1069
|
throw err;
|
|
948
|
-
|
|
1070
|
+
// Must be awaited: an async handler returning `null` — the documented
|
|
1071
|
+
// way to say "re-throw" — would otherwise be a truthy Promise here.
|
|
1072
|
+
const result = await handler(err, op, () => next(op));
|
|
949
1073
|
if (result)
|
|
950
1074
|
return result;
|
|
951
1075
|
throw err;
|
|
@@ -997,6 +1121,13 @@ export function loggingLink(logger = (msg, data) => console.log(msg, data)) {
|
|
|
997
1121
|
export function retryLink(options = {}) {
|
|
998
1122
|
const max = options.maxRetries ?? 3;
|
|
999
1123
|
const delay = options.delayMs ?? 300;
|
|
1124
|
+
// Same `throw undefined` defect as the client's own `retries`.
|
|
1125
|
+
if (!Number.isInteger(max) || max < 0) {
|
|
1126
|
+
throw new RangeError(`retryLink: maxRetries must be a non-negative integer, got ${max}`);
|
|
1127
|
+
}
|
|
1128
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
1129
|
+
throw new RangeError(`retryLink: delayMs must be a non-negative number, got ${delay}`);
|
|
1130
|
+
}
|
|
1000
1131
|
const should = options.shouldRetry ?? ((err) => !(err instanceof GraphQLClientError && err.isGraphQLError));
|
|
1001
1132
|
return async (op, next) => {
|
|
1002
1133
|
let lastErr;
|