Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move the error unwrapper into common #1558

Merged
merged 8 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 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 Expand Up @@ -52,6 +53,7 @@ export default function () {
},
pool: "forks", // forks is slower than 'threads' but more compatible with low-level libs (e.g. bcrypt)
testTimeout: 10000,
//disableConsoleIntercept: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove new unused comment

},
plugins: [
VitePluginSourcemapExclude({ excludeNodeModules: true }),
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: ProsopoApiError | SyntaxError | ZodError) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this typed as a ProsopoApiError? Aren't there other error types which we might want to unwrap?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment its only being used for ProsopoApiError, but yes it should probably be ProsopoBaseError. Changed.

const code = "code" in err ? err.code : 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 function isZodError(err: unknown): err is ZodError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using consts or functions? different declaration in same file between line 184 and line 218

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it to a const.

Don't understand the second comment. What do you mean about a different declaration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was trying to communicate:

Don't mind function or const, both are fine. Just imo best to keep consistent within files, not use both

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
Loading