Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Zendesk) - improve error handling #9578

Merged
merged 1 commit into from
Dec 20, 2024
Merged
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
9 changes: 9 additions & 0 deletions connectors/src/connectors/zendesk/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ export function isZendeskExpiredCursorError(
);
}

/**
* Catches 404 errors that were already caught in fetchFromZendeskWithRetries and rethrown as ZendeskApiErrors.
* The idea is that we only try/catch the part where we call the API, without wrapping any of our code and from then
* only certain functions can actually handle 404 by returning a null.
*/
export function isZendeskNotFoundError(err: unknown): boolean {
return err instanceof ZendeskApiError && err.status === 404;
}

export function isNodeZendeskEpipeError(err: unknown): err is NodeZendeskError {
return (
typeof err === "object" &&
Expand Down
34 changes: 23 additions & 11 deletions connectors/src/connectors/zendesk/lib/zendesk_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
ZendeskFetchedTicket,
ZendeskFetchedUser,
} from "@connectors/@types/node-zendesk";
import { ZendeskApiError } from "@connectors/connectors/zendesk/lib/errors";
import {
isZendeskNotFoundError,
ZendeskApiError,
} from "@connectors/connectors/zendesk/lib/errors";
import { setTimeoutAsync } from "@connectors/lib/async_utils";
import logger from "@connectors/logger/logger";
import type { ZendeskCategoryResource } from "@connectors/resources/zendesk_resources";
Expand Down Expand Up @@ -118,6 +121,7 @@ async function handleZendeskRateLimit(response: Response): Promise<boolean> {

/**
* Runs a GET request to the Zendesk API with a maximum number of retries before throwing.
* TODO(2024-12-20): add some basic io-ts validation here (pass a codec as argument and decode with it)
*/
async function fetchFromZendeskWithRetries({
url,
Expand Down Expand Up @@ -155,19 +159,13 @@ async function fetchFromZendeskWithRetries({
try {
response = await rawResponse.json();
} catch (e) {
if (rawResponse.status === 404) {
return null;
}
throw new ZendeskApiError(
"Error parsing Zendesk API response",
rawResponse.status,
rawResponse
);
}
if (!rawResponse.ok) {
if (rawResponse.status === 404) {
return null;
}
throw new ZendeskApiError(
"Zendesk API error.",
rawResponse.status,
Expand All @@ -191,8 +189,15 @@ export async function fetchZendeskBrand({
brandId: number;
}): Promise<ZendeskFetchedBrand | null> {
const url = `https://${subdomain}.zendesk.com/api/v2/brands/${brandId}`;
const response = await fetchFromZendeskWithRetries({ url, accessToken });
return response?.brand ?? null;
try {
const response = await fetchFromZendeskWithRetries({ url, accessToken });
return response?.brand ?? null;
} catch (e) {
if (isZendeskNotFoundError(e)) {
return null;
}
throw e;
}
}

/**
Expand All @@ -208,8 +213,15 @@ export async function fetchZendeskArticle({
articleId: number;
}): Promise<ZendeskFetchedArticle | null> {
const url = `https://${brandSubdomain}.zendesk.com/api/v2/help_center/articles/${articleId}`;
const response = await fetchFromZendeskWithRetries({ url, accessToken });
return response?.article ?? null;
try {
const response = await fetchFromZendeskWithRetries({ url, accessToken });
return response?.article ?? null;
} catch (e) {
if (isZendeskNotFoundError(e)) {
return null;
}
throw e;
}
}

/**
Expand Down
Loading