fix: return clean error messages instead of raw SDK objects#51
fix: return clean error messages instead of raw SDK objects#51dylankilkenny merged 2 commits intomainfrom
Conversation
- Check simResult.error before calling assembleTransaction to prevent massive JSON dumps with _attributes/_switch/_value being exposed - Add parseSimulationError() to extract human-readable messages from Soroban diagnostic events (e.g., "signature has expired (Auth, InvalidInput)") - Add sanitizeReason() to strip provider wrapper text from transaction errors (e.g., "TxInsufficientBalance" instead of nested error chain) - Add comprehensive tests for both error sanitization functions
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis PR improves error handling across the plugin by introducing two new helper functions to extract human-readable messages from RPC and transaction failures. The functions parse diagnostic data and sanitize error strings, with comprehensive unit tests validating edge cases and specific error code extraction. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR improves error handling by preventing raw SDK objects from being exposed to API clients. It introduces two new error sanitization functions to extract human-readable error messages from both Soroban simulation errors and transaction submission failures.
- Adds early
simResult.errorchecking before callingassembleTransaction()to prevent exposing internal SDK objects - Implements
parseSimulationError()to extract meaningful messages from Soroban diagnostic event logs - Implements
sanitizeReason()to strip provider wrapper text from transaction errors
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| test/error-sanitization.test.ts | Comprehensive test suite covering both error sanitization functions with various edge cases |
| src/plugin/simulation.ts | Adds parseSimulationError() function and updates error handling to check simulation errors before assembly |
| src/plugin/submit.ts | Adds sanitizeReason() function and applies it to transaction failure messages before throwing errors |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/plugin/simulation.ts
Outdated
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | ||
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | ||
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | ||
| } | ||
|
|
||
| const stringMatch = error.match(/data:\s*"([^"]+)"/); |
There was a problem hiding this comment.
The regex pattern data:\s*\["([^"]+)" will fail to match data arrays that contain escaped quotes within the message string. For example, if the diagnostic event contains a message like ["error with \"quotes\"", more data], the pattern will stop at the first escaped quote instead of capturing the full message. Consider using a more robust parsing approach or updating the regex to handle escaped characters.
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | |
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | |
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | |
| } | |
| const stringMatch = error.match(/data:\s*"([^"]+)"/); | |
| const arrayMatch = error.match(/data:\s*\["((?:[^"\\]|\\.)*)"/); | |
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | |
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | |
| } | |
| const stringMatch = error.match(/data:\s*"((?:[^"\\]|\\.)*)"/); |
src/plugin/simulation.ts
Outdated
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | ||
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | ||
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | ||
| } | ||
|
|
||
| const stringMatch = error.match(/data:\s*"([^"]+)"/); |
There was a problem hiding this comment.
The regex pattern data:\s*"([^"]+)" will fail to match data strings that contain escaped quotes. For example, if the diagnostic event contains a message like "error with \"quotes\"", the pattern will stop at the first escaped quote instead of capturing the full message. Consider using a more robust parsing approach or updating the regex to handle escaped characters.
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | |
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | |
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | |
| } | |
| const stringMatch = error.match(/data:\s*"([^"]+)"/); | |
| const arrayMatch = error.match(/data:\s*\["((?:[^"\\]|\\.)*)"/); | |
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | |
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | |
| } | |
| const stringMatch = error.match(/data:\s*"((?:[^"\\]|\\.)*)"/); |
| return data !== null && typeof data === 'object' && 'signature' in data && 'signedXdr' in data; | ||
| } | ||
|
|
||
| /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */ |
There was a problem hiding this comment.
The comment says "Strip provider wrapper text, extract last segment" but the function also performs length validation (must be > 2 characters) and checks that the segment doesn't contain "provider". The comment should be updated to reflect all the conditions that determine whether the last segment is returned or the fallback is used.
| /** Strip provider wrapper text, extract last segment (e.g., "TxInsufficientBalance") */ | |
| /** | |
| * Normalize an error reason by stripping provider wrapper text. | |
| * - Split on colon and trim whitespace, then consider the last segment (e.g., "TxInsufficientBalance"). | |
| * - Return the last segment only if it is longer than 2 characters and does not contain "provider" (case-insensitive). | |
| * - Otherwise, return the original reason, truncated to 100 characters with "..." if it is longer than 100 characters. | |
| */ |
| const errorType = firstLine.match(/Error\(([^)]+)\)/)?.[1]; | ||
|
|
||
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | ||
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { |
There was a problem hiding this comment.
The test comment claims "Messages <= 3 chars are ignored" but the implementation checks length > 3 which means messages with exactly 3 characters will also be ignored. If the intent is to ignore messages with 3 or fewer characters, the comment is correct but misleading. If the intent is to accept 3-character messages, the implementation should use length >= 3 instead.
| export function sanitizeReason(reason: string): string { | ||
| const segments = reason.split(/:\s*/); | ||
| const last = segments[segments.length - 1]?.trim(); | ||
| if (last && last.length > 2 && !last.toLowerCase().includes('provider')) { |
There was a problem hiding this comment.
The condition checks last.length > 2 which means messages with exactly 2 characters are filtered out, while the test at line 124 expects empty strings to pass through. The logic is inconsistent - a 1 or 2 character error code like "OK" or "NO" would trigger the fallback path, but an empty string (length 0) would also satisfy !last and trigger the fallback. Consider if the length check threshold of 2 is appropriate for all use cases.
| if (last && last.length > 2 && !last.toLowerCase().includes('provider')) { | |
| if (last && !last.toLowerCase().includes('provider')) { |
| throw pluginError('Simulation RPC failed', { | ||
| code: 'SIMULATION_RPC_FAILURE', | ||
| status: HTTP_STATUS.BAD_GATEWAY, | ||
| details: { message: 'RPC provider error' }, |
There was a problem hiding this comment.
The error message was changed from "Simulation RPC execution failed" to "Simulation RPC failed", and the details object now only contains a generic message "RPC provider error" instead of the specific RPC error details (rpcCode, message, description/data). This removes valuable debugging information that could help diagnose RPC-level failures. Consider preserving at least the message field from the RPC error response while still sanitizing the output appropriately.
| details: { message: 'RPC provider error' }, | |
| details: { | |
| // Preserve the RPC error message for debugging while keeping the top-level error generic. | |
| message: typeof message === 'string' && message.length > 0 ? message : 'RPC provider error', | |
| }, |
There was a problem hiding this comment.
@copilot details for the rpc error is added to console.error above and want to keep this generic as to not expose details to the client
src/plugin/simulation.ts
Outdated
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | ||
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | ||
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | ||
| } | ||
|
|
||
| const stringMatch = error.match(/data:\s*"([^"]+)"/); | ||
| if (stringMatch?.[1] && stringMatch[1].length > 3) { | ||
| return errorType ? `${stringMatch[1]} (${errorType})` : stringMatch[1]; |
There was a problem hiding this comment.
The function performs three regex matches sequentially on potentially large error strings. For better performance, consider using a single regex with capture groups or using String.prototype.indexOf() to locate the relevant sections before applying regex matches. This would be more efficient especially for long error messages with extensive event logs.
| const arrayMatch = error.match(/data:\s*\["([^"]+)"/); | |
| if (arrayMatch?.[1] && arrayMatch[1].length > 3) { | |
| return errorType ? `${arrayMatch[1]} (${errorType})` : arrayMatch[1]; | |
| } | |
| const stringMatch = error.match(/data:\s*"([^"]+)"/); | |
| if (stringMatch?.[1] && stringMatch[1].length > 3) { | |
| return errorType ? `${stringMatch[1]} (${errorType})` : stringMatch[1]; | |
| // Use a single regex with capture groups to extract either array-style or string-style data | |
| const dataMatch = error.match(/data:\s*(?:\["([^"]+)"|"([^"]+)")/); | |
| const extracted = dataMatch?.[1] ?? dataMatch?.[2]; | |
| if (extracted && extracted.length > 3) { | |
| return errorType ? `${extracted} (${errorType})` : extracted; |
| test('filters out segments containing provider', () => { | ||
| const reason = 'Error: provider error'; | ||
| // Last segment contains "provider", should fallback | ||
| expect(sanitizeReason(reason)).toBe('Error: provider error'); | ||
| }); |
There was a problem hiding this comment.
The test expects that when the last segment contains "provider", the entire original reason should be returned (line 107). However, looking at the implementation in submit.ts (lines 143-150), when the last segment contains "provider" OR when the segment is too short, the fallback returns either the truncated reason (if > 100 chars) or the full reason. This test passes only because "Error: provider error" is less than 100 characters. The test should be clearer about what behavior it's actually testing - it seems to be testing the fallback path but doesn't verify truncation isn't applied in this case.
| const rawReason = final.status_reason || 'Transaction failed'; | ||
| console.error(`[channels] Transaction failed: ${rawReason}`); | ||
| const reason = sanitizeReason(rawReason); |
There was a problem hiding this comment.
The variable name rawReason might be misleading since it contains final.status_reason || 'Transaction failed' which already includes a fallback. Consider naming it something like statusReason or originalReason to better reflect that it's the unsanitized reason from the status response.
| const rawReason = final.status_reason || 'Transaction failed'; | |
| console.error(`[channels] Transaction failed: ${rawReason}`); | |
| const reason = sanitizeReason(rawReason); | |
| const statusReason = final.status_reason || 'Transaction failed'; | |
| console.error(`[channels] Transaction failed: ${statusReason}`); | |
| const reason = sanitizeReason(statusReason); |
| test('ignores short data messages', () => { | ||
| // Messages <= 3 chars are ignored (likely not human-readable) | ||
| const error = `HostError: Error(Test, Code) | ||
|
|
||
| Event log: | ||
| 0: [Diagnostic Event] data:"ab"`; | ||
|
|
||
| expect(parseSimulationError(error)).toBe('HostError: Error(Test, Code)'); | ||
| }); |
There was a problem hiding this comment.
The test description says "ignores short data messages" and the comment claims "Messages <= 3 chars are ignored", but the test only verifies that a 2-character message ("ab") is ignored. To properly test the boundary condition, add a test case with a 3-character message to verify it's also ignored, and a 4-character message to verify it's accepted. This would confirm the implementation's length > 3 check is correct.
|
@tirumerla I've opened a new pull request, #52, to work on those changes. Once the pull request is ready, I'll request review from you. |
tirumerla
left a comment
There was a problem hiding this comment.
thanks, lgtm. Please take a look at regex comments from copilot.
| throw pluginError('Simulation failed', { | ||
| code: 'SIMULATION_FAILED', | ||
| status: HTTP_STATUS.BAD_REQUEST, | ||
| details: { | ||
| rpcCode: code, | ||
| message, | ||
| description: description || data, | ||
| }, | ||
| details: { error: parseSimulationError(simResult.error) }, |
There was a problem hiding this comment.
Should we add console.error log here
| throw pluginError('Simulation failed', { | ||
| code: 'SIMULATION_FAILED', |
Signed-off-by: Dylan Kilkenny <dylankilkenny95@gmail.com>
Summary
simResult.errorbefore callingassembleTransaction()to prevent massive JSON dumps with_attributes/_switch/_valuebeing exposed to clientsparseSimulationError()to extract human-readable messages from Soroban diagnostic events (e.g., "signature has expired (Auth, InvalidInput)")sanitizeReason()to strip provider wrapper text from transaction errors (e.g., "TxInsufficientBalance" instead of nested error chain)Test plan
parseSimulationError()andsanitizeReason()Summary by CodeRabbit
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.