Skip to content

🥅 server: handle expected bridge pairing errors#933

Open
cruzdanilo wants to merge 1 commit intomainfrom
flake
Open

🥅 server: handle expected bridge pairing errors#933
cruzdanilo wants to merge 1 commit intomainfrom
flake

Conversation

@cruzdanilo
Copy link
Copy Markdown
Member

@cruzdanilo cruzdanilo commented Apr 2, 2026


Open with Devin

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling for bridge credential pairing operations to provide more appropriate error responses when pairing fails.

@cruzdanilo cruzdanilo requested a review from nfmelendez as a code owner April 2, 2026 15:44
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 2, 2026

🦋 Changeset detected

Latest commit: fea9821

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request updates the bridge pairing logic to return a 409 Conflict status instead of a generic error when a database update fails to find a matching credential. The changes include importing HTTPException in the bridge hook and updating the test suite to expect 409 status codes. A review comment points out that using a 4xx error for missing database records might violate repository conventions regarding data integrity issues, suggesting that a 5xx error remains more appropriate.

.returning({ account: credentials.account, source: credentials.source })
.then(([updated]) => {
if (!updated) throw new Error("no match found when pairing bridge id");
if (!updated) throw new HTTPException(409, { message: "credential pairing failed" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

According to the general rules of this repository, when an expected database record is not found, a 5xx-level error is preferred over a 4xx to indicate a system or data integrity issue. Since !updated here could mean the referenceId (obtained from Persona) does not exist in the credentials table, a 500 error might be more appropriate than a 409 Conflict, unless this specific failure is considered a recoverable state conflict rather than a data integrity issue.

References
  1. Throw an error when an expected database record (like a card) is not found. This indicates a system or data integrity issue, not a client-side error that can be fixed by the user, so a 5xx-level error is more appropriate than a 4xx.

.returning({ account: credentials.account, source: credentials.source })
.then(([updated]) => {
if (!updated) throw new Error("no match found when pairing bridge id");
if (!updated) throw new HTTPException(409, { message: "credential pairing failed" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The global app.onError() handler doesn't check for HTTPException. It will catch the new 409 exception, log it, and incorrectly return a 555 status instead of 409.
Severity: MEDIUM

Suggested Fix

Update the global app.onError() handler in server/index.ts to check if the error is an instanceof HTTPException. If it is, re-throw the exception or return its response directly. For all other errors, maintain the existing behavior of calling captureException() and returning a 555 response.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/hooks/bridge.ts#L132

Potential issue: The code at `server/hooks/bridge.ts:132` now throws an
`HTTPException(409)` for credential pairing failures. While the intent is to return a
409 status code, the global `app.onError()` handler in `server/index.ts` does not
differentiate `HTTPException` from other errors. As a result, when this exception is
thrown in production, it will be caught by the global handler, which will log it to
Sentry via `captureException()` and return a generic 555 error response. This defeats
the purpose of using `HTTPException` and will cause clients to receive an incorrect
status code. The tests pass because they only test the isolated sub-app, not the full
application stack with the global error handler.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

.returning({ account: credentials.account, source: credentials.source })
.then(([updated]) => {
if (!updated) throw new Error("no match found when pairing bridge id");
if (!updated) throw new HTTPException(409, { message: "credential pairing failed" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Webhook caller behavior may change with 409 vs 500

Bridge's webhook system may treat 4xx and 5xx responses differently — many webhook providers retry on 5xx (transient server errors) but not on 4xx (client errors indicating the request itself is invalid). Changing from 500 to 409 could stop Bridge from retrying these specific webhook deliveries. This appears intentional given the PR's stated goal of handling "expected" pairing errors, but worth confirming that Bridge's retry behavior for 409 is acceptable for these scenarios (credential already paired, or reference-id not found).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Walkthrough

A patch-level release for the @exactly/server package improves error handling in the bridge credential-pairing flow. When database updates fail to pair a bridge ID, the handler now throws an HTTPException with a 409 (conflict) status code instead of a generic error, with corresponding test updates.

Changes

Cohort / File(s) Summary
Release Metadata
.changeset/calm-panda-handle.md
Added changelog entry for patch release documenting improved handling of expected bridge pairing errors.
Bridge Credential Pairing
server/hooks/bridge.ts
Modified error handling to throw HTTPException(409) with explicit message when database update fails to pair bridge ID, replacing generic Error.
Test Updates
server/test/hooks/bridge.test.ts
Updated two test cases to expect HTTP status 409 instead of 500 for credential pairing failure scenarios during persona email fallback.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • nfmelendez
🚥 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 directly addresses the main change: handling expected bridge pairing errors by returning HTTP 409 instead of generic errors.

✏️ 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 flake

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sentry
Copy link
Copy Markdown

sentry bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.69%. Comparing base (12f9b8e) to head (fea9821).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #933   +/-   ##
=======================================
  Coverage   71.69%   71.69%           
=======================================
  Files         228      228           
  Lines        8277     8277           
  Branches     2661     2661           
=======================================
  Hits         5934     5934           
  Misses       2113     2113           
  Partials      230      230           
Flag Coverage Δ
e2e 52.39% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/test/hooks/bridge.test.ts (1)

288-306: ⚠️ Potential issue | 🟠 Major

This test is order-dependent and can fail when run in isolation.

The new 409 expectation relies on fallback-test already being paired by a previous test. Please set that precondition inside this test to keep it deterministic.

🔧 Suggested deterministic setup
 it("returns 409 when fallback credential already paired", async () => {
+  await database.update(credentials).set({ bridgeId: "already-paired-bridge-id" }).where(eq(credentials.id, "fallback-test"));
   vi.spyOn(database.query.credentials, "findFirst").mockResolvedValueOnce(undefined); // eslint-disable-line unicorn/no-useless-undefined
   vi.spyOn(bridge, "getCustomer").mockResolvedValue({
-    id: "conflict-bridge-id",
+    id: "fresh-bridge-id",
     email: "conflict@example.com",
     status: "active",
     endorsements: [],
   });

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b602c26-cbdb-4820-85e6-6b16d030fa83

📥 Commits

Reviewing files that changed from the base of the PR and between 12f9b8e and fea9821.

📒 Files selected for processing (3)
  • .changeset/calm-panda-handle.md
  • server/hooks/bridge.ts
  • server/test/hooks/bridge.test.ts

.returning({ account: credentials.account, source: credentials.source })
.then(([updated]) => {
if (!updated) throw new Error("no match found when pairing bridge id");
if (!updated) throw new HTTPException(409, { message: "credential pairing failed" });
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== confirm throw site =="
rg -n -C3 'throw new HTTPException\(409' server/hooks/bridge.ts

echo
echo "== inspect global error handler behavior =="
rg -n -C8 'app\.onError|captureException\(|unexpected error|555 as UnofficialStatusCode' server/index.ts

Repository: exactly/exa

Length of output: 1810


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find HTTPException import source =="
rg -n 'import.*HTTPException|from.*HTTPException' server/hooks/bridge.ts

echo
echo "== search for HTTPException definition in codebase =="
rg -n 'class HTTPException|export.*HTTPException' --type ts

echo
echo "== check if Hono exports HTTPException =="
rg -n 'HTTPException' package.json

Repository: exactly/exa

Length of output: 203


🌐 Web query:

Hono HTTPException error handling how does it work

💡 Result:

Hono's HTTPException provides a standardized way to throw and handle HTTP errors. Import it from 'hono/http-exception'. To throw an HTTPException: - Basic: new HTTPException(401, { message: 'Unauthorized' }) - Custom response: new HTTPException(401, { res: new Response('Custom', { status: 401 }) }) - With cause: new HTTPException(401, { message: 'Error', cause: originalError }) Throw it in handlers or middleware for validation failures, auth errors, etc. Handling: Use app.onError((err, c) => { if (err instanceof HTTPException) { return err.getResponse; // Returns Response with status and message/res } // Handle other errors, e.g. console.error(err); return c.text('Internal Server Error', 500); }); This catches all unhandled exceptions, including HTTPExceptions from Hono core/middleware. HTTPException extends Error with status, res?, and getResponse method. For structured JSON errors, customize in onError: return c.json({ error: err.message, status: err.status }, err.status); Official docs confirm this mechanism remains current as of 2026.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== check for other error handlers or middleware before app.onError =="
rg -n 'app\.use.*error|middleware.*error|app\.onError' server/index.ts | head -20

echo
echo "== verify HTTPException is imported in index.ts =="
rg -n 'HTTPException' server/index.ts

Repository: exactly/exa

Length of output: 208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== check full error handler logic to confirm status extraction won't match 'credential pairing failed' =="
sed -n '284,316p' server/index.ts

Repository: exactly/exa

Length of output: 1501


HTTPException(409) is masked by the global error handler.

Line 132 throws HTTPException(409, ...), but the global error handler in server/index.ts:284–316 only tries to extract a 3-digit status from error.message. Since the message is "credential pairing failed" (not a status code), hasStatus remains false, and the handler unconditionally returns 555 regardless of the thrown status code.

Fix by either:

  1. Importing and checking instanceof HTTPException in app.onError, then using err.getResponse() or c.json(..., err.status) to preserve the intended status, or
  2. Returning a 409 response directly from the handler instead of throwing.

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