Skip to content

Commit

Permalink
chore(feat): add error convention and implementing on error hook (#22)
Browse files Browse the repository at this point in the history
* refactor(app/error): yeah last time instanceof is not working so we'll so back

* feat(app/error/errorbuilder): create new errorbuilder

* feat(app/error/errorbuilder): add new method

* refactor(app/error): improving error using errorbuilder

* fix(app/error): fix linter

* refactor(app/error): modify validator error

* refactor(app/error): modify not found error

* fix(app/error): change not found code

* refactor(app/error/builder): create method for generate MDN web to type

* refactor(app/error/builder): add docs on builder
  • Loading branch information
ikr4-m authored Mar 27, 2024
1 parent 27ce5a4 commit e8d3e22
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 34 deletions.
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
91 changes: 91 additions & 0 deletions src/App/Error/ErrorBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { StatusCode } from "hono/utils/http-status";

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;
}

/**
* Set additional fields on error query
*/
public setKind<D extends T>(data: D): ErrorBuilder<D> {
return this.setProps("kind", data) as unknown as ErrorBuilder<D>;
}

/**
* Set instance or path where error raised on error query
*/
public setInstance(instance: string): this {
return this.setProps("instance", instance);
}

/**
* Set code error type on error query
*/
public setMDNCodeType(code: StatusCode): this {
return this.setProps("type", `https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${code}`);
}

/**
* Set type of error URI on error query
*/
public setType(type: string): this {
return this.setProps("type", type);
}

/**
* Set the title on error query
*/
public setTitle(title: string): this {
return this.setProps("title", title);
}

/**
* Set the detail on error query
*/
public setDetail(detail: string): this {
return this.setProps("detail", detail);
}

/**
* Generate the error query
*/
public build(): IErrorBuilder<T> {
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)
.setMDNCodeType(code);

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)
.setMDNCodeType(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);
}

0 comments on commit e8d3e22

Please sign in to comment.