Skip to content

Commit 93e183e

Browse files
fix: Zoom - More logging and fix for unexpected errors seen in logs (calcom#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
1 parent 88a2bd3 commit 93e183e

4 files changed

Lines changed: 81 additions & 20 deletions

File tree

packages/app-store/_utils/oauth/OAuthManager.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/**
2-
* Manages OAuth2.0 tokens for an app and resourceOwner. It automatically refreshes the token when needed.
3-
* It is aware of the credential sync endpoint and can sync the token from the third party source.
4-
* It is unaware of Prisma and App logic. It is just a utility to manage OAuth2.0 tokens with life cycle methods
2+
* Manages OAuth2.0 as well as JWT tokens(For JWT tokens, only Google Calendar use it at the moment) for an app and resourceOwner.
3+
* What it does
4+
* - It automatically refreshes the token if needed when making a request.
5+
* - It is aware of the credential sync endpoint and can sync the token from the third party source.
6+
* - It is kept unaware of Prisma and App logic. It is just a utility to manage OAuth2.0 tokens with life cycle methods
7+
*
8+
* What it doesn't do yet
9+
* - 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.
510
*
611
* For a recommended usage example, see Zoom VideoApiAdapter.ts
712
*/
@@ -89,7 +94,13 @@ export class OAuthManager {
8994
currentTokenObject,
9095
fetchNewTokenObject,
9196
updateTokenObject,
97+
/**
98+
* The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
99+
*/
92100
isTokenObjectUnusable,
101+
/**
102+
* The fn must not crash. It is the responsibility of the caller to handle any error and appropriately decide what to return
103+
*/
93104
isAccessTokenUnusable,
94105
invalidateTokenObject,
95106
expireAccessToken,

packages/app-store/zoomvideo/api/add.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { NextApiRequest } from "next";
22
import { stringify } from "querystring";
33

4-
import { WEBAPP_URL } from "@calcom/lib/constants";
4+
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
55
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
66
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
77
import prisma from "@calcom/prisma";
@@ -26,7 +26,7 @@ async function handler(req: NextApiRequest) {
2626
const params = {
2727
response_type: "code",
2828
client_id,
29-
redirect_uri: `${WEBAPP_URL}/api/integrations/zoomvideo/callback`,
29+
redirect_uri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/zoomvideo/callback`,
3030
state,
3131
};
3232
const query = stringify(params);

packages/app-store/zoomvideo/api/callback.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { NextApiRequest, NextApiResponse } from "next";
22

3-
import { WEBAPP_URL } from "@calcom/lib/constants";
3+
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
44
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
55
import prisma from "@calcom/prisma";
66

@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
1414
const { code } = req.query;
1515
const { client_id, client_secret } = await getZoomAppKeys();
1616

17-
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/zoomvideo/callback`);
17+
const redirectUri = encodeURI(`${WEBAPP_URL_FOR_OAUTH}/api/integrations/zoomvideo/callback`);
1818
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
1919
const result = await fetch(
2020
`https://zoom.us/oauth/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`,

packages/app-store/zoomvideo/lib/VideoApiAdapter.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CREDENTIAL_SYNC_SECRET_HEADER_NAME,
99
} from "@calcom/lib/constants";
1010
import logger from "@calcom/lib/logger";
11+
import { getPiiFreeCalendarEvent } from "@calcom/lib/piiFreeData";
1112
import { safeStringify } from "@calcom/lib/safeStringify";
1213
import prisma from "@calcom/prisma";
1314
import { Frequency } from "@calcom/prisma/zod-utils";
@@ -65,12 +66,12 @@ export type ZoomUserSettings = z.infer<typeof zoomUserSettingsSchema>;
6566
export const zoomUserSettingsSchema = z.object({
6667
recording: z
6768
.object({
68-
auto_recording: z.string(),
69+
auto_recording: z.string().nullish(),
6970
})
7071
.nullish(),
7172
schedule_meeting: z
7273
.object({
73-
default_password_for_scheduled_meetings: z.string(),
74+
default_password_for_scheduled_meetings: z.string().nullish(),
7475
})
7576
.nullish(),
7677
});
@@ -185,6 +186,32 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
185186
};
186187
};
187188

189+
/**
190+
* Zoom is known to return xml response in some cases.
191+
* e.g. Wrong request or some special case of invalid token
192+
*/
193+
const handleZoomResponseJsonParseError = async ({
194+
error,
195+
clonedResponse,
196+
}: {
197+
error: unknown;
198+
clonedResponse: Response;
199+
}) => {
200+
// In some cases, Zoom responds with xml response, so we log the response for debugging
201+
// 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
202+
log.error(
203+
"Error in JSON parsing Zoom API response",
204+
safeStringify({
205+
error: safeStringify(error),
206+
// Log Raw response body here.
207+
responseBody: await clonedResponse.text(),
208+
status: clonedResponse.status,
209+
})
210+
);
211+
212+
return null;
213+
};
214+
188215
const fetchZoomApi = async (endpoint: string, options?: RequestInit) => {
189216
const auth = new OAuthManager({
190217
credentialSyncVariables: {
@@ -220,9 +247,18 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
220247
},
221248
isTokenObjectUnusable: async function (response) {
222249
const myLog = logger.getSubLogger({ prefix: ["zoomvideo:isTokenObjectUnusable"] });
223-
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
224-
if (!response.ok || (response.status < 200 && response.status >= 300)) {
225-
const responseBody = await response.json();
250+
myLog.info(safeStringify({ status: response.status, ok: response.ok }));
251+
if (!response.ok) {
252+
let responseBody;
253+
const responseToUseInCaseOfError = response.clone();
254+
try {
255+
responseBody = await response.json();
256+
} catch (e) {
257+
return await handleZoomResponseJsonParseError({
258+
error: e,
259+
clonedResponse: responseToUseInCaseOfError,
260+
});
261+
}
226262
myLog.debug(safeStringify({ responseBody }));
227263

228264
if (responseBody.error === "invalid_grant") {
@@ -233,11 +269,20 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
233269
},
234270
isAccessTokenUnusable: async function (response) {
235271
const myLog = logger.getSubLogger({ prefix: ["zoomvideo:isAccessTokenUnusable"] });
236-
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
237-
if (!response.ok || (response.status < 200 && response.status >= 300)) {
238-
const responseBody = await response.json();
272+
myLog.info(safeStringify({ status: response.status, ok: response.ok }));
273+
if (!response.ok) {
274+
let responseBody;
275+
const responseToUseInCaseOfError = response.clone();
276+
try {
277+
responseBody = await response.json();
278+
} catch (e) {
279+
return await handleZoomResponseJsonParseError({
280+
error: e,
281+
clonedResponse: responseToUseInCaseOfError,
282+
});
283+
}
239284
myLog.debug(safeStringify({ responseBody }));
240-
285+
// 124 is the error code for invalid access token from Zoom API
241286
if (responseBody.code === 124) {
242287
return { reason: responseBody.message ?? "" };
243288
}
@@ -284,7 +329,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
284329
end: new Date(new Date(meeting.start_time).getTime() + meeting.duration * 60000).toISOString(),
285330
}));
286331
} catch (err) {
287-
console.error(err);
332+
log.error("Failed to get availability", safeStringify(err));
288333
/* Prevents booking failure when Zoom Token is expired */
289334
return [];
290335
}
@@ -311,8 +356,10 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
311356
}
312357
throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
313358
} catch (err) {
314-
console.error(err);
315-
log.error("Zoom meeting creation failed", safeStringify({ error: err, event }));
359+
log.error(
360+
"Zoom meeting creation failed",
361+
safeStringify({ error: safeStringify(err), event: getPiiFreeCalendarEvent(event) })
362+
);
316363
/* Prevents meeting creation failure when Zoom Token is expired */
317364
throw new Error("Unexpected error");
318365
}
@@ -347,7 +394,10 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
347394
url: result.join_url,
348395
};
349396
} catch (err) {
350-
log.error("Failed to update meeting", safeStringify(err));
397+
log.error(
398+
"Failed to update meeting",
399+
safeStringify({ error: err, event: getPiiFreeCalendarEvent(event) })
400+
);
351401
return Promise.reject(new Error("Failed to update meeting"));
352402
}
353403
},

0 commit comments

Comments
 (0)