From 9d02e1300c434bf05803644142546ac80376c531 Mon Sep 17 00:00:00 2001
From: Xiao Yijun <xiaoyijun@silverhand.io>
Date: Tue, 25 Jul 2023 14:24:56 +0800
Subject: [PATCH] fix(console): avoid reading response error body more than
 once (#4223)

---
 .../console/src/cloud/hooks/use-cloud-api.ts  | 19 ++++++++++++++-----
 packages/console/src/utils/subscription.ts    | 12 ++++--------
 2 files changed, 18 insertions(+), 13 deletions(-)

diff --git a/packages/console/src/cloud/hooks/use-cloud-api.ts b/packages/console/src/cloud/hooks/use-cloud-api.ts
index c768fc8c5..56c9b9cf2 100644
--- a/packages/console/src/cloud/hooks/use-cloud-api.ts
+++ b/packages/console/src/cloud/hooks/use-cloud-api.ts
@@ -1,6 +1,6 @@
 import type router from '@logto/cloud/routes';
 import { useLogto } from '@logto/react';
-import { conditional } from '@silverhand/essentials';
+import { conditional, trySafe } from '@silverhand/essentials';
 import Client, { ResponseError } from '@withtyped/client';
 import { useMemo } from 'react';
 import { toast } from 'react-hot-toast';
@@ -8,15 +8,24 @@ import { z } from 'zod';
 
 import { cloudApi } from '@/consts';
 
-export const responseErrorBodyGuard = z.object({
+const responseErrorBodyGuard = z.object({
   message: z.string(),
 });
 
+export const tryReadResponseErrorBody = async (error: ResponseError) =>
+  trySafe(async () => {
+    // Clone the response to avoid blocking later usage since the response body can only be read once
+    const responseBody = await error.response.clone().json();
+    return responseErrorBodyGuard.parse(responseBody);
+  });
+
 export const toastResponseError = async (error: unknown) => {
   if (error instanceof ResponseError) {
-    const parsed = responseErrorBodyGuard.safeParse(await error.response.json());
-    toast.error(parsed.success ? parsed.data.message : error.message);
-    return;
+    const responseBody = await tryReadResponseErrorBody(error);
+    if (responseBody) {
+      toast.error(responseBody.message);
+      return;
+    }
   }
 
   toast(error instanceof Error ? error.message : String(error));
diff --git a/packages/console/src/utils/subscription.ts b/packages/console/src/utils/subscription.ts
index 7a21841e6..6446b9496 100644
--- a/packages/console/src/utils/subscription.ts
+++ b/packages/console/src/utils/subscription.ts
@@ -1,7 +1,7 @@
 import { ResponseError } from '@withtyped/client';
 import dayjs from 'dayjs';
 
-import { responseErrorBodyGuard } from '@/cloud/hooks/use-cloud-api';
+import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api';
 import { type SubscriptionPlanResponse } from '@/cloud/types/router';
 import {
   communitySupportEnabledMap,
@@ -64,11 +64,7 @@ export const isExceededQuotaLimitError = async (error: unknown) => {
     return false;
   }
 
-  try {
-    const responseBody = await error.response.json();
-    const { message } = responseErrorBodyGuard.parse(responseBody);
-    return message.includes('Exceeded quota limit');
-  } catch {
-    return false;
-  }
+  const { message } = (await tryReadResponseErrorBody(error)) ?? {};
+
+  return Boolean(message?.includes('Exceeded quota limit'));
 };