ProvenPin Install

http-errors with Express error middleware: working versions (tested 2026-10-18)

Express routes that throw http-errors and reject asynchronously, mapped by a central middleware to RFC 7807 problem details with expose-aware messages and a 500 fallback, tested offline with supertest.

passed7 passed · 0 failed2026-10-18 03:58 UTC
Status
passed
Last passed
2026-10-18 03:58 UTC
Tests
7 / 7
Language
TypeScript
Runtime
node v22.23.3
pinned versions8
express@5.3.0http-errors@2.0.1supertest@7.3.1@types/express@5.0.6@types/http-errors@2.0.5@types/node@26.6.5@types/supertest@7.2.1tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 39095442401

The code

Exactly the files that passed. Your agent can pull them with get_recipe("http-errors-express-error-middleware") (install), or see them on GitHub.

package.json
{
  "name": "http-errors-express-error-middleware",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "express": "5.3.0",
    "http-errors": "2.0.1",
    "supertest": "7.3.1"
  },
  "devDependencies": {
    "@types/express": "5.0.6",
    "@types/http-errors": "2.0.5",
    "@types/node": "26.6.5",
    "@types/supertest": "7.2.1",
    "tsx": "4.23.15"
  }
}
src/problem.ts
import { STATUS_CODES } from "node:http";
import { isHttpError } from "http-errors";
import type { ErrorRequestHandler, Request } from "express";

export type ProblemDetails = {
  type: string;
  title: string;
  status: number;
  detail: string;
  instance: string;
  [key: string]: unknown;
};

const RESERVED = new Set([
  "type",
  "title",
  "status",
  "detail",
  "instance",
  "statusCode",
  "expose",
  "name",
  "message",
  "stack",
  "headers",
]);

export function toProblemDetails(err: unknown, req: Request): ProblemDetails {
  const httpError = isHttpError(err) ? err : undefined;
  const status = httpError ? httpError.status : 500;
  const title = STATUS_CODES[status] ?? "Internal Server Error";
  const expose = httpError ? httpError.expose : false;
  const detail = expose && httpError ? httpError.message : title;

  const problem: ProblemDetails = {
    type: `https://example.com/problems/${status}`,
    title,
    status,
    detail,
    instance: req.originalUrl,
  };

  if (expose) {
    for (const [key, value] of Object.entries(err as Record<string, unknown>)) {
      if (RESERVED.has(key)) continue;
      problem[key] = value;
    }
  }

  return problem;
}

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  if (res.headersSent) {
    next(err);
    return;
  }
  const problem = toProblemDetails(err, req);
  res.status(problem.status).type("application/problem+json").json(problem);
};
src/app.ts
import createError from "http-errors";
import express from "express";
import { errorHandler } from "./problem.ts";

type User = { id: string; email: string };

export function createApp() {
  const app = express();
  app.use(express.json());

  const users = new Map<string, User>([
    ["1", { id: "1", email: "[email protected]" }],
  ]);

  app.get("/users/:id", (req, res) => {
    const user = users.get(req.params.id);
    if (!user) {
      throw createError(404, "User not found");
    }
    res.json(user);
  });

  app.post("/users", (req, res) => {
    const email: unknown = (req.body ?? {}).email;
    if (typeof email !== "string" || !email.includes("@")) {
      throw createError(422, "Validation failed", {
        errors: [{ field: "email", message: "must be a valid email" }],
      });
    }
    const id = String(users.size + 1);
    const user: User = { id, email };
    users.set(id, user);
    res.status(201).json(user);
  });

  app.get("/reports/:id", async (req, res) => {
    await Promise.resolve();
    if (req.params.id !== "42") {
      throw createError(404, `Report ${req.params.id} not found`);
    }
    res.json({ id: req.params.id });
  });

  app.get("/maintenance", () => {
    throw createError(503, "Down for maintenance", { expose: true });
  });

  app.get("/boom", () => {
    throw new Error("secret database credentials");
  });

  app.use((_req, _res, next) => {
    next(createError(404, "Route not found"));
  });

  app.use(errorHandler);

  return app;
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { createApp } from "../src/app.ts";

const app = createApp();

test("returns a known user", async () => {
  const res = await request(app).get("/users/1");
  assert.equal(res.status, 200);
  assert.deepEqual(res.body, { id: "1", email: "[email protected]" });
});

test("maps a thrown 404 to problem details", async () => {
  const res = await request(app).get("/users/999");
  assert.equal(res.status, 404);
  assert.match(String(res.headers["content-type"]), /application\/problem\+json/);
  assert.equal(res.body.type, "https://example.com/problems/404");
  assert.equal(res.body.title, "Not Found");
  assert.equal(res.body.status, 404);
  assert.equal(res.body.detail, "User not found");
  assert.equal(res.body.instance, "/users/999");
});

test("exposes custom properties on a 422", async () => {
  const res = await request(app).post("/users").send({ email: "not-an-email" });
  assert.equal(res.status, 422);
  assert.equal(res.body.title, "Unprocessable Entity");
  assert.equal(res.body.detail, "Validation failed");
  assert.deepEqual(res.body.errors, [
    { field: "email", message: "must be a valid email" },
  ]);
});

test("forwards async route rejections", async () => {
  const res = await request(app).get("/reports/7");
  assert.equal(res.status, 404);
  assert.equal(res.body.detail, "Report 7 not found");
});

test("serves an exposed 5xx message when opted in", async () => {
  const res = await request(app).get("/maintenance");
  assert.equal(res.status, 503);
  assert.equal(res.body.detail, "Down for maintenance");
});

test("hides the message of an unexposed 500", async () => {
  const res = await request(app).get("/boom");
  assert.equal(res.status, 500);
  assert.equal(res.body.title, "Internal Server Error");
  assert.equal(res.body.detail, "Internal Server Error");
  assert.equal(JSON.stringify(res.body).includes("secret"), false);
});

test("returns problem details for unknown routes", async () => {
  const res = await request(app).get("/missing");
  assert.equal(res.status, 404);
  assert.equal(res.body.detail, "Route not found");
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.