kinetex 1.2.0 → 1.4.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 +1164 -453
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +3127 -715
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +137 -20
- package/dist/cjs/cache.js +101 -21
- package/dist/cjs/circuit-breaker.js +69 -7
- package/dist/cjs/client.js +838 -191
- package/dist/cjs/cookie-parser.js +110 -9
- package/dist/cjs/cookie-store.js +141 -36
- package/dist/cjs/core.js +501 -63
- package/dist/cjs/dedup.js +58 -18
- package/dist/cjs/digest.js +185 -23
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +362 -48
- package/dist/cjs/interceptors.js +285 -29
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +169 -16
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +261 -28
- package/dist/cjs/progress.js +282 -52
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +167 -36
- package/dist/cjs/sse.js +201 -34
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/worker.js +6 -6
- package/dist/cjs/ws.js +32 -16
- package/dist/esm/aws-sigv4.js +137 -20
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +101 -21
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +69 -7
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +838 -191
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +110 -9
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +141 -36
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +501 -63
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +58 -18
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +185 -23
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +164 -24
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +362 -48
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +285 -29
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +89 -40
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +169 -16
- 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 +261 -28
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +282 -52
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +413 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +316 -47
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +167 -36
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +201 -34
- 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 +222 -48
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/worker.js +6 -6
- package/dist/esm/worker.js.map +1 -1
- package/dist/esm/ws.js +32 -16
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +27 -2
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +14 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +98 -23
- 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 +109 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts +0 -7
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +31 -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 +62 -29
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +102 -0
- 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 +23 -4
- 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 +7 -1
- 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 +139 -5
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -14
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/worker.d.ts +6 -6
- package/dist/types/worker.d.ts.map +1 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/cjs/graphql.js
CHANGED
|
@@ -179,6 +179,72 @@ const MALICIOUS_QUERY_PATTERNS = [
|
|
|
179
179
|
/onerror\s*=/i, // Event handler injection (onerror=, onclick=, etc.)
|
|
180
180
|
/eval\s*\(/i, // Code execution via eval()
|
|
181
181
|
];
|
|
182
|
+
/**
|
|
183
|
+
* Blank out the contents of every GraphQL string literal, preserving the
|
|
184
|
+
* document's length so the surrounding structure is unchanged.
|
|
185
|
+
*
|
|
186
|
+
* Both the injection scan and the brace counter used to read string literals
|
|
187
|
+
* as if they were code, so a query the caller was entitled to send was
|
|
188
|
+
* rejected:
|
|
189
|
+
*
|
|
190
|
+
* - `{ search(term: "javascript:void(0)") }` -> "malicious pattern: /javascript:/"
|
|
191
|
+
* - `{ run(code: "eval(x)") }` -> "malicious pattern: /eval\s*\(/"
|
|
192
|
+
* - `{ page(html: "<div onerror=alert(1)>") }` -> "malicious pattern: /onerror\s*=/"
|
|
193
|
+
* - `{ css(v: "<!-- hi -->") }` -> "malicious pattern: /<!--/"
|
|
194
|
+
* - `{ a(s: "}") b }` -> "GraphQL query has unbalanced braces"
|
|
195
|
+
* - `{ f(x: "{{") }` -> "malicious pattern: /\{\s*\{/"
|
|
196
|
+
*
|
|
197
|
+
* A client refusing to transmit a well-formed query is a hard failure for the
|
|
198
|
+
* caller, and none of these is a client-side hazard: what the *server* renders
|
|
199
|
+
* from a string it was sent is the server's responsibility to escape, which is
|
|
200
|
+
* precisely what a scan of the request text cannot check. The patterns stay
|
|
201
|
+
* exactly as strict for text *outside* string literals, which is where a
|
|
202
|
+
* malformed or hostile document actually shows up.
|
|
203
|
+
*
|
|
204
|
+
* Handles `"..."`, the block forms `"""..."""` and `'''...'''`, and `#`
|
|
205
|
+
* comments, and leaves an unterminated quote alone so the structural checks
|
|
206
|
+
* still see it.
|
|
207
|
+
*/
|
|
208
|
+
function maskGraphQLLiterals(query) {
|
|
209
|
+
const out = query.split("");
|
|
210
|
+
let i = 0;
|
|
211
|
+
const n = query.length;
|
|
212
|
+
while (i < n) {
|
|
213
|
+
const ch = query[i];
|
|
214
|
+
// Comment to end of line.
|
|
215
|
+
if (ch === "#") {
|
|
216
|
+
while (i < n && query[i] !== "\n")
|
|
217
|
+
out[i++] = " ";
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Block string: """ or '''.
|
|
221
|
+
if ((ch === '"' || ch === "'") && query[i + 1] === ch && query[i + 2] === ch) {
|
|
222
|
+
const delim = ch.repeat(3);
|
|
223
|
+
const end = query.indexOf(delim, i + 3);
|
|
224
|
+
const stop = end === -1 ? n : end + 3;
|
|
225
|
+
while (i < stop)
|
|
226
|
+
out[i++] = " ";
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Ordinary string.
|
|
230
|
+
if (ch === '"' || ch === "'") {
|
|
231
|
+
const quote = ch;
|
|
232
|
+
out[i] = " ";
|
|
233
|
+
i++;
|
|
234
|
+
while (i < n && query[i] !== quote && query[i] !== "\n") {
|
|
235
|
+
out[i] = " ";
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
if (i < n && query[i] === quote) {
|
|
239
|
+
out[i] = " ";
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
return out.join("");
|
|
247
|
+
}
|
|
182
248
|
/**
|
|
183
249
|
* Validate a GraphQL query for safety.
|
|
184
250
|
* Prevents excessively large queries and detects malicious patterns.
|
|
@@ -192,17 +258,20 @@ function validateGraphQLQuery(query) {
|
|
|
192
258
|
if (query.length > MAX_QUERY_LENGTH) {
|
|
193
259
|
throw new ValidationError(`GraphQL query exceeds maximum length of ${MAX_QUERY_LENGTH} bytes`);
|
|
194
260
|
}
|
|
261
|
+
// Structural checks run on the document with its string literals blanked, so
|
|
262
|
+
// a brace or a keyword inside a value is treated as data, not as syntax.
|
|
263
|
+
const structural = maskGraphQLLiterals(query);
|
|
195
264
|
// Check for malicious patterns
|
|
196
265
|
for (const pattern of MALICIOUS_QUERY_PATTERNS) {
|
|
197
|
-
if (pattern.test(
|
|
266
|
+
if (pattern.test(structural)) {
|
|
198
267
|
throw new ValidationError(`GraphQL query contains potentially malicious pattern: ${pattern}`);
|
|
199
268
|
}
|
|
200
269
|
}
|
|
201
270
|
// Basic depth check by counting open braces
|
|
202
271
|
let depth = 0;
|
|
203
272
|
let maxDepth = 0;
|
|
204
|
-
for (let i = 0; i <
|
|
205
|
-
const char =
|
|
273
|
+
for (let i = 0; i < structural.length; i++) {
|
|
274
|
+
const char = structural[i];
|
|
206
275
|
if (char === "{") {
|
|
207
276
|
depth++;
|
|
208
277
|
maxDepth = Math.max(maxDepth, depth);
|
|
@@ -289,12 +358,25 @@ function buildMultipartBody(req, uploads) {
|
|
|
289
358
|
const form = new FormData();
|
|
290
359
|
// Null out file variables in the operations object
|
|
291
360
|
const operations = JSON.parse(buildJSONBody(req));
|
|
292
|
-
//
|
|
361
|
+
// The multipart spec's `map` paths are rooted at the `operations` object,
|
|
362
|
+
// and its example is `{"0": ["variables.file"]}` — so the path a caller
|
|
363
|
+
// passes has to name `variables` for the nulling below to reach the variable
|
|
364
|
+
// the mutation actually declares. The documented examples here did the
|
|
365
|
+
// opposite (`path: "file"`, `path: "input.file"`), and they produced
|
|
366
|
+
// `{"variables": {"file": {}}, "file": null}`: the file was nulled at the
|
|
367
|
+
// top level of the request object where no variable lives, and the real
|
|
368
|
+
// variable kept its original value. The upload never bound.
|
|
369
|
+
//
|
|
370
|
+
// Both spellings are now accepted, and the map always carries the rooted
|
|
371
|
+
// form so the request stays spec-conformant.
|
|
293
372
|
const map = {};
|
|
294
373
|
for (let i = 0; i < uploads.length; i++) {
|
|
295
374
|
const upload = uploads[i];
|
|
296
|
-
|
|
297
|
-
|
|
375
|
+
const rooted = upload.path === "variables" || upload.path.startsWith("variables.")
|
|
376
|
+
? upload.path
|
|
377
|
+
: `variables.${upload.path}`;
|
|
378
|
+
map[String(i)] = [rooted];
|
|
379
|
+
setNestedValue(operations, rooted, null);
|
|
298
380
|
}
|
|
299
381
|
form.append("operations", JSON.stringify(operations));
|
|
300
382
|
form.append("map", JSON.stringify(map));
|
|
@@ -315,8 +397,15 @@ function setNestedValue(obj, path, value) {
|
|
|
315
397
|
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
316
398
|
throw new ValidationError(`Invalid upload path: "${path}" — reserved key "${key}"`);
|
|
317
399
|
}
|
|
400
|
+
// Whether this segment is an array or an object is decided by the segment
|
|
401
|
+
// *after* it: in `files.0` the container `files` holds an index, so it must
|
|
402
|
+
// be created as an array. Deciding by `key` instead produced `{}`, and the
|
|
403
|
+
// server then saw `variables.files` as `{"0": null}` — an object, not the
|
|
404
|
+
// list the mutation declared.
|
|
405
|
+
const nextIsIndex = /^(0|[1-9][0-9]*)$/.test(parts[i + 1]);
|
|
406
|
+
const next = nextIsIndex ? [] : {};
|
|
318
407
|
if (!current[key] || typeof current[key] !== "object")
|
|
319
|
-
current[key] =
|
|
408
|
+
current[key] = next;
|
|
320
409
|
current = current[key];
|
|
321
410
|
}
|
|
322
411
|
const leaf = parts[parts.length - 1];
|
|
@@ -348,7 +437,9 @@ async function parseGraphQLResponse(response, req) {
|
|
|
348
437
|
// §9 CORE EXECUTE FUNCTION
|
|
349
438
|
// ============================================================================
|
|
350
439
|
async function executeHTTP(req, config, signal, apqMode = "none", getAPQHashFn) {
|
|
351
|
-
|
|
440
|
+
// `req.headers` is last so a link (or a caller using `raw`) can override a
|
|
441
|
+
// default. It used to be ignored entirely.
|
|
442
|
+
const headers = await buildHeaders(config, req.headers ?? {});
|
|
352
443
|
const opType = detectOperationType(req.query);
|
|
353
444
|
const useGET = config.useGETForQueries && opType === "query";
|
|
354
445
|
let fetchUrl = config.url;
|
|
@@ -469,8 +560,12 @@ export class GraphQLClient {
|
|
|
469
560
|
onError: () => { },
|
|
470
561
|
...config,
|
|
471
562
|
};
|
|
472
|
-
// Terminal link — does the actual HTTP fetch
|
|
473
|
-
|
|
563
|
+
// Terminal link — does the actual HTTP fetch.
|
|
564
|
+
// `op.config` is honoured, not just `this.config`: links such as
|
|
565
|
+
// `authLink` replace it to inject headers, and the terminal previously
|
|
566
|
+
// discarded the replacement and read the constructor-time object, so
|
|
567
|
+
// nothing a link put there was ever sent.
|
|
568
|
+
const terminal = (op) => this._executeWithAPQ(op.request, op.signal ?? null, op.config ?? this.config);
|
|
474
569
|
this.executeLink = buildLinkChain(this.config.links, terminal);
|
|
475
570
|
}
|
|
476
571
|
// ── APQ (Automatic Persisted Queries) ───────────────────────────────────
|
|
@@ -570,7 +665,7 @@ export class GraphQLClient {
|
|
|
570
665
|
...(variables !== undefined ? { variables } : {}),
|
|
571
666
|
...(options.operationName !== undefined
|
|
572
667
|
? { operationName: options.operationName }
|
|
573
|
-
: extractOperationName(query) !==
|
|
668
|
+
: extractOperationName(query) !== null
|
|
574
669
|
? { operationName: extractOperationName(query) }
|
|
575
670
|
: {}),
|
|
576
671
|
...(options.extensions !== undefined ? { extensions: options.extensions } : {}),
|
|
@@ -627,13 +722,11 @@ export class GraphQLClient {
|
|
|
627
722
|
variables,
|
|
628
723
|
...(options.operationName !== undefined
|
|
629
724
|
? { operationName: options.operationName }
|
|
630
|
-
: extractOperationName(query) !==
|
|
725
|
+
: extractOperationName(query) !== null
|
|
631
726
|
? { operationName: extractOperationName(query) }
|
|
632
727
|
: {}),
|
|
633
728
|
};
|
|
634
|
-
const headers = await buildHeaders(this.config, {
|
|
635
|
-
/* drop content-type for multipart */
|
|
636
|
-
});
|
|
729
|
+
const headers = await buildHeaders(this.config, { /* drop content-type for multipart */});
|
|
637
730
|
delete headers["content-type"]; // FormData sets it with boundary
|
|
638
731
|
const form = buildMultipartBody(req, uploads);
|
|
639
732
|
const controller = new AbortController();
|
|
@@ -686,6 +779,12 @@ export class GraphQLClient {
|
|
|
686
779
|
* contains GraphQL errors, or is missing data for any request
|
|
687
780
|
*/
|
|
688
781
|
async batch(requests, options = {}) {
|
|
782
|
+
// `query()` and `upload()` both run the query through the validator before
|
|
783
|
+
// sending; `batch()` did not, so a batch was the one way to bypass the
|
|
784
|
+
// length, depth and injection limits the module applies everywhere else.
|
|
785
|
+
for (const req of requests) {
|
|
786
|
+
validateGraphQLQuery(req.query);
|
|
787
|
+
}
|
|
689
788
|
const headers = await buildHeaders(this.config);
|
|
690
789
|
const controller = new AbortController();
|
|
691
790
|
// Listener removed when the fetch settles — avoids accumulating on the
|
|
@@ -707,13 +806,38 @@ export class GraphQLClient {
|
|
|
707
806
|
finally {
|
|
708
807
|
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
709
808
|
}
|
|
710
|
-
//
|
|
711
|
-
|
|
809
|
+
// Every other path routes a non-JSON body through `parseGraphQLResponse`,
|
|
810
|
+
// which turns it into a typed `EPARSE` / `ENETWORK` GraphQLClientError.
|
|
811
|
+
// `batch()` called `response.json()` bare, so a 502 HTML error page threw a
|
|
812
|
+
// raw `SyntaxError` with no `code` at all — a caller switching on
|
|
813
|
+
// `err.code` got `undefined` and had no way to tell this from a bug.
|
|
814
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
815
|
+
if (!response.ok && !ct.includes("application/json")) {
|
|
816
|
+
const text = await response.text().catch(() => "(unreadable)");
|
|
817
|
+
throw new GraphQLClientError(`HTTP ${response.status}: ${response.statusText}\n${text}`, "ENETWORK", undefined, requests[0], undefined, new Error(`HTTP ${response.status}`));
|
|
818
|
+
}
|
|
819
|
+
let rawResults;
|
|
820
|
+
try {
|
|
821
|
+
// FIX (H6): sanitize untrusted batch response JSON before validation.
|
|
822
|
+
rawResults = sanitizeParsedJSON((await response.json()));
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
throw new GraphQLClientError("Failed to parse batch response as JSON", "EPARSE", undefined, requests[0], undefined, err);
|
|
826
|
+
}
|
|
712
827
|
// Validate response is array (B-5 fix)
|
|
713
828
|
if (!Array.isArray(rawResults)) {
|
|
714
829
|
throw new GraphQLClientError("Batch response must be an array of GraphQL responses", "EINVALIDRESPONSE", undefined, requests[0], rawResults);
|
|
715
830
|
}
|
|
716
831
|
const results = rawResults;
|
|
832
|
+
// The contract is "one `data` per request, in order". A server returning a
|
|
833
|
+
// different number was mapped over anyway: two requests answered with one
|
|
834
|
+
// result silently returned one item, and one request answered with three
|
|
835
|
+
// returned three — with `requests[i]` `undefined` for the extra ones, so an
|
|
836
|
+
// `errors` entry there was attributed to no request at all. Positional
|
|
837
|
+
// alignment is the whole point of a batch, so a mismatch is an error.
|
|
838
|
+
if (results.length !== requests.length) {
|
|
839
|
+
throw new GraphQLClientError(`Batch response has ${results.length} result(s) but ${requests.length} request(s) were sent`, "EINVALIDRESPONSE", undefined, requests[0], rawResults);
|
|
840
|
+
}
|
|
717
841
|
return results.map((res, i) => {
|
|
718
842
|
if (res.errors?.length)
|
|
719
843
|
throw buildGraphQLError(res.errors, requests[i], res);
|
|
@@ -776,7 +900,7 @@ export class GraphQLClient {
|
|
|
776
900
|
...(variables !== undefined ? { variables } : {}),
|
|
777
901
|
...(options.operationName !== undefined
|
|
778
902
|
? { operationName: options.operationName }
|
|
779
|
-
: extractOperationName(query) !==
|
|
903
|
+
: extractOperationName(query) !== null
|
|
780
904
|
? { operationName: extractOperationName(query) }
|
|
781
905
|
: {}),
|
|
782
906
|
};
|
|
@@ -861,6 +985,13 @@ export class GraphQLClient {
|
|
|
861
985
|
});
|
|
862
986
|
}
|
|
863
987
|
async _executeWithRetry(req, signal) {
|
|
988
|
+
// A negative count made the loop condition false on the first evaluation,
|
|
989
|
+
// so the body never ran, `lastErr` was never assigned, and the method
|
|
990
|
+
// ended in `throw undefined` — a rejection carrying no value at all, so
|
|
991
|
+
// `err.message` threw a TypeError inside the caller's own error handler.
|
|
992
|
+
if (!Number.isInteger(this.config.retries) || this.config.retries < 0) {
|
|
993
|
+
throw new RangeError(`retries must be a non-negative integer, got ${this.config.retries}`);
|
|
994
|
+
}
|
|
864
995
|
let lastErr;
|
|
865
996
|
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
866
997
|
if (attempt > 0)
|
|
@@ -878,18 +1009,18 @@ export class GraphQLClient {
|
|
|
878
1009
|
}
|
|
879
1010
|
throw lastErr;
|
|
880
1011
|
}
|
|
881
|
-
async _executeWithAPQ(req, signal) {
|
|
882
|
-
if (!
|
|
883
|
-
return executeHTTP(req,
|
|
1012
|
+
async _executeWithAPQ(req, signal, config = this.config) {
|
|
1013
|
+
if (!config.enableAPQ) {
|
|
1014
|
+
return executeHTTP(req, config, signal, "none", undefined);
|
|
884
1015
|
}
|
|
885
1016
|
// APQ: first try without query string
|
|
886
|
-
const res1 = await executeHTTP(req,
|
|
1017
|
+
const res1 = await executeHTTP(req, config, signal, "omitQuery", (q) => this._getAPQHash(q));
|
|
887
1018
|
// Check if server responded with PersistedQueryNotFound
|
|
888
1019
|
const notFound = res1.errors?.some((e) => e.extensions?.["code"] === "PERSISTED_QUERY_NOT_FOUND");
|
|
889
1020
|
if (!notFound)
|
|
890
1021
|
return res1;
|
|
891
1022
|
// Retry with full query
|
|
892
|
-
return executeHTTP(req,
|
|
1023
|
+
return executeHTTP(req, config, signal, "full", (q) => this._getAPQHash(q));
|
|
893
1024
|
}
|
|
894
1025
|
}
|
|
895
1026
|
// ============================================================================
|
|
@@ -945,7 +1076,9 @@ export function errorLink(handler) {
|
|
|
945
1076
|
catch (err) {
|
|
946
1077
|
if (!(err instanceof GraphQLClientError))
|
|
947
1078
|
throw err;
|
|
948
|
-
|
|
1079
|
+
// Must be awaited: an async handler returning `null` — the documented
|
|
1080
|
+
// way to say "re-throw" — would otherwise be a truthy Promise here.
|
|
1081
|
+
const result = await handler(err, op, () => next(op));
|
|
949
1082
|
if (result)
|
|
950
1083
|
return result;
|
|
951
1084
|
throw err;
|
|
@@ -997,6 +1130,13 @@ export function loggingLink(logger = (msg, data) => console.log(msg, data)) {
|
|
|
997
1130
|
export function retryLink(options = {}) {
|
|
998
1131
|
const max = options.maxRetries ?? 3;
|
|
999
1132
|
const delay = options.delayMs ?? 300;
|
|
1133
|
+
// Same `throw undefined` defect as the client's own `retries`.
|
|
1134
|
+
if (!Number.isInteger(max) || max < 0) {
|
|
1135
|
+
throw new RangeError(`retryLink: maxRetries must be a non-negative integer, got ${max}`);
|
|
1136
|
+
}
|
|
1137
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
1138
|
+
throw new RangeError(`retryLink: delayMs must be a non-negative number, got ${delay}`);
|
|
1139
|
+
}
|
|
1000
1140
|
const should = options.shouldRetry ?? ((err) => !(err instanceof GraphQLClientError && err.isGraphQLError));
|
|
1001
1141
|
return async (op, next) => {
|
|
1002
1142
|
let lastErr;
|