fix: answer the page when a background handler throws (closes #280)
All checks were successful
check / check (push) Successful in 46s

handleRpc(...).then(sendResponse) had no .catch(), and sendResponse is the only
thing that settles the dApp's window.ethereum.request() promise. Any throw
inside handleRpc therefore sent nothing back: the content script posted nothing,
and the page's promise stayed pending forever with no error and no timeout,
indistinguishable from a slow wallet. handleRpc does real work -- state loads,
provider calls, transaction population, approval plumbing -- so "it does not
throw today" was not a property anyone was maintaining.

A rejected handleRpc now answers { code: -32603, message }. -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" and none was
invented for it. The cause is not put in the message: the page gets a stable
sentence, the background console gets the method and the throw, so the failure
is visible rather than swallowed.

The two async IIFEs behind AUTISTMASK_TX_RESPONSE and AUTISTMASK_SIGN_RESPONSE
are the same shape one level down. Every statement is inside a try, but a throw
from one of the catch blocks escapes as an unhandled rejection and neither the
popup nor the page is answered. Each gets a last-resort .catch() that settles
the approval through settleApproval() -- the existing chokepoint, with no new
delete or resolve -- and answers the popup. The transaction one reports the
broadcast stage, because it cannot tell whether the transaction reached the
network and that is the wording that does not invite a second send. Every other
message handler on the path is synchronous and cannot leave a promise pending.

Each of the three is driven by a real failure rather than a hook in the handler:
a rejecting extension-storage read, which getState() awaits unguarded, and a
failure classifier that throws while classifying a genuine verification failure.
All three were demonstrated failing against the unfixed code, the RPC one with
sendResponse at zero calls, which is precisely the page-side hang.
This commit is contained in:
2026-08-14 04:06:17 +00:00
parent 9dcd875dd4
commit 9665ac448e
3 changed files with 265 additions and 6 deletions

19
TODO.md
View File

@@ -45,6 +45,25 @@ undefined identifiers, which is how
# Completed Steps
- 2026-08-14: A background message handler that throws now rejects the page
instead of hanging it. `handleRpc(...).then(sendResponse)` had no `.catch()`,
and `sendResponse` is the only thing that settles the dApp's
`window.ethereum.request()` promise — so any throw inside `handleRpc` left
that promise pending forever, with no error and no timeout, indistinguishable
from a slow wallet. It now answers `{ code: -32603, message }` (the JSON-RPC
internal error EIP-1474 defines and EIP-1193 defers to; no EIP-1193 4xxx code
describes "the wallet broke" and none was invented) and logs the method and
the throw to the background console rather than swallowing them. The two async
IIFEs behind `AUTISTMASK_TX_RESPONSE` and `AUTISTMASK_SIGN_RESPONSE` were the
same shape one level down — every statement inside a `try`, but a throw out of
a `catch` block escaping unhandled — and each got a last-resort `.catch()`
settling the approval through `settleApproval()` and answering the popup; the
transaction one reports the broadcast stage, because it cannot tell whether
the transaction reached the network. Every other handler on the path is
synchronous. All three are driven by real failures — a rejecting storage read,
and a failure classifier that throws while classifying a genuine verification
failure — and were demonstrated failing first, the RPC one with `sendResponse`
at zero calls ([#280](https://git.eeqj.de/sneak/AutistMask/issues/280)).
- 2026-08-12: EIP-1193 error codes now reach the page. `src/content/inpage.js`
rebuilt every failure as `new Error(error.message)`, so the code the
background produced and the content script relayed intact was dropped in the