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

chore(feat): add error convention and implementing on error hook #22

Merged
merged 10 commits into from
Mar 27, 2024
2 changes: 1 addition & 1 deletion src/App/Decorator/Middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function validator<T extends {}>(method: keyof ValidationTargets, data: Z
const targetFunc = descriptor.value as Function;
const funcValidator = honoValidator(method, (val, c) => {
const parsed = z.object(data).safeParse(val);
if (!parsed.success) return onValidateError(c, data);
if (!parsed.success) return onValidateError(c, parsed.error.issues);
return parsed.data;
});
reflectingMetadata(targetFunc, funcValidator);
Expand Down
64 changes: 64 additions & 0 deletions src/App/Error/ErrorBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type IErrorBuilder<T> = {
title: string;
detail: string;
type: string;
instance?: string;
kind?: T;
};

/**
* Error builder for Agni based on IETF Error Convention.
*
* @see - RFC 9457
*/
export default class ErrorBuilder<T extends {}> implements IErrorBuilder<T> {
public type: string = "about:blank";
public instance?: string;
public kind?: T;

public constructor(public title: string, public detail: string) {}

private setProps<P extends keyof this>(prop: P, val: this[P]): this {
this[prop] = val;
return this;
}

public setKind<D extends T>(data: D): ErrorBuilder<D> {
return this.setProps("kind", data) as unknown as ErrorBuilder<D>;
}

public setInstance(instance: string): this {
return this.setProps("instance", instance);
}

public setType(type: string): this {
return this.setProps("type", type);
}

public setTitle(title: string): this {
return this.setProps("title", title);
}

public setDetail(detail: string): this {
return this.setProps("detail", detail);
}

public build(): any {
const data: IErrorBuilder<T> = {
type: this.type,
title: this.title,
detail: this.detail
};

if (this.instance !== undefined) {
data.instance = this.instance;
}

if (this.kind !== undefined) {
data.kind = this.kind;
}

return data;
}
}

39 changes: 13 additions & 26 deletions src/Function/OnError.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
import { randomUUID } from "node:crypto";
import type { Context, Env } from "hono";
import type { HTTPException } from "hono/http-exception";
import { HTTPException } from "hono/http-exception";
import type { StatusCode } from "hono/utils/http-status";
import ErrorBuilder from "App/Error/ErrorBuilder.js";
import isJson from "App/Function/IsJson.js";
import Logger from "Logger.js";

type ErrorRecord = {
message: string;
errMessage?: string;
uuidErr?: string;
};

export default function onError(err: Error, c: Context<Env, any, any>): Response {
const errResponse: ErrorRecord = {
message: ""
};
errResponse.message = "Something happened, but don't worry maybe it's you or our developer.";
errResponse.errMessage = err.message;
const builder = new ErrorBuilder(
"Fatal error from server.", err.message
);
let code: StatusCode = 500;

// Expecting this is HTTPError
if (Object.keys(err).includes("status")) {
const newErr = err as HTTPException;
code = newErr.status;

errResponse.message = newErr.message;
delete errResponse.errMessage;
if (err instanceof HTTPException) {
code = err.status;
builder.setTitle("HTTP Error has thrown.");
}

// If fatal error
if (code === 500) {
errResponse.uuidErr = randomUUID();
Logger.child({ uuid: errResponse.uuidErr }).error(err, "Fatal error on server");
const uuid = randomUUID();
builder.setKind({ uuid });
Logger.child({ uuid }).error(err, "Fatal error on server");
}

const jsonMethod = isJson(c.req.header());
if (jsonMethod) return c.json(errResponse, code);
if (jsonMethod) return c.json(builder.build(), code);

// You can improve on this side, this is just example.
let msgText = errResponse.message;
if (errResponse.errMessage !== undefined) msgText += `\n${errResponse.errMessage}`;
if (errResponse.uuidErr !== undefined) msgText += `\nUUID Code: ${errResponse.uuidErr}`;
return c.text(msgText, code);
return c.text(`Error: ${builder.title}\n${builder.detail}`, code);
}
10 changes: 7 additions & 3 deletions src/Function/OnNotFound.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { Context } from "hono";
import ErrorBuilder from "App/Error/ErrorBuilder.js";
import isJson from "App/Function/IsJson.js";

export function onNotFound(c: Context<any, any, any>): Response {
const message = "Path not found.";
const code = 404;
const builder = new ErrorBuilder("Not found.", "Path or route not found.");
builder
.setInstance(c.req.path)
.setType(`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${code}`);
satoufuyuki marked this conversation as resolved.
Show resolved Hide resolved

const jsonMethod = isJson(c.req.header());
if (jsonMethod) return c.json({ message }, code);
if (jsonMethod) return c.json(builder.build(), code);

return c.text(message, code);
return c.text(builder.detail, code);
}
12 changes: 8 additions & 4 deletions src/Function/OnValidateError.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { Context } from "hono";
import type { ZodData } from "App/Decorator/Middleware.js";
import ErrorBuilder from "App/Error/ErrorBuilder.js";
import isJson from "App/Function/IsJson.js";

export function onValidateError(c: Context<any, any, any>, data: ZodData<any>): Response {
const message = "Bad request performed.";
const code = 400;
const propsOnServer = Object.keys(data);
const builder = new ErrorBuilder("Bad request.", "Bad request performed.");
builder
.setKind(data)
.setInstance(c.req.path)
.setType(`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${code}`);

const jsonMethod = isJson(c.req.header());
if (jsonMethod) return c.json({ message, propsOnServer }, code);
if (jsonMethod) return c.json(builder.build(), code);

return c.text(message, code);
return c.text(builder.detail, code);
}