Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/GraphQL/Client/BaseClients/Apollo.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,44 @@ const createClientWithWebsockets = function (opts) {
},
});

// Close codes that indicate expected/benign disconnects
// 1000: Normal closure, 1001: Going away (tab close, navigation, mobile backgrounding)
const expectedCloseCodes = new Set([1000, 1001]);

const wsLink = new GraphQLWsLink(
createWsClient({
webSocketImpl: globalThis.WebSocket,
url: opts.websocketUrl,
timeout: 30000,
retryAttempts: Infinity,
shouldRetry: () => true,
connectionParams: {
headers: opts.authToken
? { Authorization: `Bearer ${opts.authToken}` }
: {},
},
on: {
closed: (/** @type {CloseEvent} */ event) => {
const code = /** @type {number | undefined} */ (event?.code);
const reason = /** @type {string | undefined} */ (event?.reason);
const wasClean = /** @type {boolean | undefined} */ (event?.wasClean);
if (expectedCloseCodes.has(code)) {
return;
}
console.warn("[graphql-ws] Socket closed unexpectedly", {
code,
reason,
wasClean,
url: opts.websocketUrl,
});
},
error: (error) => {
console.error("[graphql-ws] WebSocket error", {
error,
url: opts.websocketUrl,
});
},
},
}),
);

Expand Down
25 changes: 25 additions & 0 deletions src/GraphQL/Client/Query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Error codes where the request definitely never reached the server
const noConnectionErrorCodes = new Set([
"ECONNREFUSED",
"ENETUNREACH",
]);
Comment on lines +2 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
const noConnectionErrorCodes = new Set([
"ECONNREFUSED",
"ENETUNREACH",
]);
const noConnectionErrorCodes = new Set(["ECONNREFUSED", "ENETUNREACH"]);


// Error codes where the connection may have been established
// before failing, so the server may have received the request
const ambiguousConnectionErrorCodes = new Set([
"ETIMEDOUT",
"ECONNRESET",
]);
Comment on lines +9 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
const ambiguousConnectionErrorCodes = new Set([
"ETIMEDOUT",
"ECONNRESET",
]);
const ambiguousConnectionErrorCodes = new Set(["ETIMEDOUT", "ECONNRESET"]);


const getCode = (error) => {
const code = error?.cause?.code;
return typeof code === "string" ? code : "";
};

export const isNoConnectionError = (error) =>
noConnectionErrorCodes.has(getCode(error));

export const isConnectionError = (error) => {
const code = getCode(error);
return noConnectionErrorCodes.has(code) || ambiguousConnectionErrorCodes.has(code);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[prettier] reported by reviewdog 🐶

Suggested change
return noConnectionErrorCodes.has(code) || ambiguousConnectionErrorCodes.has(code);
return (
noConnectionErrorCodes.has(code) || ambiguousConnectionErrorCodes.has(code)
);

};
19 changes: 15 additions & 4 deletions src/GraphQL/Client/Query.purs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import GraphQL.Client.Types (class GqlQuery, class QueryClient, Client(..), GqlR
import GraphQL.Client.Variables (class VarsTypeChecked, getVarsJson, getVarsTypeNames)
import Type.Proxy (Proxy(..))

foreign import isConnectionError :: Error -> Boolean
foreign import isNoConnectionError :: Error -> Boolean

-- | Run a graphQL query with a custom decoder and custom options
queryOptsWithDecoder
:: forall client directives schema query returns queryOpts mutationOpts sr
Expand Down Expand Up @@ -199,8 +202,12 @@ runQuery
-> Aff returns
runQuery decodeFn opts client _ queryNameUnsafe q =
addErrorInfo (Proxy @schema) queryName q do
json <- clientQuery opts client queryName (getVarsTypeNames (Proxy :: _ schema) q <> toGqlQueryString q)
(getVarsJson (Proxy :: _ schema) q)
let doQuery = clientQuery opts client queryName (getVarsTypeNames (Proxy :: _ schema) q <> toGqlQueryString q)
(getVarsJson (Proxy :: _ schema) q)
json <- doQuery `catchError` \err ->
-- Retry once on connection-level errors (ETIMEDOUT, ECONNREFUSED, etc.)
if isConnectionError err then doQuery
else throwError err
decodeJsonData decodeFn json
where
queryName = safeQueryName queryNameUnsafe
Expand All @@ -218,8 +225,12 @@ runMutation
-> Aff returns
runMutation decodeFn opts client _ queryNameUnsafe q =
addErrorInfo (Proxy @schema) queryName q do
json <- clientMutation opts client queryName (getVarsTypeNames (Proxy :: _ schema) q <> toGqlQueryString q)
(getVarsJson (Proxy :: _ schema) q)
let doMutation = clientMutation opts client queryName (getVarsTypeNames (Proxy :: _ schema) q <> toGqlQueryString q)
(getVarsJson (Proxy :: _ schema) q)
json <- doMutation `catchError` \err ->
-- Only retry on ECONNREFUSED/ENETUNREACH where the request never reached the server
if isNoConnectionError err then doMutation
else throwError err
decodeJsonData decodeFn json
where
queryName = safeQueryName queryNameUnsafe
Expand Down
Loading