Skip to content

Commit

Permalink
Move the error unwrapper into common (#1558)
Browse files Browse the repository at this point in the history
  • Loading branch information
forgetso authored Dec 10, 2024
1 parent 796e216 commit 2fdbbf4
Show file tree
Hide file tree
Showing 15 changed files with 105 additions and 62 deletions.
1 change: 1 addition & 0 deletions dev/config/src/vite/vite.test.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { defineConfig } from "vitest/config";
import VitePluginCloseAndCopy from "./vite-plugin-close-and-copy.js";
import VitePluginSourcemapExclude from "./vite-plugin-sourcemap-exclude.js";
Expand Down
44 changes: 44 additions & 0 deletions packages/common/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
// limitations under the License.

import { type TranslationKey, i18n as i18next } from "@prosopo/locale";
import { ZodError } from "zod";
import { type LogLevel, type Logger, getLoggerDefault } from "./index.js";
import type { ApiJsonError } from "./types.js";

type BaseErrorOptions<ContextType> = {
name?: string;
Expand Down Expand Up @@ -70,6 +72,9 @@ export abstract class ProsopoBaseError<
private logError(logger: Logger, logLevel: LogLevel, errorName?: string) {
const errorParams = { error: this.message, context: this.context };
const errorMessage = { errorType: errorName || this.name, errorParams };
if (logLevel === "debug") {
logger.debug(this.stack);
}
logger[logLevel](errorMessage);
}
}
Expand Down Expand Up @@ -176,3 +181,42 @@ export class ProsopoApiError extends ProsopoBaseError<ApiContextParams> {
this.code = code;
}
}

export const unwrapError = (err: ProsopoBaseError | SyntaxError | ZodError) => {
const code = "code" in err ? (err.code as number) : 400;
let message = i18next.t(err.message);
let jsonError: ApiJsonError = { code, message };
let statusMessage = err.message;
jsonError.message = message;
// unwrap the errors to get the actual error message
while (err instanceof ProsopoBaseError && err.context) {
// base error will not have a translation key
jsonError.key =
err.context.translationKey || err.translationKey || "API.UNKNOWN";
jsonError.message = err.message;
if (err.context.error) {
err = err.context.error;
} else {
break;
}
}

if (isZodError(err)) {
message = i18next.t("API.PARSE_ERROR");
statusMessage = message;
if (typeof err.message === "object") {
jsonError = err.message;
} else {
jsonError.message = JSON.parse(err.message);
}
}

jsonError.code = jsonError.code || code;
return { code, statusMessage, jsonError };
};

export const isZodError = (err: unknown): err is ZodError => {
return Boolean(
err && (err instanceof ZodError || (err as ZodError).name === "ZodError"),
);
};
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./logger.js";
export * from "./array.js";
export * from "./hash.js";
export * from "./string.js";
export * from "./types.js";
18 changes: 18 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export type ApiJsonError = {
message: string;
key?: string;
code: number;
};
5 changes: 4 additions & 1 deletion packages/locale/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Falscher CAPTCHA-Typ",
"UNAUTHORIZED_ORIGIN_URL": "Nicht autorisierte Ursprungs-URL",
"INVALID_SITE_KEY": "Ungültiger Site-Schlüssel",
"INVALID_IP": "Ungültige IP"
"INVALID_IP": "Ungültige IP",
"INVALID_URL": "Ungültiger URL",
"INVALID_BODY": "Ungültiger Körper",
"PARSE_ERROR": "Fehler beim Parsen der Anfrage"
},
"CLI": {
"PARAMETER_ERROR": "Ungültiger Parameter"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Incorrect CAPTCHA type",
"INVALID_SITE_KEY": "Invalid site key",
"UNAUTHORIZED_ORIGIN_URL": "Unauthorized origin URL",
"INVALID_IP": "Invalid IP"
"INVALID_IP": "Invalid IP",
"INVALID_URL": "Invalid URL",
"INVALID_BODY": "Invalid body",
"PARSE_ERROR": "Error parsing request"
},
"CLI": {
"PARAMETER_ERROR": "Invalid parameter"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Tipo di CAPTCHA errato",
"UNAUTHORIZED_ORIGIN_URL": "URL de origen no autorizada",
"INVALID_SITE_KEY": "Clave de sitio no válida",
"INVALID_IP": "IP no válida"
"INVALID_IP": "IP no válida",
"INVALID_URL": "URL inválida",
"INVALID_BODY": "Cuerpo inválido",
"PARSE_ERROR": "Error al analizar la respuesta"
},
"CLI": {
"PARAMETER_ERROR": "Parámetro inválido"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Type de CAPTCHA incorrect",
"UNAUTHORIZED_ORIGIN_URL": "URL d'origine non autorisée",
"INVALID_SITE_KEY": "Clé de site non valide",
"INVALID_IP": "IP invalide"
"INVALID_IP": "IP invalide",
"INVALID_URL": "URL inválida",
"INVALID_BODY": "Cuerpo inválido",
"PARSE_ERROR": "Erreur d'analyse de la demande"
},
"CLI": {
"PARAMETER_ERROR": "Paramètre invalide"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Tipo di CAPTCHA errato",
"UNAUTHORIZED_ORIGIN_URL": "URL di origine non autorizzato",
"INVALID_SITE_KEY": "Chiave del sito non valida",
"INVALID_IP": "IP non valido"
"INVALID_IP": "IP non valido",
"INVALID_URL": "URL non valido",
"INVALID_BODY": "Corpo non valido",
"PARSE_ERROR": "Errore nell'analisi della richiesta"
},
"CLI": {
"PARAMETER_ERROR": "Parametro non valido"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Tipo de CAPTCHA incorreto",
"UNAUTHORIZED_ORIGIN_URL": "URL de origem não autorizada",
"INVALID_SITE_KEY": "Chave de site inválida",
"INVALID_IP": "IP inválido"
"INVALID_IP": "IP inválido",
"INVALID_URL": "URL inválido",
"INVALID_BODY": "Corpo inválido",
"PARSE_ERROR": "Erro ao analisar a resposta"
},
"CLI": {
"PARAMETER_ERROR": "Parâmetro inválido"
Expand Down
5 changes: 4 additions & 1 deletion packages/locale/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
"INCORRECT_CAPTCHA_TYPE": "Tipo de CAPTCHA incorreto",
"UNAUTHORIZED_ORIGIN_URL": "URL de origem não autorizada",
"INVALID_SITE_KEY": "Chave de site inválida",
"INVALID_IP": "IP inválido"
"INVALID_IP": "IP inválido",
"INVALID_URL": "URL inválido",
"INVALID_BODY": "Corpo inválido",
"PARSE_ERROR": "Erro ao analisar a resposta"
},
"CLI": {
"PARAMETER_ERROR": "Parâmetro inválido"
Expand Down
10 changes: 1 addition & 9 deletions packages/provider/src/api/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,21 @@ import {
type CaptchaSolutionResponse,
type DappUserSolutionResult,
GetFrictionlessCaptchaChallengeRequestBody,
type GetFrictionlessCaptchaResponse,
GetPowCaptchaChallengeRequestBody,
type GetPowCaptchaChallengeRequestBodyTypeOutput,
type GetPowCaptchaResponse,
type PowCaptchaSolutionResponse,
SubmitPowCaptchaSolutionBody,
type SubmitPowCaptchaSolutionBodyTypeOutput,
type TGetImageCaptchaChallengePathAndParams,
} from "@prosopo/types";
import {
FrictionlessToken,
FrictionlessTokenRecord,
} from "@prosopo/types-database";
import type { ProviderEnvironment } from "@prosopo/types-env";
import { flatten, version } from "@prosopo/util";
import { flatten } from "@prosopo/util";
import express, { type Router } from "express";
import { v4 as uuidv4 } from "uuid";
import { getBotScore } from "../tasks/detection/getBotScore.js";
import { Tasks } from "../tasks/tasks.js";
import { getIPAddress } from "../util.js";
import { handleErrors } from "./errorHandler.js";

const NO_IP_ADDRESS = "NO_IP_ADDRESS" as const;
const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;

/**
Expand Down
39 changes: 2 additions & 37 deletions packages/provider/src/api/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// We need the unused params to make express recognise this function as an error handler
import { type ProsopoApiError, ProsopoBaseError } from "@prosopo/common";
import { i18n as i18next } from "@prosopo/locale";
import type { ApiJsonError } from "@prosopo/types";
import { type ProsopoApiError, unwrapError } from "@prosopo/common";
import type { NextFunction, Request, Response } from "express";
import { ZodError } from "zod";
import type { ZodError } from "zod";

export const handleErrors = (
err: ProsopoApiError | SyntaxError | ZodError,
Expand All @@ -31,36 +29,3 @@ export const handleErrors = (
response.send({ error: jsonError });
response.end();
};

export const unwrapError = (err: ProsopoApiError | SyntaxError | ZodError) => {
const code = "code" in err ? err.code : 400;
let message = err.message;
let jsonError: ApiJsonError = { code, message };
let statusMessage = err.message;
jsonError.message = message;
// unwrap the errors to get the actual error message
while (err instanceof ProsopoBaseError && err.context) {
// base error will not have a translation key
jsonError.code =
err.context.translationKey || err.translationKey || jsonError.code;
jsonError.message = err.message;
if (err.context.error) {
err = err.context.error;
} else {
break;
}
}

if (err instanceof ZodError) {
message = i18next.t("CAPTCHA.PARSE_ERROR");
statusMessage = message;
if (typeof err.message === "object") {
jsonError = err.message;
} else {
jsonError.message = JSON.parse(err.message);
}
}

jsonError.code = jsonError.code || code;
return { code, statusMessage, jsonError };
};
13 changes: 9 additions & 4 deletions packages/provider/src/tests/unit/api/errorHandler.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ describe("handleErrors", () => {
} as unknown as Response;
const mockNext = vi.fn() as unknown as NextFunction;

const error = new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT");
const error = new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT", {
context: { code: 400 },
});
console.log(error);

handleErrors(error, mockRequest, mockResponse, mockNext);

Expand All @@ -43,11 +46,12 @@ describe("handleErrors", () => {
);
expect(mockResponse.send).toHaveBeenCalledWith({
error: {
code: "CONTRACT.INVALID_DATA_FORMAT",
code: 400,
key: "CONTRACT.INVALID_DATA_FORMAT",
message: "Invalid data format",
},
});
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.end).toHaveBeenCalled();
});

Expand Down Expand Up @@ -136,7 +140,8 @@ describe("handleErrors", () => {
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.send).toHaveBeenCalledWith({
error: {
code: "GENERAL.ENVIRONMENT_NOT_READY",
code: 500,
key: "GENERAL.ENVIRONMENT_NOT_READY",
message: "Environment not ready",
},
});
Expand Down
6 changes: 1 addition & 5 deletions packages/types/src/provider/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import type { ApiJsonError } from "@prosopo/common";
import type { Address4, Address6 } from "ip-address";
import {
type ZodDefault,
Expand Down Expand Up @@ -230,11 +231,6 @@ export interface ProviderRegistered {
status: "Registered" | "Unregistered";
}

export type ApiJsonError = {
message: string;
code: number;
};

export interface ApiResponse {
[ApiParams.status]: string;
[ApiParams.error]?: ApiJsonError;
Expand Down

0 comments on commit 2fdbbf4

Please sign in to comment.