diff --git a/js/src/sdk/base.toolset.spec.ts b/js/src/sdk/base.toolset.spec.ts index 82099e4f985..d48760523c1 100644 --- a/js/src/sdk/base.toolset.spec.ts +++ b/js/src/sdk/base.toolset.spec.ts @@ -42,7 +42,6 @@ describe("ComposioToolSet class tests", () => { actionName, toolSchema, }) => { - console.log("actionName", actionName); return { ...toolSchema, parameters: { @@ -56,7 +55,6 @@ describe("ComposioToolSet class tests", () => { const tools = await toolset.getToolsSchema({ actions: ["github_issues_create"], }); - console.log("tools", tools); }); it("should execute an action", async () => { diff --git a/js/src/sdk/index.spec.ts b/js/src/sdk/index.spec.ts index 21309d73d0d..f41db6b559b 100644 --- a/js/src/sdk/index.spec.ts +++ b/js/src/sdk/index.spec.ts @@ -26,54 +26,66 @@ describe("Basic SDK spec suite", () => { }); it("should handle 404 error gracefully", async () => { - const client = new Composio({ apiKey: COMPOSIO_API_KEY }); const mock = new AxiosMockAdapter(axiosClient.instance); - mock.onGet("/api/v1/apps").reply(404, { detail: "Not found" }); + const mockError = { + error: { + type: "NotFoundError", + name: "AppNotFoundError", + message: "Not found", + }, + }; + mock.onGet("/api/v1/apps").reply(404, mockError); + + const client = new Composio({ apiKey: COMPOSIO_API_KEY }); try { - await client.apps.list(); + const apps = await client.apps.list(); } catch (e) { - const error = e as ComposioError; - const errorCode = COMPOSIO_SDK_ERROR_CODES.BACKEND.NOT_FOUND; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(error.errCode).toBe(errorCode); - expect(error.message).toContain(errorInfo.message); - expect(error.description).toBe(errorInfo.description); - expect(error.errorId).toBeDefined(); - expect(error.name).toBe("ComposioError"); - expect(error.possibleFix).toBe(errorInfo.possibleFix); + if (e instanceof ComposioError) { + expect(e.errCode).toBe(COMPOSIO_SDK_ERROR_CODES.BACKEND.NOT_FOUND); + expect(e.description).toBe("Not found"); + expect(e.errorId).toBeDefined(); + expect(e.name).toBe("ComposioError"); + expect(e.possibleFix).toBe(e.possibleFix); + expect(e.message).toContain(mockError.error.message); + expect(e.message).toContain(mockError.error.name); + } else { + throw e; + } } mock.reset(); }); it("should handle 400 error gracefully", async () => { - const client = new Composio({ apiKey: COMPOSIO_API_KEY }); const mock = new AxiosMockAdapter(axiosClient.instance); - mock - .onGet("/api/v1/apps") - .reply(400, { errors: ["Invalid request for apps"] }); + mock.onGet("/api/v1/apps").reply(400, { + error: { + type: "BadRequestError", + name: "InvalidRequestError", + message: "Invalid request for apps", + }, + }); + const client = new Composio({ apiKey: COMPOSIO_API_KEY }); try { await client.apps.list(); } catch (e) { const error = e as ComposioError; const errorCode = COMPOSIO_SDK_ERROR_CODES.BACKEND.BAD_REQUEST; expect(error.errCode).toBe(errorCode); - expect(error.message).toContain( - "Validation Errors while making request to https://backend.composio.dev/api/v1/apps" - ); + expect(error.message).toContain("InvalidRequestError "); + expect(error.message).toContain("InvalidRequestError"); expect(error.description).toContain("Invalid request for apps"); } mock.reset(); }); - it("should handle 500 and 502 error gracefully", async () => { - const client = new Composio({ apiKey: COMPOSIO_API_KEY }); + it("should handle 500 and 502 error gracefully, and without backend fix", async () => { const mock = new AxiosMockAdapter(axiosClient.instance); mock.onGet("/api/v1/apps").reply(500, { detail: "Internal Server Error" }); - + const client = new Composio({ apiKey: COMPOSIO_API_KEY }); try { await client.apps.list(); } catch (e) { @@ -105,6 +117,20 @@ describe("Basic SDK spec suite", () => { } mock.reset(); + + mock.onGet("/api/v1/apps").reply(500, { + error: { + type: "NotFoundError", + name: "AppNotFoundError", + message: "Not found", + }, + }); + try { + await client.apps.list(); + } catch (e) { + const error = e as ComposioError; + expect(error.message).toContain("AppNotFoundError - NotFoundError"); + } }); it("should give request timeout error", async () => { diff --git a/js/src/sdk/index.ts b/js/src/sdk/index.ts index 7ac8a4a89ba..26c435e7027 100644 --- a/js/src/sdk/index.ts +++ b/js/src/sdk/index.ts @@ -22,6 +22,7 @@ import { ZGetExpectedParamsForUserParams, ZGetExpectedParamsRes, } from "../types/composio"; +import { getUUID } from "../utils/common"; import { ZAuthMode } from "./types/integration"; export type ComposioInputFieldsParams = z.infer< @@ -63,6 +64,7 @@ export class Composio { ); ComposioSDKContext.apiKey = apiKeyParsed; + ComposioSDKContext.sessionId = getUUID(); ComposioSDKContext.baseURL = baseURLParsed; ComposioSDKContext.frameworkRuntime = config?.runtime; diff --git a/js/src/sdk/models/backendClient.ts b/js/src/sdk/models/backendClient.ts index 7e0921d6744..83eb25761e3 100644 --- a/js/src/sdk/models/backendClient.ts +++ b/js/src/sdk/models/backendClient.ts @@ -1,3 +1,4 @@ +import { AxiosInstance } from "axios"; import apiClient from "../client/client"; import { client as axiosClient } from "../client/services.gen"; import { setAxiosClientConfig } from "../utils/config"; @@ -22,6 +23,7 @@ export class BackendClient { * The runtime environment where the client is being used. */ public runtime: string; + public instance: AxiosInstance; /** * Creates an instance of apiClientDetails. @@ -34,6 +36,7 @@ export class BackendClient { this.runtime = runtime || ""; this.apiKey = apiKey; this.baseUrl = baseUrl; + this.instance = axiosClient.instance; if (!apiKey) { throw CEG.getCustomError( @@ -86,5 +89,6 @@ export class BackendClient { }); setAxiosClientConfig(axiosClient.instance); + this.instance = axiosClient.instance; } } diff --git a/js/src/sdk/utils/composioContext.ts b/js/src/sdk/utils/composioContext.ts index c58068f018e..3ade6e3f440 100644 --- a/js/src/sdk/utils/composioContext.ts +++ b/js/src/sdk/utils/composioContext.ts @@ -11,6 +11,7 @@ class ComposioSDKContext { static frameworkRuntime?: string; static source?: string = "javascript"; static composioVersion?: string; + static sessionId?: string; } export default ComposioSDKContext; diff --git a/js/src/sdk/utils/error.ts b/js/src/sdk/utils/error.ts index 45bc0aa0a08..372893bbad8 100644 --- a/js/src/sdk/utils/error.ts +++ b/js/src/sdk/utils/error.ts @@ -2,8 +2,8 @@ import { AxiosError } from "axios"; import { ZodError } from "zod"; import { ComposioError } from "./errors/src/composioError"; import { + API_TO_SDK_ERROR_CODE, BASE_ERROR_CODE_INFO, - BE_STATUS_CODE_TO_SDK_ERROR_CODES, COMPOSIO_SDK_ERROR_CODES, } from "./errors/src/constants"; import { @@ -124,15 +124,12 @@ export class CEG { static throwAPIError(error: AxiosError) { const statusCode = error?.response?.status || null; const errorCode = statusCode - ? BE_STATUS_CODE_TO_SDK_ERROR_CODES[statusCode] || + ? API_TO_SDK_ERROR_CODE[statusCode] || COMPOSIO_SDK_ERROR_CODES.BACKEND.UNKNOWN : COMPOSIO_SDK_ERROR_CODES.BACKEND.UNKNOWN; - const predefinedError = BASE_ERROR_CODE_INFO[errorCode]; const errorDetails = getAPIErrorDetails( - errorCode, - error as AxiosError, - predefinedError + error as AxiosError ); const metadata = generateMetadataFromAxiosError(error); diff --git a/js/src/sdk/utils/errors/src/constants.ts b/js/src/sdk/utils/errors/src/constants.ts index 1eb124b0a48..e5eb47acb35 100644 --- a/js/src/sdk/utils/errors/src/constants.ts +++ b/js/src/sdk/utils/errors/src/constants.ts @@ -95,7 +95,7 @@ export const BASE_ERROR_CODE_INFO = { }, }; -export const BE_STATUS_CODE_TO_SDK_ERROR_CODES = { +export const API_TO_SDK_ERROR_CODE = { 400: COMPOSIO_SDK_ERROR_CODES.BACKEND.BAD_REQUEST, 401: COMPOSIO_SDK_ERROR_CODES.BACKEND.UNAUTHORIZED, 404: COMPOSIO_SDK_ERROR_CODES.BACKEND.NOT_FOUND, diff --git a/js/src/sdk/utils/errors/src/formatter.ts b/js/src/sdk/utils/errors/src/formatter.ts index fb97ed5fcc4..78ae6072d19 100644 --- a/js/src/sdk/utils/errors/src/formatter.ts +++ b/js/src/sdk/utils/errors/src/formatter.ts @@ -1,10 +1,16 @@ import { AxiosError } from "axios"; -import { COMPOSIO_SDK_ERROR_CODES } from "./constants"; +import { + API_TO_SDK_ERROR_CODE, + BASE_ERROR_CODE_INFO, + COMPOSIO_SDK_ERROR_CODES, +} from "./constants"; export interface ErrorResponseData { - message: string; - error: string; - errors?: Record[]; + error: { + type: string; + name: string; + message: string; + }; } interface ErrorDetails { @@ -15,14 +21,18 @@ interface ErrorDetails { } export const getAPIErrorDetails = ( - errorKey: string, - axiosError: AxiosError, - predefinedError: Record + axiosError: AxiosError ): ErrorDetails => { + const statusCode = axiosError.response?.status; + const errorCode = statusCode + ? API_TO_SDK_ERROR_CODE[statusCode] + : COMPOSIO_SDK_ERROR_CODES.BACKEND.UNKNOWN; + const predefinedError = BASE_ERROR_CODE_INFO[errorCode]; + const defaultErrorDetails = { message: axiosError.message, description: - axiosError.response?.data?.message || + axiosError.response?.data?.error?.message || axiosError.response?.data?.error || axiosError.message, possibleFix: @@ -30,57 +40,37 @@ export const getAPIErrorDetails = ( }; const metadata = generateMetadataFromAxiosError(axiosError); - switch (errorKey) { + + const errorNameFromBE = axiosError.response?.data?.error?.name; + const errorTypeFromBE = axiosError.response?.data?.error?.type; + const errorMessage = axiosError.response?.data?.error?.message; + + const genericMessage = `${errorNameFromBE || predefinedError.message} ${errorTypeFromBE ? `- ${errorTypeFromBE}` : ""} on ${axiosError.config?.baseURL! + axiosError.config?.url!}`; + + switch (errorCode) { case COMPOSIO_SDK_ERROR_CODES.BACKEND.NOT_FOUND: case COMPOSIO_SDK_ERROR_CODES.BACKEND.UNAUTHORIZED: case COMPOSIO_SDK_ERROR_CODES.BACKEND.SERVER_ERROR: case COMPOSIO_SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE: case COMPOSIO_SDK_ERROR_CODES.BACKEND.RATE_LIMIT: - return { - message: `${predefinedError.message || axiosError.message} for ${axiosError.config?.baseURL! + axiosError.config?.url!}`, - description: (axiosError.response?.data?.message! || - predefinedError.description) as string, - possibleFix: (predefinedError.possibleFix! || - defaultErrorDetails.possibleFix) as string, - metadata, - }; - + case COMPOSIO_SDK_ERROR_CODES.BACKEND.UNKNOWN: case COMPOSIO_SDK_ERROR_CODES.BACKEND.BAD_REQUEST: - const validationErrors = axiosError.response?.data?.errors; - const formattedErrors = Array.isArray(validationErrors) - ? validationErrors - .map((err) => JSON.stringify(err as Record)) - .join(", ") - : JSON.stringify( - validationErrors as unknown as Record - ); - return { - message: `Validation Errors while making request to ${axiosError.config?.baseURL! + axiosError.config?.url!}`, - description: `Validation Errors: ${formattedErrors}`, + message: genericMessage, + description: errorMessage || (predefinedError.description as string), possibleFix: - "Please check the request parameters and ensure they are correct.", - metadata, - }; - - case COMPOSIO_SDK_ERROR_CODES.BACKEND.UNKNOWN: - case COMPOSIO_SDK_ERROR_CODES.COMMON.UNKNOWN: - return { - message: `${axiosError.message} for ${axiosError.config?.baseURL! + axiosError.config?.url!}`, - description: (axiosError.response?.data?.message! || - axiosError.response?.data?.error! || - axiosError.message) as string, - possibleFix: "Please contact tech@composio.dev with the error details.", + predefinedError.possibleFix! || + (defaultErrorDetails.possibleFix as string), metadata, }; default: return { - message: `${predefinedError.message || axiosError.message} for ${axiosError.config?.baseURL! + axiosError.config?.url!}`, - description: (axiosError.response?.data?.message! || - predefinedError.description) as string, - possibleFix: (predefinedError.possibleFix! || - defaultErrorDetails.possibleFix) as string, + message: genericMessage, + description: errorMessage || (predefinedError.description as string), + possibleFix: + predefinedError.possibleFix! || + (defaultErrorDetails.possibleFix as string), metadata, }; }