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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user