fix: Zoom - More logging and fix for unexpected errors seen in logs (#22118)

* fix: Use WEBAPP_URL_FOR_OAUTH to be able to use http://localhost:3000
for local testing

* In case of JSON.parse crash that could happen if zoom reponsed with xml

* Fix cases where auto_recording and default_password_for_scheduled_meetings could be nullish
This commit is contained in:
Hariom Balhara
2025-06-28 08:11:25 +00:00
committed by GitHub
parent 88a2bd3317
commit 93e183e6c3
4 changed files with 81 additions and 20 deletions
@@ -1,7 +1,12 @@
/**
* Manages OAuth2.0 tokens for an app and resourceOwner. It automatically refreshes the token when needed.
* It is aware of the credential sync endpoint and can sync the token from the third party source.
* It is unaware of Prisma and App logic. It is just a utility to manage OAuth2.0 tokens with life cycle methods
* Manages OAuth2.0 as well as JWT tokens(For JWT tokens, only Google Calendar use it at the moment) for an app and resourceOwner.
* What it does
* - It automatically refreshes the token if needed when making a request.
* - It is aware of the credential sync endpoint and can sync the token from the third party source.
* - It is kept unaware of Prisma and App logic. It is just a utility to manage OAuth2.0 tokens with life cycle methods
*
* What it doesn't do yet
* - It doesn't have a flow to re-send the request if the access-token had been communicated as invalid after making the request itself. It relies on the caller to make the next request in which it will actually refresh the token.
*
* For a recommended usage example, see Zoom VideoApiAdapter.ts
*/
@@ -89,7 +94,13 @@ export class OAuthManager {
currentTokenObject,
fetchNewTokenObject,
updateTokenObject,
/**
* The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
*/
isTokenObjectUnusable,
/**
* The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
*/
isAccessTokenUnusable,
invalidateTokenObject,
expireAccessToken,
+2 -2
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { stringify } from "querystring";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -26,7 +26,7 @@ async function handler(req: NextApiRequest) {
const params = {
response_type: "code",
client_id,
redirect_uri: `${WEBAPP_URL}/api/integrations/zoomvideo/callback`,
redirect_uri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/zoomvideo/callback`,
state,
};
const query = stringify(params);
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code } = req.query;
const { client_id, client_secret } = await getZoomAppKeys();
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/zoomvideo/callback`);
const redirectUri = encodeURI(`${WEBAPP_URL_FOR_OAUTH}/api/integrations/zoomvideo/callback`);
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
const result = await fetch(
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,
@@ -8,6 +8,7 @@ import {
CREDENTIAL_SYNC_SECRET_HEADER_NAME,
} from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { getPiiFreeCalendarEvent } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import { Frequency } from "@calcom/prisma/zod-utils";
@@ -65,12 +66,12 @@ export type ZoomUserSettings = z.infer<typeof zoomUserSettingsSchema>;
export const zoomUserSettingsSchema = z.object({
recording: z
.object({
auto_recording: z.string(),
auto_recording: z.string().nullish(),
})
.nullish(),
schedule_meeting: z
.object({
default_password_for_scheduled_meetings: z.string(),
default_password_for_scheduled_meetings: z.string().nullish(),
})
.nullish(),
});
@@ -185,6 +186,32 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
};
};
/**
* Zoom is known to return xml response in some cases.
* e.g. Wrong request or some special case of invalid token
*/
const handleZoomResponseJsonParseError = async ({
error,
clonedResponse,
}: {
error: unknown;
clonedResponse: Response;
}) => {
// In some cases, Zoom responds with xml response, so we log the response for debugging
// We need to see why that error occurs exactly and then later we decide if mark the access token and token object unusable or not
log.error(
"Error in JSON parsing Zoom API response",
safeStringify({
error: safeStringify(error),
// Log Raw response body here.
responseBody: await clonedResponse.text(),
status: clonedResponse.status,
})
);
return null;
};
const fetchZoomApi = async (endpoint: string, options?: RequestInit) => {
const auth = new OAuthManager({
credentialSyncVariables: {
@@ -220,9 +247,18 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
},
isTokenObjectUnusable: async function (response) {
const myLog = logger.getSubLogger({ prefix: ["zoomvideo:isTokenObjectUnusable"] });
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
if (!response.ok || (response.status < 200 && response.status >= 300)) {
const responseBody = await response.json();
myLog.info(safeStringify({ status: response.status, ok: response.ok }));
if (!response.ok) {
let responseBody;
const responseToUseInCaseOfError = response.clone();
try {
responseBody = await response.json();
} catch (e) {
return await handleZoomResponseJsonParseError({
error: e,
clonedResponse: responseToUseInCaseOfError,
});
}
myLog.debug(safeStringify({ responseBody }));
if (responseBody.error === "invalid_grant") {
@@ -233,11 +269,20 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
},
isAccessTokenUnusable: async function (response) {
const myLog = logger.getSubLogger({ prefix: ["zoomvideo:isAccessTokenUnusable"] });
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
if (!response.ok || (response.status < 200 && response.status >= 300)) {
const responseBody = await response.json();
myLog.info(safeStringify({ status: response.status, ok: response.ok }));
if (!response.ok) {
let responseBody;
const responseToUseInCaseOfError = response.clone();
try {
responseBody = await response.json();
} catch (e) {
return await handleZoomResponseJsonParseError({
error: e,
clonedResponse: responseToUseInCaseOfError,
});
}
myLog.debug(safeStringify({ responseBody }));
// 124 is the error code for invalid access token from Zoom API
if (responseBody.code === 124) {
return { reason: responseBody.message ?? "" };
}
@@ -284,7 +329,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
end: new Date(new Date(meeting.start_time).getTime() + meeting.duration * 60000).toISOString(),
}));
} catch (err) {
console.error(err);
log.error("Failed to get availability", safeStringify(err));
/* Prevents booking failure when Zoom Token is expired */
return [];
}
@@ -311,8 +356,10 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
}
throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
} catch (err) {
console.error(err);
log.error("Zoom meeting creation failed", safeStringify({ error: err, event }));
log.error(
"Zoom meeting creation failed",
safeStringify({ error: safeStringify(err), event: getPiiFreeCalendarEvent(event) })
);
/* Prevents meeting creation failure when Zoom Token is expired */
throw new Error("Unexpected error");
}
@@ -347,7 +394,10 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
url: result.join_url,
};
} catch (err) {
log.error("Failed to update meeting", safeStringify(err));
log.error(
"Failed to update meeting",
safeStringify({ error: err, event: getPiiFreeCalendarEvent(event) })
);
return Promise.reject(new Error("Failed to update meeting"));
}
},