Skip to content

Fix error reporting and handling across all quickstart backends#565

Open
phoenixy1 wants to merge 6 commits intomasterfrom
ah-surface-plaid-api-errors
Open

Fix error reporting and handling across all quickstart backends#565
phoenixy1 wants to merge 6 commits intomasterfrom
ah-surface-plaid-api-errors

Conversation

@phoenixy1
Copy link
Copy Markdown
Collaborator

@phoenixy1 phoenixy1 commented Apr 16, 2026

Summary

  • Errors from Link were not surfaced at all. Errors like INVALID_LINK_CUSTOMIZATION and INSTITUTION_REGISTRATION_REQUIRED come from the Link component (via the onExit callback), not from backend API calls. The frontend had no handler for these, so they were silently dropped. These are now displayed in the UI and forwarded to backend logs via a new /api/link_exit_error endpoint in each backend.
  • Errors from backend API calls displayed with incorrect or missing information. Python showed no error message, Ruby showed "UNKNOWN" / "API_ERROR" instead of the real error code and type, and Java was missing user-friendly display messages. Error formatting is now consistent across all backends, matching Node's approach of passing through the full Plaid error body.
  • Error handling was duplicated per-endpoint instead of centralized. Python, Ruby, and Java each had per-route try/catch blocks. These are replaced with centralized handlers (@app.errorhandler in Python, Sinatra error blocks in Ruby, ExceptionMapper + shared PlaidApiHelper.callPlaid() in Java). Java specifically needs the helper pattern because Retrofit doesn't throw on HTTP errors — it returns them as successful responses with error bodies, so each endpoint would otherwise need its own if (!response.isSuccessful()) boilerplate. The other backends' HTTP clients throw on errors, so a single global handler is sufficient.
  • Polling retry logic was broken or too aggressive. Some backends retried on all errors instead of only PRODUCT_NOT_READY, and none retried on transient 5xx errors. Java's AssetsResource had a no-op polling loop. Node's pollWithRetries never propagated rejections. All backends now share the same retry policy: retry on PRODUCT_NOT_READY or 5xx, fail fast on everything else.
  • Frontend didn't handle non-JSON error responses. If a backend returned HTML (e.g. Ruby's Sinatra stack trace page, or a proxy 502), response.json() threw an unhandled exception. The frontend now checks response.ok and gracefully handles parse failures.
  • Go and Node error handlers returned HTTP 200 for all Plaid errors. The frontend received errors but with a 200 status, masking the actual failure. Both now return the proper HTTP status code from the Plaid error response.
  • Common errors lacked actionable guidance. The frontend now shows contextual tips for specific error codes — INVALID_LINK_CUSTOMIZATION points to the dashboard Data Transparency settings, and INSTITUTION_REGISTRATION_REQUIRED explains OAuth institution availability timelines.

Frontend

  • Add onExit callback to Link component to capture and surface Link errors
  • Display Link exit errors in the UI via a warning callout in the header
  • Send Link exit errors to the backend (/api/link_exit_error) so they appear in server logs as well as the UI
  • Add contextual tips for INVALID_LINK_CUSTOMIZATION (dashboard link) and INSTITUTION_REGISTRATION_REQUIRED (OAuth timing guidance)
  • Handle both wrapped ({error: ...}) and flat error formats in generateToken
  • Check response.ok before calling response.json() so non-JSON error responses (e.g. HTML 502) don't throw unhandled exceptions

Python

  • Add /api/link_exit_error endpoint to log Link errors on the server
  • Fix format_error — was mapping error_message into display_message and omitting error_message entirely, so the frontend had nothing to show. Now spreads the full Plaid error body, matching Node's approach.
  • Centralize error handling via @app.errorhandler instead of per-endpoint try/catch
  • Fix 4 endpoints returning raw error bodies without the { error: ... } wrapper the frontend expects
  • Harden poll_with_retries JSON parsing so a non-JSON 5xx doesn't crash instead of retrying

Ruby

  • Add /api/link_exit_error endpoint to log Link errors on the server
  • Fix Sinatra's show_exceptions middleware replacing JSON error responses with HTML stack trace pages — the frontend couldn't parse the HTML and fell back to hardcoded "UNKNOWN" / "API_ERROR" defaults
  • Disable dump_errors to stop logging full stack traces for handled errors
  • Centralize error handling via Sinatra error block instead of per-endpoint rescue
  • Add generic catch-all error handler that returns clean JSON
  • Add defensive rescue inside the Plaid error handler
  • Harden poll_with_retries JSON parsing

Java

  • Add /api/link_exit_error endpoint to log Link errors on the server
  • Fix all endpoints NPE-ing on API errors (Retrofit returns errors in the response, not as exceptions)
  • Add PlaidApiException, PlaidApiExceptionMapper, and shared PlaidApiHelper.callPlaid() to centralize error handling. Java needs this pattern because Retrofit doesn't throw on HTTP errors — without it, every endpoint would need its own error-checking boilerplate.
  • Add missing display_message in error construction
  • Fix AssetsResource polling loop (had no-op .equals() checks that never gated retries)
  • Fix CraResource to only retry on PRODUCT_NOT_READY errors instead of all exceptions

Node

  • Add /api/link_exit_error endpoint to log Link errors on the server
  • Fix pollWithRetries retrying on all errors instead of only PRODUCT_NOT_READY
  • Fix pollWithRetries not propagating rejections up the promise chain
  • Fix error middleware returning HTTP 200 for all errors — now returns the actual status code

Go

  • Add /api/link_exit_error endpoint to log Link errors on the server
  • Fix pollWithRetries retrying on all errors instead of only PRODUCT_NOT_READY
  • Fix renderError returning HTTP 200 for all Plaid errors — now returns the actual status code and logs the error

Test plan

  • Start each backend (Python, Ruby, Java, Node, Go), trigger an API error (e.g. invalid API keys) → verify error_code, error_type, and error_message all display correctly in the frontend
  • Verify error responses return proper HTTP status codes (not 200)
  • Ruby: verify server logs show clean formatted errors without stack traces
  • Java: verify display_message renders in the frontend when available
  • Test Link flow with an institution that triggers an exit error → verify the error is surfaced in the UI with contextual tips and logged on the server
  • Test asset report / CRA polling with transient errors → verify retries work and eventually time out cleanly

🤖 Generated with Claude Code

Claude Session: cbe534d1-8262-4037-9c93-a5b9c18dd64a

phoenixy1 and others added 5 commits April 16, 2026 00:27
Link errors were invisible to users — the only way to discover errors
like INVALID_LINK_CUSTOMIZATION was to open the browser network tab.
This was caused by missing error callbacks in the frontend and
inconsistent error formatting across backends.

Frontend:
- Add onExit callback to Link component to surface client-side errors
- Display Link exit errors in the UI via a warning callout
- Handle both wrapped and flat error formats in generateToken

Backends (all languages):
- Centralize Plaid API error handling instead of repeating per-endpoint
  try/catch blocks (Python: @app.errorhandler, Ruby: error block,
  Java: ExceptionMapper + shared callPlaid() helper)
- Python: fix 4 endpoints returning raw error bodies without the
  { error: ... } wrapper the frontend expects
- Java: fix all endpoints NPE-ing on API errors (Retrofit returns
  errors in the response, not as exceptions)
- Node/Ruby: fix pollWithRetries retrying on all errors instead of
  only PRODUCT_NOT_READY

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
App.tsx: Check response.ok before calling response.json() so
non-JSON error responses (e.g. HTML 502) don't throw an unhandled
exception.

CraResource.java: Only retry on PRODUCT_NOT_READY errors instead
of retrying on all PlaidApiExceptions. Also align retry sleep to
1000ms to match other backends.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PRODUCT_NOT_READY is the expected retryable error, but transient 500s
from the API should also be retried rather than immediately failing.
This aligns all five backends on the same retry logic: retry on
PRODUCT_NOT_READY or 5xx, fail fast on everything else.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AssetsResource.java: Replace broken polling loop (had no-op .equals()
checks that never gated retries, and would NPE on error responses)
with PlaidApiHelper.callPlaid() and the same PRODUCT_NOT_READY/5xx
retry logic used everywhere else.

Python/Ruby poll_with_retries: Wrap JSON parsing of error body in
try/catch so a non-JSON 5xx from a proxy doesn't crash the request
instead of retrying.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Python was missing the error_message field entirely — it mapped
error_message into display_message and dropped the original, so the
frontend had nothing to show. Now spreads the full Plaid error body,
matching Node's approach.

Ruby errors were invisible because Sinatra's show_exceptions middleware
replaced our JSON error response with an HTML stack trace page. The
frontend couldn't parse it and fell back to hardcoded "UNKNOWN" /
"API_ERROR" defaults. Disabled show_exceptions and dump_errors, added
a defensive rescue inside the Plaid error handler, and added a generic
catch-all error handler that returns clean JSON.

Java was missing display_message in its error construction, so the
frontend could never show user-friendly error messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@phoenixy1 phoenixy1 changed the title Surface Plaid API errors instead of silently swallowing them Fix error reporting across all quickstart backends Apr 17, 2026
@phoenixy1 phoenixy1 marked this pull request as ready for review April 17, 2026 01:03
@phoenixy1 phoenixy1 changed the title Fix error reporting across all quickstart backends Fix error reporting and handling across all quickstart backends Apr 17, 2026
Frontend: send Link exit errors to backend /api/link_exit_error
endpoint for server-side logging, include institution_name from Link
metadata, and add contextual tips for INVALID_LINK_CUSTOMIZATION and
INSTITUTION_REGISTRATION_REQUIRED errors.

All backends: add /api/link_exit_error endpoint (Go, Node, Java).

Go: fix renderError to log errors and return proper HTTP status codes
instead of always returning 200.

Node: fix error middleware to return proper HTTP status codes and
handle cases where error.response is undefined.

Java: add statusCode to PlaidApiException so the ExceptionMapper can
return the actual HTTP status instead of 200. Register
LinkExitErrorResource.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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