Skip to content

Commit

Permalink
chore: handle Event sent on WS error and add tests for error cases (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alessbell authored Mar 31, 2023
1 parent c4e2a14 commit 4175af5
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-elephants-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Improve WebSocket error handling for generic `Event` received on error. For more information see [https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event).
72 changes: 71 additions & 1 deletion src/link/subscriptions/__tests__/graphqlWsLink.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Client } from "graphql-ws";
import { ExecutionResult } from "graphql";
import { ExecutionResult, GraphQLError } from "graphql";
import gql from "graphql-tag";

import { Observable } from "../../../utilities";
import { ApolloError } from "../../../errors";
import { execute } from "../../core";
import { GraphQLWsLink } from "..";

Expand Down Expand Up @@ -104,4 +105,73 @@ describe("GraphQLWSlink", () => {
const obs = execute(link, { query: subscription });
await expect(observableToArray(obs)).resolves.toEqual(results);
});

describe("should reject", () => {
it("with Error on subscription error via Error", async () => {
const subscribe: Client["subscribe"] = (_, sink) => {
sink.error(new Error("an error occurred"));
return () => {};
};
const client = mockClient(subscribe);
const link = new GraphQLWsLink(client);

const obs = execute(link, { query: subscription });
await expect(observableToArray(obs)).rejects.toEqual(
new Error("an error occurred")
);
});

it("with Error on subscription error via CloseEvent", async () => {
const subscribe: Client["subscribe"] = (_, sink) => {
// A WebSocket close event receives a CloseEvent
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
sink.error(
new CloseEvent("an error occurred", {
code: 1006,
reason: "abnormally closed",
})
);
return () => {};
};
const client = mockClient(subscribe);
const link = new GraphQLWsLink(client);

const obs = execute(link, { query: subscription });
await expect(observableToArray(obs)).rejects.toEqual(
new Error("Socket closed with event 1006 abnormally closed")
);
});

it("with ApolloError on subscription error via Event (network disconnected)", async () => {
const subscribe: Client["subscribe"] = (_, sink) => {
// A WebSocket error event receives a generic Event
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
sink.error({ target: { readyState: WebSocket.CLOSED } });
return () => {};
};
const client = mockClient(subscribe);
const link = new GraphQLWsLink(client);

const obs = execute(link, { query: subscription });
await expect(observableToArray(obs)).rejects.toEqual(
new Error("Socket closed")
);
});

it("with ApolloError on subscription error via GraphQLError[]", async () => {
const subscribe: Client["subscribe"] = (_, sink) => {
sink.error([new GraphQLError("Foo bar.")]);
return () => {};
};
const client = mockClient(subscribe);
const link = new GraphQLWsLink(client);

const obs = execute(link, { query: subscription });
await expect(observableToArray(obs)).rejects.toEqual(
new ApolloError({
graphQLErrors: [new GraphQLError("Foo bar.")],
})
);
});
});
});
26 changes: 13 additions & 13 deletions src/link/subscriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,16 @@ import { ApolloLink, Operation, FetchResult } from "../core";
import { isNonNullObject, Observable } from "../../utilities";
import { ApolloError } from "../../errors";

interface LikeCloseEvent {
/** Returns the WebSocket connection close code provided by the server. */
readonly code: number;
/** Returns the WebSocket connection close reason provided by the server. */
readonly reason: string;
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
function isLikeCloseEvent(val: unknown): val is CloseEvent {
return isNonNullObject(val) && "code" in val && "reason" in val;
}

function isLikeCloseEvent(val: unknown): val is LikeCloseEvent {
return isNonNullObject(val) && 'code' in val && 'reason' in val;
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
function isLikeErrorEvent(err: unknown): err is Event {
return isNonNullObject(err) && err.target?.readyState === WebSocket.CLOSED;
}


export class GraphQLWsLink extends ApolloLink {
constructor(public readonly client: Client) {
super();
Expand All @@ -63,13 +61,15 @@ export class GraphQLWsLink extends ApolloLink {
if (err instanceof Error) {
return observer.error(err);
}

if (isLikeCloseEvent(err)) {
const likeClose = isLikeCloseEvent(err);
if (likeClose || isLikeErrorEvent(err)) {
return observer.error(
// reason will be available on clean closes
new Error(
`Socket closed with event ${err.code} ${err.reason || ""}`
)
new Error(`Socket closed${
likeClose ? ` with event ${err.code}` : ""
}${
likeClose ? ` ${err.reason}` : ""
}`)
);
}

Expand Down

0 comments on commit 4175af5

Please sign in to comment.