Skip to content

Add consistent JSON error responses with structured error codes#10

Closed
DominicBM wants to merge 1 commit into
mainfrom
json-error-responses
Closed

Add consistent JSON error responses with structured error codes#10
DominicBM wants to merge 1 commit into
mainfrom
json-error-responses

Conversation

@DominicBM
Copy link
Copy Markdown
Contributor

@DominicBM DominicBM commented Apr 16, 2026

Summary

  • All error responses now include a machine-readable error field alongside message (e.g. {"error": "unauthorized", "message": "Unauthorized"})
  • Consolidates the now-identical FourHundredResponse and FiveHundredResponse behind a shared ErrorResponse base class
  • Adds UnauthorizedResponse class; authMiddleware now uses it instead of inline literals
  • Fixes a for...in / for...of bug in queryParams that silently dropped all query parameters (iterating Object.entries() with for...in yields index strings, not [key, value] pairs)

Error format

Before:

{"message": "Unauthorized"}

After:

{"error": "unauthorized", "message": "Unauthorized"}

All existing subclasses (InvalidEmail, UnrecognizedParameters, InvalidParameter, TooManyIdentifiers, InternalErrorResponse) carry their own error codes unchanged.

Test plan

  • GET /items with no API key → 401 {"error": "unauthorized", "message": "Unauthorized"}
  • GET /items with invalid API key → 401 {"error": "unauthorized", "message": "Unauthorized"}
  • GET /items?q=dogs with valid API key → 200 with results
  • GET /items?badparam=x400 {"error": "unrecognized_parameters", "message": "Unrecognized parameters: badparam"}
  • Query parameter values reach the search controller (regression for the for...in fix)

🤖 Generated with Claude Code

Summary

This PR adds structured, machine-readable error codes to all JSON error responses throughout the API. All error responses now include both an error field with a machine-readable code (e.g., "unauthorized", "invalid_email", "internal_error") and a human-readable message field.

Key Changes

Response Structure (Public API)

  • Introduces an ErrorResponse base class that adds a structured error field to all error responses
  • Example: {"error": "unauthorized", "message": "Unauthorized"} (previously just {"message": "Unauthorized"})
  • All existing error subclasses (InvalidEmail, UnrecognizedParameters, InvalidParameter, TooManyIdentifiers, InternalErrorResponse) now include consistent error codes

Auth Middleware

  • Adds a new UnauthorizedResponse class (401 status) for authentication failures
  • Replaces hardcoded { message: "Unauthorized" } responses with structured UnauthorizedResponse instances in both API key validation paths
  • Uses res.status(r.errorCode).json(r) pattern for consistent error handling

Bug Fix

  • Fixes a bug in queryParams extraction where incorrect loop syntax with Object.entries() was causing query parameter values to be silently dropped
  • Now correctly iterates over query parameters and maps both string and array values to the returned Map

API Impact

⚠️ Public API Response Shape Change: This is a breaking change for clients expecting the previous error response format. All error responses now include the error field alongside message.

No Deployment Changes Required

  • No environment variables added/removed/modified
  • No database migrations
  • No infrastructure configuration changes
  • No manual pipeline trigger or ECS redeployment required

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 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Walkthrough

This pull request refactors error response handling by introducing a base ErrorResponse class that consolidates error code and error identifier fields, converting existing response classes to extend this base, adding a new UnauthorizedResponse for 401 errors, and updating middleware to use the new response structures.

Changes

Cohort / File(s) Summary
Error Response Class Hierarchy
src/aggregation/responses.ts
Introduced internal ErrorResponse base class with errorCode, error identifier, and toJSON() method. Refactored FourHundredResponse and FiveHundredResponse to extend the base class. Updated all 4xx subclasses (InvalidEmail, UnrecognizedParameters, InvalidParameter, TooManyIdentifiers) to pass error identifier strings to base constructor. Added new UnauthorizedResponse (401) class. Updated InternalErrorResponse with error identifier. Minor type change: bucketsLabel: String to bucketsLabel: string.
Auth Middleware & Utilities
src/index.ts
Replaced hardcoded 401 JSON payloads with constructed UnauthorizedResponse instances in auth middleware. Refactored queryParams extraction from for...in loop to Object.entries() iteration, removing hasOwnProperty guard.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing consistent JSON error responses with structured error codes through a new ErrorResponse base class and error field additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch json-error-responses

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/aggregation/responses.ts (1)

1-17: Consider hardening ErrorResponse as an abstract immutable base.

Small maintainability improvement: make this base abstract and mark message/error as readonly since these values should not mutate after construction.

♻️ Proposed refactor
-class ErrorResponse {
+abstract class ErrorResponse {
   constructor(
-    message: string,
+    readonly message: string,
     readonly errorCode: number,
-    error: string,
-  ) {
-    this.message = message;
-    this.error = error;
-  }
-
-  message: string;
-  error: string;
+    readonly error: string,
+  ) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/aggregation/responses.ts` around lines 1 - 17, Make ErrorResponse an
abstract, immutable base by declaring the class abstract and marking its
instance properties message and error as readonly so they cannot be mutated
after construction; update the constructor (the ErrorResponse constructor) to
initialize these readonly properties (or use readonly parameter properties) and
keep toJSON() unchanged to return { error, message }. Ensure references to
ErrorResponse, message, error, constructor, and toJSON are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 142-147: The loop over req.query is writing array entries into the
params Map<string,string> without checking array contents; update the
Array.isArray branch (where params.set(key, value[0] as string) is used) to
first ensure value.length > 0 and typeof value[0] === "string" (or coerce safely
with String(value[0]) if you intend to accept non-strings) before calling
params.set; otherwise skip the key (or log/handle the empty/non-string case) so
you never insert undefined or invalid types into params.

---

Nitpick comments:
In `@src/aggregation/responses.ts`:
- Around line 1-17: Make ErrorResponse an abstract, immutable base by declaring
the class abstract and marking its instance properties message and error as
readonly so they cannot be mutated after construction; update the constructor
(the ErrorResponse constructor) to initialize these readonly properties (or use
readonly parameter properties) and keep toJSON() unchanged to return { error,
message }. Ensure references to ErrorResponse, message, error, constructor, and
toJSON are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4b6e5bf-67f8-43ad-9edf-631976b2a43f

📥 Commits

Reviewing files that changed from the base of the PR and between fab8380 and e54fd81.

📒 Files selected for processing (2)
  • src/aggregation/responses.ts
  • src/index.ts

Comment thread src/index.ts
Comment on lines +142 to 147
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard array query values before writing to Map<string, string>.

On Line 146, value[0] as string can write undefined (empty array) or non-string values. Add a runtime type check before params.set.

🛠️ Proposed fix
     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);
+        const first = value[0];
+        if (typeof first === "string") {
+          params.set(key, first);
+        }
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
for (const [key, value] of Object.entries(req.query)) {
if (typeof value === "string") {
params.set(key, value);
} else if (Array.isArray(value)) {
const first = value[0];
if (typeof first === "string") {
params.set(key, first);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 142 - 147, The loop over req.query is writing
array entries into the params Map<string,string> without checking array
contents; update the Array.isArray branch (where params.set(key, value[0] as
string) is used) to first ensure value.length > 0 and typeof value[0] ===
"string" (or coerce safely with String(value[0]) if you intend to accept
non-strings) before calling params.set; otherwise skip the key (or log/handle
the empty/non-string case) so you never insert undefined or invalid types into
params.

@DominicBM
Copy link
Copy Markdown
Contributor Author

Wrong repository — production API is dpla/api (Scala/Akka). Closing.

@DominicBM DominicBM closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant