hey-api / openapi-ts

✨ Turn your OpenAPI specification into a beautiful TypeScript client
https://heyapi.vercel.app
MIT License
635 stars 44 forks source link

@hey-api/client-fetch does not really `throw` where it should #680

Open steinathan opened 1 week ago

steinathan commented 1 week ago

Description

Using @hey-api/client-fetch does not actually show errors where they should be, see the comparison examples below

My client (would love an axios client :) )

import { createClient } from "@hey-api/client-fetch";

  const [idToken] = useLocalStorage("id_token");
  createClient({
    baseUrl: import.meta.env["VITE_APP_API_URL"],
    headers: {
      Authorization: `Bearer ${idToken}`,
    },
  });

@hey-api/client-fetch ❌

export const useLogin = () => {
  const { toast } = useToast();
  const navigate = useNavigate();
  const setUser = useUserStore((state) => state.setUser);

  const {
    error,
    isPending: loading,
    mutateAsync: loginFn,
  } = useMutation({
    mutationFn: login,
    onError: (err) => console.log("ERROR", err), // NEVER WORKS
    onSuccess: ({ data, error }) => {
      if (data) {
        setUser(data.user);
        localStorage.setItem("id_token", data.access_token);
        navigate("/dashboard");
      }

      if (error) { /// Errors shouldn't be handled here
        toast({
          variant: "destructive",
          title: "Uh oh! Something went wrong.",
          description: error?.detail,
        });

        console.log("ERROR", error);
      }
    },
  });

  return { loginFn, loading, error };
};

Axios client ✅

export const useLogin = () => {
  const { toast } = useToast();
  const navigate = useNavigate();
  const setUser = useUserStore((state) => state.setUser);

  const {
    error,
    isPending: loading,
    mutateAsync: loginFn,
  } = useMutation({
    mutationFn: login,
    onError: (error) => {
      toast({
        variant: "destructive",
        title: error?.detail ?? error.message,
        description: "Uh oh! Something went wrong.",
      });
    },
    onSuccess: (data) => {
      if (data) {
        setUser(data.user);
        localStorage.setItem("id_token", data.access_token);
        navigate("/dashboard");
      }
    },
  });

  return { loginFn, loading, error };
};

OpenAPI specification (optional)

{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/api/auth/me":{"get":{"tags":["auth"],"summary":"Get Me","operationId":"get_me","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/User"},{"type":"null"}],"title":"Response Get Me Api Auth Me Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/auth/login":{"post":{"tags":["auth"],"summary":"Login","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLoginInput"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLoginResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/register":{"post":{"tags":["auth"],"summary":"Register","description":"Registers a new user by creating a user in the authentication service and adding them to the user table.\n\nParameters:\n    params (UserRegisterInput): The input parameters for registering a new user, including the email and password.\n\nReturns:\n    User","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegisterInput"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Index","operationId":"index","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Index  Get"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"User":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"picture":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Picture"},"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"email_verified":{"type":"boolean","title":"Email Verified"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","username","email","email_verified","created_at","updated_at"],"title":"User","description":"Represents a User record"},"UserLoginInput":{"properties":{"identifier":{"type":"string","title":"Identifier"},"password":{"type":"string","title":"Password"}},"type":"object","required":["identifier","password"],"title":"UserLoginInput"},"UserLoginResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type"},"user":{"$ref":"#/components/schemas/User"}},"type":"object","required":["access_token","token_type","user"],"title":"UserLoginResponse"},"UserRegisterInput":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"email":{"type":"string","format":"email","title":"Email"},"name":{"type":"string","title":"Name"},"password":{"type":"string","title":"Password"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"}},"type":"object","required":["email","name","password"],"title":"UserRegisterInput"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}

Configuration


export default defineConfig({
  input: "http://localhost:1337/openapi.json",
  client: "@hey-api/client-fetch",
  output: {
    path: "src/openapi",
    lint: "eslint",
    format: "prettier",
  },
  services: {
    asClass: false,
  },
  types: {
    enums: "javascript",
    name: "PascalCase",
  },
});

System information (optional)

No response

mrlubos commented 1 week ago

Yeah I think this is more TanStack Query integration question as it will need a slightly different implementation to make this work 👀

steinathan commented 1 week ago

Ok, i'll stick to using fetch then until @hey-api/client-axios is available

frdwhite24 commented 1 week ago

the returning of an object of { data: DataType, error: ErrorType, request: Request, response: Response } makes things quite hard to work with Tanstack Query, but it's totally doable. With regards to not throwing an error, I encountered this yesterday and just put this into a response interceptor, hope it helps!

client.interceptors.response.use(async (response, request) => {
  const statusCategory = response?.status.toString()[0]

  switch (statusCategory) {
    case '4': {
      throw new MyApiError()
    }
    case '5':
      throw new MyApiError()
  }

  return response
})
steinathan commented 6 days ago

@frdwhite24 Thanks a lot - your solution works for me

 const setOpenAPIClient = () => {
    const client = createClient({
      baseUrl: import.meta.env["VITE_APP_API_URL"],
      headers: {
        Authorization: `Bearer ${idToken}`,
      },
    });

    client.interceptors.response.use(async (response, _) => {
      const statusCategory = response?.status?.toString()[0];

      if (!statusCategory) {
        throw new Error("Invalid response status");
      }

      const responseData = await response.clone().json();
      const message = responseData?.detail;

      if (["3", "4", "5"].includes(statusCategory)) {
        const serverError = new ServerApiError(message);
        console.error(message, statusCategory);
        throw serverError;
      }

      return response;
    });
  };