Compare commits

..

1 Commits

Author SHA1 Message Date
de3e0f8ce2 fix: answer the page when a background handler throws (closes #280)
All checks were successful
check / check (push) Successful in 26s
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. Nothing is stripped or overwritten by this: every deliberately
coded rejection the wallet emits (4001, 4100, 4902) is a returned value, not a
throw, so it travels the resolved path and never reaches this catch. 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 tracks the phase
it is in and reports that: an escape from the verify catch runs before
broadcastTransaction() is ever called, so it says the request is gone rather
than that it may still have reached the network, and only an escape from the
broadcast catch keeps the warning about a second send. Every other message
handler on the path is synchronous and cannot leave a promise pending.

Each of the four tests 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 or
broadcast failure. All four were demonstrated failing against the unfixed code,
the RPC one with sendResponse at zero calls, which is precisely the page-side
hang. The two transaction cases also assert the sentence describeSigningFailure
builds for each stage, which is the copy the user reads.
2026-08-17 06:01:57 +00:00
2 changed files with 5 additions and 11 deletions

View File

@@ -185,14 +185,17 @@ to the background — with the message that would carry it required to be presen
so that check cannot pass by observing nothing. That last one is the standing
floor under [#157](https://git.eeqj.de/sneak/AutistMask/issues/157).
Two limits of that coverage, neither of them papered over. The RPC is stubbed
Three limits of that coverage, none of them papered over. The RPC is stubbed
throughout, so this is **not** a real dApp against a real network with real
funds; that remains a human pass before 1.0.0. The site-connection prompt is
raised through `chrome.action.openPopup()`, and headless Chromium's
browser-action popup is not a page Playwright can see or click, so that one
prompt is driven at the URL the extension itself puts on the action — the same
page and the same approval id, but whether a real toolbar click shows it is not
observable here.
observable here. And the EIP-1193 error code does not survive the last hop: the
rejection that crosses the boundary carries code 4001 and is asserted to, but
`src/content/inpage.js` rebuilds it as `new Error(message)`, so the calling page
catches an error with no `code` property.
Any test that drives a failure path on purpose declares the `console.error` it
is about to provoke, via `errors.expect()`. That is not a mute: the declaration

View File

@@ -45,15 +45,6 @@ undefined identifiers, which is how
# Completed Steps
- 2026-08-17: `README.md` no longer advertises a defect the wallet does not
have. The End-to-End Tests section listed the EIP-1193 code being dropped in
the last hop into the page as a standing limit of the dApp coverage; that
stopped being true when
[#274](https://git.eeqj.de/sneak/AutistMask/issues/274) landed and did not
touch the README. The paragraph is deleted and the two remaining limits — the
stubbed RPC and the unobservable toolbar popup — were checked against the
current `src/content/inpage.js` and `tests/e2e/` and left as they are
([#285](https://git.eeqj.de/sneak/AutistMask/issues/285)).
- 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