From b17968b61f0e35b1ba20d081dacee66af8225491 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 3 Feb 2025 10:50:24 -0700 Subject: [PATCH 1/3] Allow protocol errors to be retryable in `RetryLink` (#12318) --- .changeset/thin-oranges-laugh.md | 18 ++++++++ src/link/retry/__tests__/retryLink.ts | 66 ++++++++++++++++++++++++++- src/link/retry/retryLink.ts | 21 ++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-oranges-laugh.md diff --git a/.changeset/thin-oranges-laugh.md b/.changeset/thin-oranges-laugh.md new file mode 100644 index 00000000000..9cda6f16002 --- /dev/null +++ b/.changeset/thin-oranges-laugh.md @@ -0,0 +1,18 @@ +--- +"@apollo/client": patch +--- + +Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. + +```js +const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + if (error instanceof ApolloError) { + // errors available on the `protocolErrors` field in `ApolloError` + console.log(error.protocolErrors) + } + + return true; + } +}); +``` diff --git a/src/link/retry/__tests__/retryLink.ts b/src/link/retry/__tests__/retryLink.ts index 85955021588..973b8ab92e6 100644 --- a/src/link/retry/__tests__/retryLink.ts +++ b/src/link/retry/__tests__/retryLink.ts @@ -5,7 +5,11 @@ import { execute } from "../../core/execute"; import { Observable } from "../../../utilities/observables/Observable"; import { fromError } from "../../utils/fromError"; import { RetryLink } from "../retryLink"; -import { ObservableStream } from "../../../testing/internal"; +import { + mockMultipartSubscriptionStream, + ObservableStream, +} from "../../../testing/internal"; +import { ApolloError } from "../../../core"; const query = gql` { @@ -210,4 +214,64 @@ describe("RetryLink", () => { [3, operation, standardError], ]); }); + + it("handles protocol errors from multipart subscriptions", async () => { + const subscription = gql` + subscription MySubscription { + aNewDieWasCreated { + die { + roll + sides + color + } + } + } + `; + + const attemptStub = jest.fn(); + attemptStub.mockReturnValueOnce(true); + + const retryLink = new RetryLink({ + delay: { initial: 1 }, + attempts: attemptStub, + }); + + const { httpLink, enqueuePayloadResult, enqueueProtocolErrors } = + mockMultipartSubscriptionStream(); + const link = ApolloLink.from([retryLink, httpLink]); + const stream = new ObservableStream(execute(link, { query: subscription })); + + enqueueProtocolErrors([ + { message: "Error field", extensions: { code: "INTERNAL_SERVER_ERROR" } }, + ]); + + enqueuePayloadResult({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + await expect(stream).toEmitValue({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + expect(attemptStub).toHaveBeenCalledTimes(1); + expect(attemptStub).toHaveBeenCalledWith( + 1, + expect.objectContaining({ + operationName: "MySubscription", + query: subscription, + }), + new ApolloError({ + protocolErrors: [ + { + message: "Error field", + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }, + ], + }) + ); + }); }); diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts index cde2dd2ea9c..37c293d99df 100644 --- a/src/link/retry/retryLink.ts +++ b/src/link/retry/retryLink.ts @@ -7,6 +7,11 @@ import { buildDelayFunction } from "./delayFunction.js"; import type { RetryFunction, RetryFunctionOptions } from "./retryFunction.js"; import { buildRetryFunction } from "./retryFunction.js"; import type { SubscriptionObserver } from "zen-observable-ts"; +import { + ApolloError, + graphQLResultHasProtocolErrors, + PROTOCOL_ERRORS_SYMBOL, +} from "../../errors/index.js"; export namespace RetryLink { export interface Options { @@ -54,7 +59,21 @@ class RetryableOperation { private try() { this.currentSubscription = this.forward(this.operation).subscribe({ - next: this.observer.next.bind(this.observer), + next: (result) => { + if (graphQLResultHasProtocolErrors(result)) { + this.onError( + new ApolloError({ + protocolErrors: result.extensions[PROTOCOL_ERRORS_SYMBOL], + }) + ); + // Unsubscribe from the current subscription to prevent the `complete` + // handler to be called as a result of the stream closing. + this.currentSubscription?.unsubscribe(); + return; + } + + this.observer.next(result); + }, error: this.onError, complete: this.observer.complete.bind(this.observer), }); From 1641eeb04c3f857f40a890f6543126f020941a41 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:55:51 -0700 Subject: [PATCH 2/3] Version Packages (#12325) Co-authored-by: github-actions[bot] --- .changeset/curvy-seahorses-walk.md | 5 ----- .changeset/thin-oranges-laugh.md | 18 ------------------ CHANGELOG.md | 21 +++++++++++++++++++++ package-lock.json | 6 +++--- package.json | 2 +- 5 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 .changeset/curvy-seahorses-walk.md delete mode 100644 .changeset/thin-oranges-laugh.md diff --git a/.changeset/curvy-seahorses-walk.md b/.changeset/curvy-seahorses-walk.md deleted file mode 100644 index 377f9ee5803..00000000000 --- a/.changeset/curvy-seahorses-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. diff --git a/.changeset/thin-oranges-laugh.md b/.changeset/thin-oranges-laugh.md deleted file mode 100644 index 9cda6f16002..00000000000 --- a/.changeset/thin-oranges-laugh.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@apollo/client": patch ---- - -Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. - -```js -const retryLink = new RetryLink({ - attempts: (count, operation, error) => { - if (error instanceof ApolloError) { - // errors available on the `protocolErrors` field in `ApolloError` - console.log(error.protocolErrors) - } - - return true; - } -}); -``` diff --git a/CHANGELOG.md b/CHANGELOG.md index fb62dad7b77..de09aed465e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # @apollo/client +## 3.12.9 + +### Patch Changes + +- [#12321](https://github.com/apollographql/apollo-client/pull/12321) [`daa4f33`](https://github.com/apollographql/apollo-client/commit/daa4f3303cfb81e8dca66c21ce3f3dc24946cafb) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. + +- [#12318](https://github.com/apollographql/apollo-client/pull/12318) [`b17968b`](https://github.com/apollographql/apollo-client/commit/b17968b61f0e35b1ba20d081dacee66af8225491) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. + + ```js + const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + if (error instanceof ApolloError) { + // errors available on the `protocolErrors` field in `ApolloError` + console.log(error.protocolErrors); + } + + return true; + }, + }); + ``` + ## 3.12.8 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 4949c78795e..b8452c3f72e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -25,7 +25,7 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@actions/github-script": "github:actions/github-script#v7", + "@actions/github-script": "github:actions/github-script#v7.0.1", "@arethetypeswrong/cli": "0.15.3", "@ark/attest": "0.28.0", "@babel/parser": "7.25.0", diff --git a/package.json b/package.json index b5ad466cc0e..de921768aa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 92db870d5ee8307ba85dd743a70f77b7e5154183 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:57:14 +0000 Subject: [PATCH 3/3] Prepare for rc release --- .changeset/pre.json | 4 ++-- package-lock.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 461875c7499..ba55303eb3c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,8 +1,8 @@ { "mode": "pre", - "tag": "alpha", + "tag": "rc", "initialVersions": { "@apollo/client": "3.12.2" }, "changesets": [] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4949c78795e..47ad09c384c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@actions/github-script": "github:actions/github-script#v7", + "@actions/github-script": "github:actions/github-script#v7.0.1", "@arethetypeswrong/cli": "0.15.3", "@ark/attest": "0.28.0", "@babel/parser": "7.25.0",