hono-ban
v0.3.1
Published
HTTP-friendly error objects for Hono, inspired by Boom
Downloads
49
Maintainers
Readme
hono-ban 💥
HTTP-friendly error objects for Hono, inspired by Boom.
[!WARNING]
This package is not production-ready. It is under active development with daily updates and could change significantly.
Table of Contents
Installation
npm install hono-ban
Overview
hono-ban provides a comprehensive error handling solution for Hono.js applications. It offers a set of factory functions for creating HTTP-friendly error objects, middleware for handling errors, and utilities for error conversion and formatting.
Key Features
- Type-Safe Error Creation: Factory functions for all standard HTTP error codes (4xx and 5xx)
- Middleware Integration: Seamless integration with Hono's middleware system
- Flexible Error Data: Support for custom error data and metadata
- Error Conversion: Convert any error to a standardized format
- Customizable Formatting: Extensible error formatting capabilities
- Standard Formatters: Built-in support for common error formats like RFC7807 Problem Details
- Security Features: Built-in sanitization to prevent sensitive data leakage
- Developer-Friendly: Detailed stack traces in development, sanitized in production
Usage
Basic Usage
import { Hono } from "hono";
import ban, { notFound, badRequest } from "hono-ban";
// Create a Hono app
const app = new Hono();
// IMPORTANT: Add the ban middleware for error handling to work
app.use(ban());
// Create a 404 Not Found error
const error = notFound("User not found");
// Create a 400 Bad Request error with custom data
const validationError = badRequest("Invalid input", {
data: {
invalidFields: ["email", "password"],
},
});
// Example route that throws an error
app.get("/users/:id", (c) => {
// The thrown error will be caught and formatted by the ban middleware
throw notFound(`User with ID ${c.req.param("id")} not found`);
});
Note: The ban middleware is required for errors to be automatically caught and formatted. Without it, thrown errors won't be properly handled.
Error Middleware
The ban middleware is required to catch and format errors thrown in your routes. Without this middleware, errors will not be properly handled.
Basic Usage
import { Hono } from "hono";
import ban from "hono-ban";
import { notFound } from "hono-ban";
const app = new Hono();
// Add the error handling middleware with default options
// This is REQUIRED for error handling to work
app.use(ban());
// Routes can throw errors that will be handled automatically
app.get("/users/:id", (c) => {
const user = findUser(c.req.param("id"));
if (!user) {
throw notFound(`User with ID ${c.req.param("id")} not found`);
}
return c.json(user);
});
Advanced Configuration
import { Hono } from "hono";
import ban from "hono-ban";
import { notFound } from "hono-ban";
const app = new Hono();
// Add the error handling middleware with advanced configuration
app.use(
ban({
formatter: customFormatter, // Custom error formatter
sanitize: ["password", "token"], // Fields to remove from error output
includeStackTrace: process.env.NODE_ENV !== "production", // Only include stack traces in development
headers: { "X-Powered-By": "hono-ban" }, // Default headers to include in error responses
})
);
// Routes can throw errors that will be handled automatically
app.get("/users/:id", (c) => {
const user = findUser(c.req.param("id"));
if (!user) {
throw notFound(`User with ID ${c.req.param("id")} not found`);
}
return c.json(user);
});
Custom Error Data
import { badRequest } from "hono-ban";
// Add custom data to your errors
const error = badRequest("Validation failed", {
data: {
field: "email",
reason: "Invalid format",
suggestion: "Use a valid email address",
},
headers: {
"X-Error-Code": "VAL_001",
},
});
Error Conversion
import { convertToBanError, isBanError } from "hono-ban";
try {
// Some operation that might throw
await someOperation();
} catch (err) {
// Convert any error to a BanError
const banError = convertToBanError(err, {
statusCode: 500,
message: "An error occurred during processing",
});
// Check if an error is a BanError
if (isBanError(err)) {
console.log("Status code:", err.status);
}
}
Error Formatting
import { formatError, createErrorResponse, defaultFormatter } from "hono-ban";
import type { ErrorFormatter } from "hono-ban";
// Create a custom formatter
const myFormatter: ErrorFormatter = {
contentType: "application/json",
format(error, headers, sanitize, includeStackTrace) {
return {
code: error.status,
message: error.message,
timestamp: new Date().toISOString(),
details: error.data,
};
},
};
// Format an error using the custom formatter
const formatted = formatError(banError, myFormatter, {
includeStackTrace: true,
});
// Create a Response from the formatted error
const response = createErrorResponse(banError, formatted);
OpenAPI Integration
hono-ban integrates seamlessly with @hono/zod-openapi to provide standardized error handling for OpenAPI-validated routes.
import { OpenAPIHono } from "@hono/zod-openapi";
import { ban, createRFC7807Formatter, rfc7807Hook } from "hono-ban";
import { RFC7807DetailsSchema } from "hono-ban/formatters/rfc7807";
import { createRoute, z } from "@hono/zod-openapi";
import type { Context } from "hono";
import type { Env } from "./config/env";
// Create an OpenAPIHono instance with the RFC7807 hook for validation errors
const app = new OpenAPIHono<Env>({ defaultHook: rfc7807Hook });
// IMPORTANT: Add the ban middleware with RFC7807 formatter
app.use(
ban({
formatter: createRFC7807Formatter({
baseUrl: "https://api.example.com/errors",
}),
})
);
// Define a schema for your data
const NoteSchema = z.object({
name: z.string().max(10),
});
// Create an OpenAPI route with error handling
const route = createRoute({
method: "post",
path: "/notes",
request: {
body: {
content: {
"application/json": {
schema: NoteSchema,
},
},
required: true,
},
},
responses: {
200: {
description: "Successfully created note",
content: {
"application/json": {
schema: z.object({
data: z.object({
id: z.string(),
type: z.string(),
attributes: NoteSchema,
}),
}),
},
},
},
400: {
description: "Validation error",
content: {
"application/json": {
schema: RFC7807DetailsSchema, // Use the RFC7807 schema for errors
},
},
},
},
});
// Register the route
app.openapi(route, async (c) => {
// Handle the request
// Any validation errors will be automatically formatted using RFC7807
return c.json({
data: {
/* response data */
},
});
});
Key Benefits
- Automatic Validation Error Handling: The
rfc7807Hook
automatically converts Zod validation errors to RFC7807 format. - Standardized Error Responses: All errors follow the RFC7807 specification.
- OpenAPI Documentation: Error schemas are properly documented in your OpenAPI specification.
- Type Safety: Full TypeScript support for request and response validation.
Note: The ban middleware is still required when using OpenAPI integration. The
rfc7807Hook
handles validation errors, but the middleware is needed to catch and format other errors.
Formatters
hono-ban includes several built-in formatters for common error response formats.
Default Formatter
The default formatter produces a clean, flat JSON structure with status code, error name, and optional message and data.
import { defaultFormatter } from "hono-ban";
// Example output:
// {
// "statusCode": 400,
// "error": "Bad Request",
// "message": "Invalid input",
// "data": { "field": "email" }
// }
RFC7807 (Problem Details) Formatter
The RFC7807 formatter implements the RFC 7807: Problem Details for HTTP APIs specification.
import { createRFC7807Formatter, createValidationError } from "hono-ban";
// or more specifically:
import {
createRFC7807Formatter,
createValidationError,
} from "hono-ban/formatters/rfc7807";
// Create the formatter
const formatter = createRFC7807Formatter({
baseUrl: "https://api.example.com/problems",
});
// Use with middleware
app.use(ban({ formatter }));
// Create validation error data
app.post("/users", (c) => {
const { email } = await c.req.json();
if (!isValidEmail(email)) {
throw badRequest("Invalid input", {
data: createValidationError([
{ name: "email", reason: "Must be a valid email" },
]),
});
}
// Process valid request...
});
Example RFC7807 Output
{
"type": "https://api.example.com/problems/400",
"title": "Bad Request",
"status": 400,
"detail": "Invalid input",
"instance": "urn:uuid:...",
"timestamp": "2024-02-20T12:00:00.000Z",
"invalid-params": [
{
"name": "email",
"reason": "Must be a valid email"
}
]
}
RFC7807 Helper Functions
The RFC7807 formatter provides several helper functions:
createValidationError(params)
: Create validation error datacreateZodValidationError(error)
: Convert Zod validation errors to RFC7807 formatcreateConstraintViolation(name, reason, resource, constraint)
: Create constraint violation datacreateRFC7807Hook(options)
: Create a Hono hook for Zod OpenAPI validation (see OpenAPI Integration for usage)
API Reference
Error Factories
hono-ban provides factory functions for all standard HTTP error codes:
Client Errors (4xx)
badRequest(messageOrOptions?, options?)
: 400 Bad Requestunauthorized(messageOrOptions?, options?)
: 401 UnauthorizedpaymentRequired(messageOrOptions?, options?)
: 402 Payment Requiredforbidden(messageOrOptions?, options?)
: 403 ForbiddennotFound(messageOrOptions?, options?)
: 404 Not FoundmethodNotAllowed(messageOrOptions?, options?)
: 405 Method Not AllowednotAcceptable(messageOrOptions?, options?)
: 406 Not AcceptableproxyAuthRequired(messageOrOptions?, options?)
: 407 Proxy Authentication RequiredclientTimeout(messageOrOptions?, options?)
: 408 Request Timeoutconflict(messageOrOptions?, options?)
: 409 ConflictresourceGone(messageOrOptions?, options?)
: 410 GonelengthRequired(messageOrOptions?, options?)
: 411 Length RequiredpreconditionFailed(messageOrOptions?, options?)
: 412 Precondition FailedentityTooLarge(messageOrOptions?, options?)
: 413 Payload Too LargeuriTooLong(messageOrOptions?, options?)
: 414 URI Too LongunsupportedMediaType(messageOrOptions?, options?)
: 415 Unsupported Media TyperangeNotSatisfiable(messageOrOptions?, options?)
: 416 Range Not SatisfiableexpectationFailed(messageOrOptions?, options?)
: 417 Expectation Failedteapot(messageOrOptions?, options?)
: 418 I'm a TeapotmisdirectedRequest(messageOrOptions?, options?)
: 421 Misdirected RequestbadData(messageOrOptions?, options?)
: 422 Unprocessable Entitylocked(messageOrOptions?, options?)
: 423 LockedfailedDependency(messageOrOptions?, options?)
: 424 Failed DependencytooEarly(messageOrOptions?, options?)
: 425 Too EarlyupgradeRequired(messageOrOptions?, options?)
: 426 Upgrade RequiredpreconditionRequired(messageOrOptions?, options?)
: 428 Precondition RequiredtooManyRequests(messageOrOptions?, options?)
: 429 Too Many RequestsheaderFieldsTooLarge(messageOrOptions?, options?)
: 431 Request Header Fields Too Largeillegal(messageOrOptions?, options?)
: 451 Unavailable For Legal Reasons
Server Errors (5xx)
internal(messageOrOptions?, options?)
: 500 Internal Server ErrornotImplemented(messageOrOptions?, options?)
: 501 Not ImplementedbadGateway(messageOrOptions?, options?)
: 502 Bad GatewayserverUnavailable(messageOrOptions?, options?)
: 503 Service UnavailablegatewayTimeout(messageOrOptions?, options?)
: 504 Gateway TimeouthttpVersionNotSupported(messageOrOptions?, options?)
: 505 HTTP Version Not SupportedvariantAlsoNegotiates(messageOrOptions?, options?)
: 506 Variant Also NegotiatesinsufficientStorage(messageOrOptions?, options?)
: 507 Insufficient StorageloopDetected(messageOrOptions?, options?)
: 508 Loop DetectednotExtended(messageOrOptions?, options?)
: 510 Not ExtendednetworkAuthRequired(messageOrOptions?, options?)
: 511 Network Authentication RequiredbadImplementation(messageOrOptions?, options?)
: 500 Internal Server Error (marked as developer error)
Core Functions
createError(options)
: Create a new error object with the given optionsconvertToBanError(err, options?)
: Convert any error into a BanErrorisBanError(err, statusCode?)
: Type guard to check if a value is a BanErrorformatError(error, formatter, options?)
: Format an error using the provided formattercreateErrorResponse(error, formatted)
: Create a Response object from a formatted error
Middleware
ban(options?)
: Create error handling middleware with the specified options
Types
interface BanError<T = unknown> {
status: ErrorStatusCode;
message: string;
data?: T;
headers?: Record<string, string>;
allow?: readonly string[];
stack?: string;
cause?: unknown;
causeStack?: string;
readonly isBan: true;
}
interface BanOptions<T = unknown> {
statusCode?: ErrorStatusCode;
message?: string;
data?: T;
headers?: Record<string, string>;
allow?: string | string[];
cause?: Error | unknown;
formatter?: ErrorFormatter;
sanitize?: readonly string[];
includeStackTrace?: boolean;
}
interface BanMiddlewareOptions {
formatter?: ErrorFormatter;
sanitize?: readonly string[];
includeStackTrace?: boolean;
headers?: Record<string, string>;
}
interface ErrorFormatter<T = unknown> {
readonly contentType: string;
format(
error: BanError,
headers?: Record<string, string>,
sanitize?: readonly string[],
includeStackTrace?: boolean
): T;
}
Best Practices
Error Handling Strategy
- Use Specific Error Types: Use the most specific error factory function that matches your use case.
// Good
throw notFound("User not found");
// Less specific
throw createError({ statusCode: 404, message: "User not found" });
- Include Meaningful Data: Add context to your errors to help with debugging and user feedback.
throw badRequest("Invalid input", {
data: {
field: "email",
reason: "Invalid format",
expected: "valid@example.com",
},
});
- Security Considerations: Sanitize sensitive data in production environments.
app.use(
ban({
sanitize: ["password", "token", "secret"],
includeStackTrace: process.env.NODE_ENV !== "production",
})
);
- Developer Errors: Use
badImplementation
for errors that should never happen in production.
if (!database) {
throw badImplementation("Database connection not initialized");
}
Performance Optimization
Reuse Formatters: Create formatters once and reuse them rather than creating new ones for each request.
Selective Stack Traces: Only include stack traces in development to reduce response size in production.
Contributing
We welcome contributions! Please see the main project's Contributing Guide for details.
License
MIT License - see the LICENSE file for details.