fix: a throw inside handleRpc hangs the dApp's promise forever with no error #280
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
src/background/index.js:868—handleRpc(...).then((response) => sendResponse(response))has no.catch().If anything inside
handleRpcthrows,sendResponseis never called, the content script posts nothing back, and the page'swindow.ethereum.request()promise stays pending forever, with no error and no timeout. The dApp has no way to distinguish it from a slow wallet.Found by the independent review of #278. Pre-existing and untouched by that PR, which is why it was not blocked there — but the reviewer's point stands that it is a worse failure mode than the one that PR fixed: #274 was a rejection arriving without its code, and this is a rejection never arriving at all.
handleRpcis large and does real work — state loads, provider calls, transaction population, approval plumbing — so "it does not throw today" is not a property anyone is maintaining.Implementation requirements
handleRpcmust stillsendResponsewith an error the page can reject on, so the promise settles. Use a code where one is defined for the situation; a generic internal error is acceptable where none is, but the message must be a full sentence per the repo's Language & Labeling rules..then()without.catch()pattern may appear on other message paths.handleRpcthrow and asserts a page-facing rejection is produced. An e2e case would be better if one can be provoked without a contrived hook — say which you did.Definition of done
handleRpcproduces a page-facing rejection rather than a pending promise.catchshape is fixed or explicitly ruled out in the PR body.handleRpcthrow and asserts the page rejects, demonstrated failing first.TODO.mdupdated in the same commit.make checkpasses.Also noted, not a defect today
An error carrying
databut nocodedropsdata, because it falls to the plain-Errorbranch in the provider. Unreachable at present — nothing emitsdata— so it is recorded here rather than filed separately.Plan.
Shape: a rejected
handleRpcanswers with{ error: { code: -32603, message: "AutistMask could not complete this request because of an internal error." } }. -32603 is the JSON-RPC internal error EIP-1474 defines and EIP-1193 defers to for RPC-layer failures; no EIP-1193 4xxx code describes "the wallet broke". No EIP-1193 4xxx code is invented for it, and the cause is not put inmessage— the page gets a stable sentence, the background console gets the throw.Visibility:
.catch()logs the method and the error throughlog.errorfbefore it settles the page, so the failure is on the background console rather than swallowed. Note the e2e harness cannot see it either way —tests/e2e/harness.jsdocuments that Playwright exposes no error event for service workers — so the console line is the evidence, and the unit test is what fails on regression.Siblings: the two async IIFEs in
AUTISTMASK_TX_RESPONSE/AUTISTMASK_SIGN_RESPONSEare the same shape one level down — every statement is inside atry, but a throw from acatchblock escapes an unhandled promise and neither the popup nor the page is answered. Both get a last-resort.catch()that settles the approval throughsettleApproval()(the existing chokepoint, no newdelete/resolve) and answers the popup. The remaining handlers are synchronous and are ruled out in the PR body.Test: a unit test drives a real failure —
chrome.storage.local.getrejecting, whichgetState()awaits unguarded — and asserts the page-facing{ error: { code, message } }reachessendResponse. Demonstrated failing first against the current code. No e2e case: provoking a storage failure in a real browser needs a contrived hook, and the harness cannot observe worker errors anyway.