Ver Fonte

graphql api 错误处理优化

fogwind há 4 dias atrás
pai
commit
b4532aed97

+ 20 - 1
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,5 +1,6 @@
 import {Suspense} from "react";
 import { getServerSession } from "next-auth";
+import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
 import { authOptions } from "@utils/auth";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
@@ -43,7 +44,7 @@ export default async function ContinueToPay({searchParams}: {
 
     const query = await searchParams;
     const orderid = query?.orderid;
-    const {data: orderDetailResponse} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
+    const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         variables: {
             id: orderid as string
@@ -62,7 +63,25 @@ export default async function ContinueToPay({searchParams}: {
         }
     });
       
+    if(orderDetailError) {
+        return (
+            <div className="box-border px-4 w-full">
+            
+                
+                <div className="w-12 h-12 mx-auto mt-9">
+                    <svg className="failure block w-full h-full" width="200" height="200" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27635"><path d="M736 332.8L556.8 512l179.2 179.2-44.8 44.8L512 556.8l-179.2 179.2-44.8-44.8L467.2 512 288 332.8l44.8-44.8L512 467.2l179.2-179.2 44.8 44.8z" p-id="27636"></path><path d="M512 0a512 512 0 1 1 0 1024A512 512 0 0 1 512 0z m0 64a448 448 0 1 0 0 896A448 448 0 0 0 512 64z" p-id="27637"></path></svg>
+                </div>
+                <p className="text-ly-16 text-center mt-6">
+                    Get Pending Order failed:
+                    <span className="text-ly-16">{orderDetailError.message}</span>
+                </p>
 
+                <Link href="/" className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                    Continue Shopping
+                </Link>
+            </div>
+        );
+    }
     return (
         <div className="w-full pb-8">
         

+ 1 - 2
src/app/(checkout)/checkout/page.tsx

@@ -25,8 +25,7 @@ export default async function CheckoutPage() {
 
 
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
-        query: GET_CART_ITEM,
-        operationName:'GetCartItem'
+        query: GET_CART_ITEM
     });
     if(cartDetailsRes.createReadCart === null) {
         redirect('/', RedirectType.replace);

+ 12 - 66
src/app/api/graphql/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { bagistoFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 import {
     CREATE_ADD_PRODUCT_IN_CART,
@@ -62,55 +61,7 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
     const guestToken = getAuthToken(req);
     const query = ALLOWED_OPERATIONS[operationName];
     const finalVariables = variables;
-
-    /*if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
-        finalVariables = { ...variables };
-    }
-
-    if (operationName === 'CreateCheckoutPaymentMethod') {
-        finalVariables = {
-            ...variables,
-            successUrl: variables?.successUrl ?? `payment/success`,
-            failureUrl: variables?.failureUrl ?? `payment/failure`,
-            cancelUrl: variables?.cancelUrl ?? `payment/cancel`
-        };
-    }*/
-
-    /*if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
-        finalVariables = {
-            billingFirstName: body.billingFirstName,
-            billingLastName: body.billingLastName,
-            billingEmail: body.billingEmail,
-            billingAddress: body.billingAddress,
-            billingCity: body.billingCity,
-            billingCountry: body.billingCountry,
-            billingState: body.billingState,
-            billingPostcode: body.billingPostcode,
-            billingPhoneNumber: body.billingPhoneNumber,
-            billingCompanyName: body.billingCompanyName,
-            useForShipping: body.useForShipping,
-            ...(!body.useForShipping && {
-                shippingFirstName: body.shippingFirstName,
-                shippingLastName: body.shippingLastName,
-                shippingEmail: body.billingEmail,
-                shippingAddress: body.shippingAddress,
-                shippingCity: body.shippingCity,
-                shippingCountry: body.shippingCountry,
-                shippingState: body.shippingState,
-                shippingPostcode: body.shippingPostcode,
-                shippingPhoneNumber: body.shippingPhoneNumber,
-                shippingCompanyName: body.shippingCompanyName,
-            })
-        };
-    }*/
-
-    /*if (operationName === 'createAddProductInCart' && body.productId) {
-        finalVariables = {
-            cartId: body.cartId ?? null,
-            productId: body.productId,
-            quantity: body.quantity,
-        };
-    }*/
+ 
     return {
         query,
         variables: finalVariables,
@@ -151,27 +102,22 @@ export async function POST(req: NextRequest) {
 
         
         const response = await bagistoFetch<any>(fetchOption);
-        // console.log('response ------ ', response);
-        return NextResponse.json({
-            data: response.body.data,
+        console.log('bagistoFetch response ------ ', response);
+        return NextResponse.json(response.body,{
+            status: response.status
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
+        console.log('bagistoFetch ERROR ------ ', error);
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                errors:[
+                    {
+                        message: error instanceof Error ? error.message : "Network error"
+                    }
+                ],
+                data:null
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 6 - 11
src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx

@@ -4,17 +4,17 @@ import { useAppDispatch } from "@/store/hooks";
 import { openAddToCartDialog } from '@/store/slices/addToCartDialogSlice';
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
 import { useLazyQuery } from '@apollo/client/react';
-import { SingleProductResponse } from "@/components/catalog/type";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import ShoppingCartIcon from "@components/common/icons/ShoppingCartIcon";
 import { useCustomToast } from "@utils/hooks/useToast";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string}) {
     const dispatch = useAppDispatch();
     // const [loading, setLoading] = useState(false);
     const { showToast } = useCustomToast();
 
-    const [getContent, { loading }] = useLazyQuery<SingleProductResponse>(GET_PRODUCT_BY_URL_KEY,{
+    const [getContent, { loading }] = useLazyQuery(GET_PRODUCT_BY_URL_KEY,{
         fetchPolicy: 'network-only'
     });
     const openDialog = async () => {
@@ -24,7 +24,7 @@ export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string
                 variables: { urlKey: productUrlKey }
             });
 
-            console.log('product ----- ', result);// 返回的数据中有__typename,如何过滤 @todo
+            console.log('product ----- ', result);
 
             const productData = result?.data?.product;
             if(result.error) {
@@ -40,14 +40,9 @@ export function OpenAddToCartModalButton({productUrlKey}: {productUrlKey: string
             
         } catch (error) {
             console.log('error ----- ', error);
-            let errorMessage = "Network Error";
-            if (error instanceof Error) {
-                errorMessage = error.message;
-            } else if (typeof error === 'object' && error !== null && 'message' in error) {
-                errorMessage = String((error as any).message);
-            }
-            
-            showToast(errorMessage, "danger");
+            handleApolloBusinessError(error, (err) => {
+                showToast(err.message, "danger");
+            });
         }
        
     }

+ 113 - 0
src/components/home/HomeImageBanner.tsx

@@ -3,7 +3,12 @@
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch";
+import makeClient from "@/lib/ApolloClientBrowser";
 
+import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON } from "@/graphql";
+import { useMutation, useLazyQuery } from "@apollo/client/react";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
+import { useCustomToast } from "@/utils/hooks/useToast";
 // interface ImageCarouselProps {
 //     options: {
 //         images: {
@@ -14,8 +19,116 @@ import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch"
 //     };
 // }
 export default function HomeImageBanner() {
+  const { showToast } = useCustomToast();
+  const apolloClient = makeClient();
+  // const res = useQuery(GET_COUNTRY_STATES, {
+  //         variables: {
+  //             countryId: 1000
+  //         }
+  //     });
+  //     console.log('query state --- res', res);
+  const ttt = async () => {
+    try {
+      const res = await apolloClient.mutate({
+        mutation: GET_CART_ITEM,
+        fetchPolicy: 'network-only', // 按需调整
+      });
+      console.log('----------',res);
+    } catch(error) {
+      console.log('ttt----------',error);
+      handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+      });
+    }  
+  }
+  // 76|nlq9qGztR7qmSS72Kal56NCjhRRVrLCeVOdZdLLYa2c769e9
+const [getContent] = useLazyQuery(GET_FILTER_ATTRIBUTES,{
+        fetchPolicy: 'network-only'
+    });
+
+  const testLazyQuery = async () => { 
+      try {
+            const result = await getContent();
+            console.log('lazy query res---', result);
+      } catch(err) {
+          console.log('lazy query ---- ', err);
+      }
+  }
+
+    // useMutation
+    
+
+
+    const [mutateAsync] = useMutation(
+        GET_CART_ITEM,
+        {
+          onCompleted: (res) => {
+            console.log('useMutation onCompleted res ----- 0',res);
+          },
+    
+          onError: (err) => {
+            console.log('useMutation onCompleted err ----- 0',err);
+          },
+        },
+      );
+
+
+    const testMutation = async () => { 
+        
+            const res = await mutateAsync();
+        console.log('testMutation ---',res)
+        
+    };
+    const testFilter= () => {
+            return apolloClient.query({
+                query: GET_FILTER_ATTRIBUTES,
+                fetchPolicy: "no-cache",
+                context: {
+                    fetchOptions: {
+                        cache: 'no-store',
+                    },
+            
+                },
+                // variables: {
+                //     orderId: Number(orderid)
+                // }
+            }).then((res) => {
+                
+                console.log('testFilter res ---- ',res);
+            
+            
+            }).catch((err) => {
+                console.error('testFilter error ---- ',err);
+                handleApolloBusinessError(err,(error) => {
+          showToast(error.message, "danger");
+      });
+            });
+        };
+    
+        const testApplyCoupon = async () => { 
+            try {
+                await apolloClient.mutate({
+                    mutation: CREATE_APPLY_COUPON,
+                    variables: {
+                        couponCode: 'BIRTHDAY20'
+                    },
+                    fetchPolicy: 'network-only', // 按需调整
+                });
+            } catch (err) {
+                console.log('testApplyCoupon error ---- ',err);
+            }
+        };
+  
   return (
     <>
+    <p>
+      <button onClick={ttt}>get cart detail</button>
+    </p>
+
+    <p><button onClick={testLazyQuery}>testLazyQuery</button></p>
+    <p><button onClick={testMutation}>testMutation</button></p>
+    <p><button onClick={testFilter}>testFilter</button></p>
+    <p><button onClick={testApplyCoupon}>testApplyCoupon</button></p>
     <Swiper
       spaceBetween={50}
       slidesPerView={3}

+ 3 - 3
src/graphql/catalog/mutations/CreateProductReview.ts

@@ -1,10 +1,10 @@
-import { gql } from "@apollo/client";
-
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { ProductReviewResponse } from "@/types/review";
 /**
  * Create a product review
  * @param input - Review input data
  */
-export const CREATE_PRODUCT_REVIEW = gql`
+export const CREATE_PRODUCT_REVIEW: TypedDocumentNode<ProductReviewResponse> = gql`
   mutation CreateProductReview($input: createProductReviewInput!) {
     createProductReview(input: $input) {
       productReview {

+ 3 - 3
src/graphql/catalog/queries/GetProductByUrlKey.ts

@@ -1,11 +1,11 @@
-import { gql } from "@apollo/client";
+import { gql,TypedDocumentNode } from "@apollo/client";
 import { PRODUCT_DETAILED_FRAGMENT } from "../fragments";
-
+import { SingleProductResponse } from "@/components/catalog/type";
 /**
  * Fetch a single product by URL key with all details
  * @param urlKey - Product URL key
  */
-export const GET_PRODUCT_BY_URL_KEY = gql`
+export const GET_PRODUCT_BY_URL_KEY: TypedDocumentNode<SingleProductResponse> = gql`
   ${PRODUCT_DETAILED_FRAGMENT}
 
   query GetProductById($urlKey: String!) {

+ 15 - 2
src/lib/ApolloClientBrowser.ts

@@ -5,12 +5,21 @@ import {
   ApolloClient,
   InMemoryCache,
 } from "@apollo/client-integration-nextjs";
+import { ErrorLink } from "@apollo/client/link/error";
+import {CombinedProtocolErrors} from "@apollo/client/errors";
+import {handleErrorForErrorLink} from "@/lib/ApolloErrorHandler";
 import { getSession } from "next-auth/react";
 import { getCartToken } from "@/utils/getCartToken";
 import { BagistoSession } from "@/types/types";
 
 // 这里注册的是客户端使用的apollo client
 
+CombinedProtocolErrors.formatMessage = (errors,{defaultFormatMessage})=>{
+  return "[Subscription Error] " +  defaultFormatMessage(errors);
+};
+
+
+
 let sessionCache: { session: BagistoSession | null; timestamp: number } | null = null;
 const SESSION_CACHE_TTL = 5000;
 
@@ -44,6 +53,10 @@ export default function makeClient() {
         },*/
   });
 
+  const errorLink = new ErrorLink(({ error }) => {
+    handleErrorForErrorLink(error);
+  });
+
 
   const authLink = new SetContextLink(async (prevContext) => {
       
@@ -60,8 +73,8 @@ export default function makeClient() {
         },
       };
     });
-
-    const link = ApolloLink.from([authLink, httpLink]);
+    // httpLink 要放在最后
+    const link = ApolloLink.from([errorLink, authLink, httpLink]);
     return new ApolloClient({
         // ssrMode,
         link,

+ 53 - 3
src/lib/ApolloErrorHandler.ts

@@ -6,23 +6,73 @@ import {
   ServerParseError,
   UnconventionalError,
 } from "@apollo/client/errors";
-
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 // about apollo client error handle https://www.apollographql.com/docs/react/data/error-handling
 // Comprehensive error handling example.
-export function handleError(error: unknown) {
+
+export function handleApolloBusinessError(error: unknown, callback: (err:CombinedGraphQLErrors) => void) {
   if (CombinedGraphQLErrors.is(error)) {
     // Handle GraphQL errors
-  } else if (CombinedProtocolErrors.is(error)) {
+    // error.errors.forEach(({ message, locations, path }) =>
+    //   console.log(
+    //     `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
+    //   )
+    // );
+    callback(error);
+    // console.log('CombinedGraphQLErrors--',error.message);
+
+  }
+}
+// graphql接口额响应状态码貌似只有200 和 500。500表示: 1.后端代码报错;2.前端代理接口抛出异常
+// 已知有一种情况graphql的响应状态码是401,即请求接口的时候请求头缺少X-STOREFRONT-KEY
+export function handleErrorForErrorLink(error: unknown) {
+  // CombinedGraphQLErrors 交给具体的业务自己捕获处理
+  if (CombinedGraphQLErrors.is(error)) return;
+  if (CombinedProtocolErrors.is(error)) {
+    // CombinedProtocolErrors 是GraphQL 传输协议层错误,该项目中遇不到
     // Handle multipart subscription protocol errors
+    console.error( "GraphQL protocol error", error.errors);
+    console.log(error.message);
   } else if (LocalStateError.is(error)) {
+    /**
+     * LocalStateError 是 Apollo Client 在执行 Local State(本地状态) 时抛出的错误,通常和 @client 字段、本地 resolver 配置、LocalState 配置错误 有关。
+     * 它不是 GraphQL Server 返回的错误,而是 Apollo Client 自己在本地解析字段时发生的致命错误。
+     * 本项目没用到
+     */
     // Handle errors thrown by the `LocalState` class
+    console.error( "Local state error message:", error.message );
+    console.error( "Local state error cause:", error.cause );
+    console.error( "Local state error path:", error.path );
   } else if (ServerError.is(error)) {
+    // 状态码不是2xx的错误
     // Handle server HTTP errors
+    /*
+    console.log('ServerError--',error);
+    console.log(`Server returned status: ${error.statusCode}`,error.message);
+    console.log(`bodyText: `,JSON.parse(error.bodyText));
+    console.log('response: ', error.response);
+    */
+   
+    const bodyData = JSON.parse(error.bodyText);
+    const msg = bodyData.errors[0].message;
+    // Handle specific status codes
+    confirmDialog({
+        title: "Server Error",
+        content: 'AC Error: ' + msg,
+        noCancel: true,
+    }).then(() => {});
+    
   } else if (ServerParseError.is(error)) {
+    // 解析接口返回数据时报错
     // Handle JSON parse errors
+    console.log('ServerParseError--',error);
   } else if (UnconventionalError.is(error)) {
+    // 不是标准的JavaScript错误,一般是自定义apollo link或者第三方Apollo link抛出的
     // Handle errors thrown by irregular types
+    console.log('UnconventionalError--',error);
+    console.error( "非法错误:", error.cause);
   } else {
     // Handle other errors
+    console.error("[other errors]:", error);
   }
 }

+ 10 - 4
src/utils/hooks/useAddToCart.ts

@@ -16,7 +16,7 @@ import {
   UPDATE_CART_ITEM,
 } from "@/graphql";
 import { formatCartDetail } from "@/utils/cartDetailTools";
-
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 
 export const useAddProduct = () => {
@@ -64,7 +64,9 @@ export const useAddProduct = () => {
       },
 
       onError: (err) => {
-        showToast(err?.message ?? "Error", "danger");
+        handleApolloBusinessError(err,(error) => {
+          showToast(error.message, "danger");
+        });
       },
     },
   );
@@ -138,7 +140,9 @@ export const useAddProduct = () => {
         }
       },
       onError: (error) => {
-        showToast(error?.message as string, "danger");
+        handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+        });
       },
     },
   );
@@ -167,7 +171,9 @@ export const useAddProduct = () => {
       },
 
       onError: (error) => {
-        showToast(error?.message as string, "danger");
+        handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+        });
       },
     },
   );

+ 6 - 3
src/utils/hooks/useProductReview.ts

@@ -3,12 +3,13 @@
 import { useMutation } from "@apollo/client/react";
 import { CREATE_PRODUCT_REVIEW } from "@/graphql";
 import { useCustomToast } from "./useToast";
-import { CreateProductReviewInput, ProductReviewResponse } from "@/types/review";
+import { CreateProductReviewInput } from "@/types/review";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 
 export function useProductReview() {
     const { showToast } = useCustomToast();
 
-    const [mutateAsync, { loading: isLoading, error }] = useMutation<ProductReviewResponse>(CREATE_PRODUCT_REVIEW, {
+    const [mutateAsync, { loading: isLoading, error }] = useMutation(CREATE_PRODUCT_REVIEW, {
         onCompleted: (response) => {
             const responseData = response?.createProductReview;
             if (responseData) {
@@ -16,7 +17,9 @@ export function useProductReview() {
             }
         },
         onError: (error) => {
-            showToast(error.message, "danger");
+            handleApolloBusinessError(error,(err) => {
+                showToast(err.message, "danger");
+            });
         },
     });