From e54fd81f2fa1e02b3307acc5fa7781ffa04d7981 Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Thu, 16 Apr 2026 15:21:02 -0400 Subject: [PATCH] Add consistent JSON error responses with structured error codes All error responses now include an `error` machine-readable code alongside `message`. Consolidate FourHundredResponse/FiveHundredResponse behind a shared ErrorResponse base class, add UnauthorizedResponse, and fix a for...in/for...of bug in queryParams that silently dropped all query parameters. Co-Authored-By: Claude Sonnet 4.6 --- src/aggregation/responses.ts | 58 ++++++++++++++++++++---------------- src/index.ts | 20 ++++++------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/aggregation/responses.ts b/src/aggregation/responses.ts index bc49f60..2fcf6da 100644 --- a/src/aggregation/responses.ts +++ b/src/aggregation/responses.ts @@ -1,56 +1,64 @@ -/** - * It's not you, it's us. - */ -export class FiveHundredResponse { - constructor(message: string, errorCode: number) { +class ErrorResponse { + constructor( + message: string, + readonly errorCode: number, + error: string, + ) { this.message = message; - this.errorCode = errorCode; + this.error = error; } message: string; - errorCode: number; -} + error: string; -export class InternalErrorResponse extends FiveHundredResponse { - constructor() { - super("Internal error", 500); + toJSON() { + return { error: this.error, message: this.message }; } } /** * It's not us, it's you. */ -export class FourHundredResponse { - constructor(message: string, errorCode: number) { - this.message = message; - this.errorCode = errorCode; - } - - message: string; - errorCode: number; -} +export class FourHundredResponse extends ErrorResponse {} export class InvalidEmail extends FourHundredResponse { constructor() { - super("Invalid email address.", 400); + super("Invalid email address.", 400, "invalid_email"); } } export class UnrecognizedParameters extends FourHundredResponse { constructor(message: string) { - super("Unrecognized parameters: " + message, 400); + super("Unrecognized parameters: " + message, 400, "unrecognized_parameters"); } } export class InvalidParameter extends FourHundredResponse { constructor(message: string) { - super("Invalid parameter: " + message, 400); + super("Invalid parameter: " + message, 400, "invalid_parameter"); } } export class TooManyIdentifiers extends FourHundredResponse { constructor(message: string) { - super(message, 400); + super(message, 400, "too_many_identifiers"); + } +} + +export class UnauthorizedResponse extends FourHundredResponse { + constructor() { + super("Unauthorized", 401, "unauthorized"); + } +} + +/** + * It's not you, it's us. + */ +export class FiveHundredResponse extends ErrorResponse {} + +export class InternalErrorResponse extends FiveHundredResponse { + constructor() { + super("Internal error", 500, "internal_error"); } } @@ -82,7 +90,7 @@ interface Facet { field: string; type: string; buckets: Bucket[]; - bucketsLabel: String; + bucketsLabel: string; } interface Bucket { diff --git a/src/index.ts b/src/index.ts index 4ea56dc..99945f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { DPLADocList, FourHundredResponse, FiveHundredResponse, + UnauthorizedResponse, EmailSent, } from "./aggregation/responses"; import ApiKeyRepository from "./aggregation/api_key_repository"; @@ -115,12 +116,14 @@ function worker() { } if (!apiKeyRepository.isApiKeyValid(apiKey)) { - return res.status(401).json({ message: "Unauthorized" }); + const r = new UnauthorizedResponse(); + return res.status(r.errorCode).json(r); } const user = await apiKeyRepository.findUserByApiKey(apiKey); if (!user) { - return res.status(401).json({ message: "Unauthorized" }); + const r = new UnauthorizedResponse(); + return res.status(r.errorCode).json(r); } next(); @@ -136,14 +139,11 @@ function worker() { const queryParams = (req: express.Request): Map => { const params = new Map(); - for (const key in Object.entries(req.query)) { - if (req.query.hasOwnProperty(key)) { - const value = req.query[key]; - if (typeof value === "string") { - params.set(key, value); - } else if (Array.isArray(value)) { - params.set(key, value[0] as string); - } + for (const [key, value] of Object.entries(req.query)) { + if (typeof value === "string") { + params.set(key, value); + } else if (Array.isArray(value)) { + params.set(key, value[0] as string); } } return params;