Skip to content

udibo/http-error

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Http Error

JSR JSR Score CI codecov license

Utilities for creating and working with Http Errors.

This package was inspired by http-errors for Node.js.

Features

  • Framework agnostic
  • RFC 9457 Problem Details compliant error responses

Usage

Below are some examples of how to use this module.

HttpError

This class can be used on its own to create any HttpError. It has a few different call signatures you can use. The 4 examples below would throw the same error.

import { HttpError } from "@udibo/http-error";

throw new HttpError(404, "file not found");
throw new HttpError(404, { message: "file not found" });
throw new HttpError("file not found", { status: 404 });
throw new HttpError({ status: 404, message: "file not found" });

You can also include a cause in the optional options argument for it like you can with regular errors. Additional HttpErrorOptions include statusText, type (a URI for the problem type), instance (a URI for this specific error occurrence), extensions (an object for additional details), and headers (to customize response headers).

import { HttpError, type HttpErrorOptions } from "@udibo/http-error";

const cause = new Error("Underlying issue");
throw new HttpError(400, "Invalid input", {
  cause,
  type: "/errors/validation-error",
  instance: "/requests/123/user-field",
  extensions: { field: "username", reason: "must be alphanumeric" },
  headers: { "X-Custom-Error-ID": "err-987" },
});

All HttpError objects have a status associated with them. If a status is not provided it will default to 500. The expose property will default to true for client error statuses (4xx) and false for server error statuses (5xx). You can override the default behavior by setting the expose property on the options argument.

For all known HTTP error status codes, a name will be generated (e.g., NotFoundError for 404). If the name is not known, it will default to UnknownClientError or UnknownServerError.

import { HttpError } from "@udibo/http-error";

const error = new HttpError(404, "file not found");
console.log(error.toString()); // NotFoundError: file not found
console.log(error.status); // 404
console.log(error.expose); // true

If you would like to extend the HttpError class, you can pass your own error name in the options.

import { HttpError, type HttpErrorOptions } from "@udibo/http-error";

class CustomError extends HttpError {
  constructor(
    message?: string,
    options?: HttpErrorOptions,
  ) {
    super(message, { name: "CustomError", status: 420, ...options });
  }
}

HttpError.from()

This static method intelligently converts various error-like inputs into an HttpError instance.

  • If an HttpError is passed, it's returned directly.
  • If an Error is passed, it's wrapped in a new HttpError (usually 500 status), with the original error as the cause.
  • If a Response object is passed, it attempts to parse the body as RFC 9457 Problem Details. If successful, it creates an HttpError from those details. If parsing fails or the body isn't Problem Details, it creates a generic HttpError based on the response status. This method is asynchronous and returns a Promise<HttpError>.
  • If a ProblemDetails object is passed, it creates an HttpError from it.
  • For other unknown inputs, it creates a generic HttpError with a 500 status.
import { HttpError } from "@udibo/http-error";

// From a standard Error
const plainError = new Error("Something went wrong");
const httpErrorFromError = HttpError.from(plainError);
console.log(httpErrorFromError.status); // 500
console.log(httpErrorFromError.message); // "Something went wrong"
console.log(httpErrorFromError.cause === plainError); // true

// From a Response (example)
async function handleErrorResponse(response: Response) {
  if (!response.ok) {
    const error = await HttpError.from(response);
    // Now 'error' is an HttpError instance
    throw error;
  }
  return response.json();
}

// From a ProblemDetails object
const problemDetails = {
  status: 403,
  title: "ForbiddenAccess",
  detail: "You do not have permission.",
  type: "/errors/forbidden",
};
const httpErrorFromDetails = HttpError.from(problemDetails);
console.log(httpErrorFromDetails.status); // 403
console.log(httpErrorFromDetails.name); // ForbiddenAccess

HttpError.toJSON()

This method returns a plain JavaScript object representing the error in the RFC 9457 Problem Details format. This is useful for serializing the error to a JSON response body. If expose is false (default for 5xx errors), the detail (message) property will be omitted.

import { HttpError } from "@udibo/http-error";

const error = new HttpError(400, "Invalid input", {
  type: "/errors/validation",
  instance: "/form/user",
  extensions: { field: "email" },
});

const problemDetails = error.toJSON();
console.log(problemDetails);
// Outputs:
// {
//   field: "email",
//   status: 400,
//   title: "BadRequestError",
//   detail: "Invalid input",
//   type: "/errors/validation",
//   instance: "/form/user"
// }

const serverError = new HttpError(500, "Internal details", { expose: false });
console.log(serverError.toJSON());
// Outputs (detail omitted):
// {
//   status: 500,
//   title: "InternalServerError"
// }

HttpError.getResponse()

This method returns a Response object, ready to be sent to the client. The response body will be the JSON string of the Problem Details object (from toJSON()), and headers (including Content-Type: application/problem+json) will be set. The response status and status text will match the error's properties.

import { HttpError } from "@udibo/http-error";

const error = new HttpError(401, "Authentication required");
const response = error.getResponse();

console.log(response.status); // 401
console.log(response.headers.get("Content-Type")); // application/problem+json
// response.body can be read to get the JSON string

createHttpErrorClass()

This factory function allows you to create custom error classes that extend HttpError. You can provide default options (like status, name, message, extensions, etc.) for your custom error class.

import { createHttpErrorClass, type HttpErrorOptions } from "@udibo/http-error";

interface MyApiErrorExtensions {
  errorCode: string;
  requestId?: string;
}

const MyApiError = createHttpErrorClass<MyApiErrorExtensions>({
  name: "MyApiError", // Default name
  status: 452, // Default status
  extensions: {
    errorCode: "API_GENERAL_FAILURE", // Default extension value
  },
});

try {
  throw new MyApiError("Specific operation failed.", {
    extensions: {
      errorCode: "API_OP_X_FAILED", // Override default extension
      requestId: "req-123", // Add instance-specific extension
    },
  });
} catch (e) {
  if (e instanceof MyApiError) {
    console.log(e.name); // "MyApiError"
    console.log(e.status); // 452
    console.log(e.message); // "Specific operation failed."
    console.log(e.extensions.errorCode); // "API_OP_X_FAILED"
    console.log(e.extensions.requestId); // "req-123"
    // console.log(e.toJSON());
  }
}

// Instance with overridden status
const anotherError = new MyApiError(453, "Another failure");
console.log(anotherError.status); // 453
console.log(anotherError.extensions.errorCode); // "API_GENERAL_FAILURE" (from default)

Framework Integration

Hono

Here's an example of how to use HttpError with Hono, utilizing its app.onError handler to return Problem Details JSON responses.

import { Hono } from "hono";
import { HTTPException } from "hono/http-exception"; // For comparison
import { HttpError } from "@udibo/http-error";

const app = new Hono();

app.get("/error", () => {
  throw new Error("This is an example of a plain Error");
});

app.get("/hono-error", () => {
  // Hono's native error, for comparison
  throw new HTTPException(400, {
    message: "This is an example of an error from hono",
  });
});

app.get("/http-error", () => {
  throw new HttpError(400, "This is an example of an HttpError", {
    type: "/errors/http-error",
    instance: "/errors/http-error/instance/123",
    extensions: {
      customField: "customValue",
    },
  });
});

// Global error handler
app.onError(async (cause, c) => { // c is Hono's context
  console.log("Hono onError caught:", cause);

  // Converts non-HttpError instances to HttpError instances
  // For Response objects (e.g. from fetch), HttpError.from is async
  const error = cause instanceof Response
    ? await HttpError.from(cause)
    : HttpError.from(cause);

  console.error(error); // Log the full HttpError

  return error.getResponse(); // Return a Response object directly
});

console.log("Hono server running on http://localhost:8000");
Deno.serve(app.fetch);

Oak

Here is an example of how an Oak server could have middleware that converts any thrown error into an HttpError and returns a Problem Details JSON response.

import { Application, Router } from "@oak/oak";
import { HttpError } from "@udibo/http-error";

const app = new Application();

// Error handling middleware
app.use(async (context, next) => {
  try {
    await next();
  } catch (cause) {
    // Converts non-HttpError instances to HttpError instances
    // For Response objects (e.g. from fetch), HttpError.from is async
    const error = cause instanceof Response
      ? await HttpError.from(cause)
      : HttpError.from(cause);

    console.error(error); // Log the full error

    const { response } = context;
    response.status = error.status;
    error.headers.forEach((value, key) => { // Set custom headers from HttpError
      response.headers.set(key, value);
    });
    // Set Content-Type if not already set by error.headers
    if (!response.headers.has("Content-Type")) {
      response.headers.set("Content-Type", "application/problem+json");
    }
    response.body = error.toJSON(); // Send Problem Details as JSON
  }
});

const router = new Router();
router.get("/test-error", () => {
  // Will be caught and transformed by the middleware
  throw new Error("A generic error occurred!");
});
router.get("/test-http-error", () => {
  throw new HttpError(400, "This is a specific HttpError.", {
    type: "/errors/my-custom-error",
    extensions: { info: "some detail" },
  });
});

app.use(router.routes());
app.use(router.allowedMethods());

console.log("Oak server running on http://localhost:8000");
await app.listen({ port: 8000 });

About

An error class for HTTP requests.

Topics

Resources

License

Stars

Watchers

Forks