diff --git a/.changeset/big-glasses-hang.md b/.changeset/big-glasses-hang.md new file mode 100644 index 0000000..ce090fc --- /dev/null +++ b/.changeset/big-glasses-hang.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": minor +--- + +Register handler hooks will now respond with errors parsable by the dashboard. "Body" in error was removed, so client code can provide message and status. diff --git a/docs/api-handlers.md b/docs/api-handlers.md index 78f4038..7ca2b66 100644 --- a/docs/api-handlers.md +++ b/docs/api-handlers.md @@ -67,7 +67,8 @@ export default createAppRegisterHandler({ allowedSaleorUrls: ["https://your-saleor.saleor.cloud/graphql/"], // optional, see options below async onRequestVerified(req, { authData, respondWithError }) { await doSomethingAndBlockInstallation(authData.token).catch((err) => { - throw respondWithError({ body: "Error, installation will fail" }); + // Return this method to break installation flow and show error in the Dashboard + return respondWithError({ message: "Error, installation will fail" }); }); }, }); @@ -93,7 +94,7 @@ export type CreateAppRegisterHandlerOptions = { authToken?: string; saleorDomain?: string; saleorApiUrl?: string; - respondWithError: ({ status, message, body }) => never; // should throw + respondWithError: ({ status, message }) => never; // will throw } ): Promise; /** @@ -104,7 +105,7 @@ export type CreateAppRegisterHandlerOptions = { request: Request, context: { authData: AuthData; - respondWithError: ({ status, message, body }) => never; // should throw + respondWithError: ({ status, message }) => never; // will throw } ): Promise; /** @@ -115,7 +116,7 @@ export type CreateAppRegisterHandlerOptions = { request: Request, context: { authData: AuthData; - respondWithError: ({ status, message, body }) => never; // should throw + respondWithError: ({ status, message }) => never; // will throw } ): Promise; /** @@ -127,7 +128,7 @@ export type CreateAppRegisterHandlerOptions = { context: { authData: AuthData; error: unknown; - respondWithError: ({ status, message, body }) => never; // should throw + respondWithError: ({ status, message }) => never; // will throw } ): Promise; }; diff --git a/src/handlers/next/create-app-register-handler.test.ts b/src/handlers/next/create-app-register-handler.test.ts index 095f505..c49de12 100644 --- a/src/handlers/next/create-app-register-handler.test.ts +++ b/src/handlers/next/create-app-register-handler.test.ts @@ -219,13 +219,12 @@ describe("create-app-register-handler", () => { context: { respondWithError(params: { status: number; body: string; message: string }): Error; } - ) => { - throw context.respondWithError({ + ) => + context.respondWithError({ status: 401, body: "test", message: "test message", - }); - } + }) ); const { res, req } = createMocks({ @@ -253,7 +252,13 @@ describe("create-app-register-handler", () => { await handler(req, res); expect(res._getStatusCode()).toBe(401); - expect(res._getData()).toBe("test"); + expect(res._getData()).toEqual({ + success: false, + error: { + code: "REGISTER_HANDLER_HOOK_ERROR", + message: "test message", + }, + }); }); }); }); diff --git a/src/handlers/next/create-app-register-handler.ts b/src/handlers/next/create-app-register-handler.ts index 5d9c9a3..759c449 100644 --- a/src/handlers/next/create-app-register-handler.ts +++ b/src/handlers/next/create-app-register-handler.ts @@ -16,33 +16,49 @@ const debug = createDebug("createAppRegisterHandler"); type HookCallbackErrorParams = { status?: number; - body?: object; message?: string; }; class RegisterCallbackError extends Error { public status = 500; - public body: object = {}; - constructor(errorParams: HookCallbackErrorParams) { super(errorParams.message); if (errorParams.status) { this.status = errorParams.status; } - - if (errorParams.body) { - this.body = errorParams.body; - } } } -const createCallbackError = (params: HookCallbackErrorParams) => new RegisterCallbackError(params); +const createCallbackError = (params: HookCallbackErrorParams) => { + throw new RegisterCallbackError(params); +}; + +export type RegisterHandlerResponseBody = { + success: boolean; + error?: { + code?: string; + message?: string; + }; +}; +export const createRegisterHandlerResponseBody = ( + success: boolean, + error?: RegisterHandlerResponseBody["error"] +): RegisterHandlerResponseBody => ({ + success, + error, +}); const handleHookError = (e: RegisterCallbackError | unknown) => { if (e instanceof RegisterCallbackError) { - return new Response(e.body, { status: e.status }); + return new Response( + createRegisterHandlerResponseBody(false, { + code: "REGISTER_HANDLER_HOOK_ERROR", + message: e.message, + }), + { status: e.status } + ); } return Response.InternalServerError("Error during app installation"); }; @@ -141,27 +157,24 @@ export const createAppRegisterHandler = ({ if (!validateAllowSaleorUrls(saleorApiUrl, allowedSaleorUrls)) { debug("Validation of URL %s against allowSaleorUrls param resolves to false, throwing"); - return Response.Forbidden({ - success: false, - error: { + return Response.Forbidden( + createRegisterHandlerResponseBody(false, { code: "SALEOR_URL_PROHIBITED", message: "This app expects to be installed only in allowed saleor instances", - }, - }); + }) + ); } const { configured: aplConfigured } = await apl.isConfigured(); if (!aplConfigured) { debug("The APL has not been configured"); + return new Response( - { - success: false, - error: { - code: "APL_NOT_CONFIGURED", - message: "APL_NOT_CONFIGURED. App is configured properly. Check APL docs for help.", - }, - }, + createRegisterHandlerResponseBody(false, { + code: "APL_NOT_CONFIGURED", + message: "APL_NOT_CONFIGURED. App is configured properly. Check APL docs for help.", + }), { status: 503, } @@ -172,14 +185,11 @@ export const createAppRegisterHandler = ({ const appId = await getAppId({ saleorApiUrl, token: authToken }); if (!appId) { return new Response( - { - success: false, - error: { - code: "UNKNOWN_APP_ID", - message: - "The auth data given during registration request could not be used to fetch app ID.", - }, - }, + createRegisterHandlerResponseBody(false, { + code: "UNKNOWN_APP_ID", + message: + "The auth data given during registration request could not be used to fetch app ID.", + }), { status: 401, } @@ -190,13 +200,10 @@ export const createAppRegisterHandler = ({ const jwks = await fetchRemoteJwks(saleorApiUrl); if (!jwks) { return new Response( - { - success: false, - error: { - code: "JWKS_NOT_AVAILABLE", - message: "Can't fetch the remote JWKS.", - }, - }, + createRegisterHandlerResponseBody(false, { + code: "JWKS_NOT_AVAILABLE", + message: "Can't fetch the remote JWKS.", + }), { status: 401, } @@ -262,17 +269,16 @@ export const createAppRegisterHandler = ({ } } - return Response.InternalServerError({ - success: false, - error: { + return Response.InternalServerError( + createRegisterHandlerResponseBody(false, { message: "Registration failed: could not save the auth data.", - }, - }); + }) + ); } debug("Register complete"); - return Response.OK({ success: true }); + return Response.OK(createRegisterHandlerResponseBody(true)); }; return toNextHandler([