Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix(core): support non-spec errors (#3395)

authored by

Jovi De Croock and committed by
GitHub
c35e7a98 052d5f49

+177 -1
+6
.changeset/olive-schools-prove.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Support non spec-compliant error bodies, i.e. the Shopify API does return `errors` but as an object. Adding 6 + a check whether we are really dealing with an Array of errors enables this.
+146
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
··· 438 438 } 439 439 `; 440 440 441 + exports[`on error with non spec-compliant body > handles network errors 1`] = ` 442 + { 443 + "data": undefined, 444 + "error": [CombinedError: [Network] Forbidden], 445 + "extensions": undefined, 446 + "hasNext": false, 447 + "operation": { 448 + "context": { 449 + "fetchOptions": { 450 + "method": "POST", 451 + }, 452 + "requestPolicy": "cache-first", 453 + "url": "http://localhost:3000/graphql", 454 + }, 455 + "key": 2, 456 + "kind": "query", 457 + "query": { 458 + "__key": -2395444236, 459 + "definitions": [ 460 + { 461 + "directives": [], 462 + "kind": "OperationDefinition", 463 + "name": { 464 + "kind": "Name", 465 + "value": "getUser", 466 + }, 467 + "operation": "query", 468 + "selectionSet": { 469 + "kind": "SelectionSet", 470 + "selections": [ 471 + { 472 + "alias": undefined, 473 + "arguments": [ 474 + { 475 + "kind": "Argument", 476 + "name": { 477 + "kind": "Name", 478 + "value": "name", 479 + }, 480 + "value": { 481 + "kind": "Variable", 482 + "name": { 483 + "kind": "Name", 484 + "value": "name", 485 + }, 486 + }, 487 + }, 488 + ], 489 + "directives": [], 490 + "kind": "Field", 491 + "name": { 492 + "kind": "Name", 493 + "value": "user", 494 + }, 495 + "selectionSet": { 496 + "kind": "SelectionSet", 497 + "selections": [ 498 + { 499 + "alias": undefined, 500 + "arguments": [], 501 + "directives": [], 502 + "kind": "Field", 503 + "name": { 504 + "kind": "Name", 505 + "value": "id", 506 + }, 507 + "selectionSet": undefined, 508 + }, 509 + { 510 + "alias": undefined, 511 + "arguments": [], 512 + "directives": [], 513 + "kind": "Field", 514 + "name": { 515 + "kind": "Name", 516 + "value": "firstName", 517 + }, 518 + "selectionSet": undefined, 519 + }, 520 + { 521 + "alias": undefined, 522 + "arguments": [], 523 + "directives": [], 524 + "kind": "Field", 525 + "name": { 526 + "kind": "Name", 527 + "value": "lastName", 528 + }, 529 + "selectionSet": undefined, 530 + }, 531 + ], 532 + }, 533 + }, 534 + ], 535 + }, 536 + "variableDefinitions": [ 537 + { 538 + "defaultValue": undefined, 539 + "directives": [], 540 + "kind": "VariableDefinition", 541 + "type": { 542 + "kind": "NamedType", 543 + "name": { 544 + "kind": "Name", 545 + "value": "String", 546 + }, 547 + }, 548 + "variable": { 549 + "kind": "Variable", 550 + "name": { 551 + "kind": "Name", 552 + "value": "name", 553 + }, 554 + }, 555 + }, 556 + ], 557 + }, 558 + ], 559 + "kind": "Document", 560 + "loc": { 561 + "end": 92, 562 + "source": { 563 + "body": "query getUser($name: String) { 564 + user(name: $name) { 565 + id 566 + firstName 567 + lastName 568 + } 569 + }", 570 + "locationOffset": { 571 + "column": 1, 572 + "line": 1, 573 + }, 574 + "name": "gql", 575 + }, 576 + "start": 0, 577 + }, 578 + }, 579 + "variables": { 580 + "name": "Clara", 581 + }, 582 + }, 583 + "stale": false, 584 + } 585 + `; 586 + 441 587 exports[`on success > returns response data 1`] = ` 442 588 { 443 589 "data": {
+21
packages/core/src/internal/fetchSource.test.ts
··· 172 172 }); 173 173 }); 174 174 175 + describe('on error with non spec-compliant body', () => { 176 + beforeEach(() => { 177 + fetch.mockResolvedValue({ 178 + status: 400, 179 + statusText: 'Forbidden', 180 + headers: { get: () => 'application/json' }, 181 + text: vi.fn().mockResolvedValue('{"errors":{"detail":"Bad Request"}}'), 182 + }); 183 + }); 184 + 185 + it('handles network errors', async () => { 186 + const data = await pipe( 187 + makeFetchSource(queryOperation, 'https://test.com/graphql', {}), 188 + toPromise 189 + ); 190 + 191 + expect(data).toMatchSnapshot(); 192 + expect(data).toHaveProperty('error.networkError.message', 'Forbidden'); 193 + }); 194 + }); 195 + 175 196 describe('on teardown', () => { 176 197 const fail = () => { 177 198 expect(true).toEqual(false);
+4 -1
packages/core/src/utils/result.ts
··· 28 28 result: ExecutionResult, 29 29 response?: any 30 30 ): OperationResult => { 31 - if (!('data' in result) && !('errors' in result)) { 31 + if ( 32 + !('data' in result) && 33 + (!('errors' in result) || !Array.isArray(result.errors)) 34 + ) { 32 35 throw new Error('No Content'); 33 36 } 34 37