Fix error reporting and handling across all quickstart backends#565
Open
Fix error reporting and handling across all quickstart backends#565
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
onExitcallback), 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_errorendpoint in each backend.@app.errorhandlerin Python, Sinatraerrorblocks in Ruby,ExceptionMapper+ sharedPlaidApiHelper.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 ownif (!response.isSuccessful())boilerplate. The other backends' HTTP clients throw on errors, so a single global handler is sufficient.PRODUCT_NOT_READY, and none retried on transient 5xx errors. Java'sAssetsResourcehad a no-op polling loop. Node'spollWithRetriesnever propagated rejections. All backends now share the same retry policy: retry onPRODUCT_NOT_READYor 5xx, fail fast on everything else.response.json()threw an unhandled exception. The frontend now checksresponse.okand gracefully handles parse failures.Frontend
onExitcallback to Link component to capture and surface Link errors/api/link_exit_error) so they appear in server logs as well as the UI{error: ...}) and flat error formats ingenerateTokenresponse.okbefore callingresponse.json()so non-JSON error responses (e.g. HTML 502) don't throw unhandled exceptionsPython
/api/link_exit_errorendpoint to log Link errors on the serverformat_error— was mappingerror_messageintodisplay_messageand omittingerror_messageentirely, so the frontend had nothing to show. Now spreads the full Plaid error body, matching Node's approach.@app.errorhandlerinstead of per-endpoint try/catch{ error: ... }wrapper the frontend expectspoll_with_retriesJSON parsing so a non-JSON 5xx doesn't crash instead of retryingRuby
/api/link_exit_errorendpoint to log Link errors on the servershow_exceptionsmiddleware replacing JSON error responses with HTML stack trace pages — the frontend couldn't parse the HTML and fell back to hardcoded "UNKNOWN" / "API_ERROR" defaultsdump_errorsto stop logging full stack traces for handled errorserrorblock instead of per-endpoint rescuepoll_with_retriesJSON parsingJava
/api/link_exit_errorendpoint to log Link errors on the serverPlaidApiException,PlaidApiExceptionMapper, and sharedPlaidApiHelper.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.display_messagein error constructionAssetsResourcepolling loop (had no-op.equals()checks that never gated retries)CraResourceto only retry onPRODUCT_NOT_READYerrors instead of all exceptionsNode
/api/link_exit_errorendpoint to log Link errors on the serverpollWithRetriesretrying on all errors instead of onlyPRODUCT_NOT_READYpollWithRetriesnot propagating rejections up the promise chainGo
/api/link_exit_errorendpoint to log Link errors on the serverpollWithRetriesretrying on all errors instead of onlyPRODUCT_NOT_READYrenderErrorreturning HTTP 200 for all Plaid errors — now returns the actual status code and logs the errorTest plan
🤖 Generated with Claude Code
Claude Session: cbe534d1-8262-4037-9c93-a5b9c18dd64a