fix: EIP-1193 error codes never reach the page — a dApp cannot detect a user rejection #274

Closed
opened 2026-08-12 13:05:38 +02:00 by clawbot · 2 comments
Collaborator

src/content/inpage.js rebuilds every rejection as p.reject(new Error(error.message || "Request failed")), dropping code. A dApp checking err.code === 4001 — the standard way to distinguish "the user said no" from "something broke" — sees undefined.

The code is produced correctly and does cross the messaging boundary; the e2e harness added by #273 asserts it is present there. It is lost in the last hop, in the provider the page actually talks to.

RULES.md requires the code. EIP-1193 defines 4001 as the user-rejection code and dApps branch on it: without it, a wallet that the user deliberately declined is indistinguishable from a wallet that failed, and well-behaved sites will show an error or retry instead of accepting the refusal.

Found by the e2e work on #183, which prints the observed code every run and deliberately does not assert the current behaviour either way, so this issue's fix can flip that assertion on.

Implementation requirements

  • Reject with an error object carrying code — and data where present — taken from the boundary response, not a bare Error with only a message.
  • Cover every request path the provider exposes, not just eth_requestAccounts.
  • Check what a non-rejection error should carry: EIP-1193 defines other codes (4100 unauthorized, 4200 unsupported method, 4900/4901 disconnected) and some are already produced upstream. Do not special-case 4001 alone.
  • Add a unit test on the provider itself, and flip the existing e2e probe from printing the code to REQUIRING it.

Definition of done

  • A user rejection reaches the page as an error with code === 4001.
  • Other EIP-1193 codes produced upstream reach the page intact.
  • message is unchanged for every case.
  • The e2e assertion in tests/e2e/run.js requires the code rather than printing it, and fails without the fix.
  • TODO.md updated in the same commit.
  • make check passes.
`src/content/inpage.js` rebuilds every rejection as `p.reject(new Error(error.message || "Request failed"))`, dropping `code`. A dApp checking `err.code === 4001` — the standard way to distinguish "the user said no" from "something broke" — sees `undefined`. The code is produced correctly and does cross the messaging boundary; the e2e harness added by https://git.eeqj.de/sneak/AutistMask/pulls/273 asserts it is present there. It is lost in the last hop, in the provider the page actually talks to. `RULES.md` requires the code. EIP-1193 defines `4001` as the user-rejection code and dApps branch on it: without it, a wallet that the user deliberately declined is indistinguishable from a wallet that failed, and well-behaved sites will show an error or retry instead of accepting the refusal. Found by the e2e work on https://git.eeqj.de/sneak/AutistMask/issues/183, which prints the observed code every run and deliberately does not assert the current behaviour either way, so this issue's fix can flip that assertion on. ## Implementation requirements - Reject with an error object carrying `code` — and `data` where present — taken from the boundary response, not a bare `Error` with only a message. - Cover every request path the provider exposes, not just `eth_requestAccounts`. - Check what a non-rejection error should carry: EIP-1193 defines other codes (4100 unauthorized, 4200 unsupported method, 4900/4901 disconnected) and some are already produced upstream. Do not special-case 4001 alone. - Add a unit test on the provider itself, and flip the existing e2e probe from printing the code to REQUIRING it. ## Definition of done - [ ] A user rejection reaches the page as an error with `code === 4001`. - [ ] Other EIP-1193 codes produced upstream reach the page intact. - [ ] `message` is unchanged for every case. - [ ] The e2e assertion in `tests/e2e/run.js` requires the code rather than printing it, and fails without the fix. - [ ] `TODO.md` updated in the same commit. - [ ] `make check` passes.
clawbot added this to the 1.0.0 milestone 2026-08-12 13:05:58 +02:00
Author
Collaborator

Plan, on branch fix/issue-274-eip1193-error-codes (base next):

Shape: an EIP-1193 ProviderRpcError extends Error defined inside the inpage.js IIFE, with name, code and optional data. A class rather than properties bolted onto an Error because the object crosses no boundary after construction — it is built in the page's own realm and handed straight to the caller's catch — so the prototype survives and name is a stable thing for a dApp to see.

Single choke point: every entry point (request, enable, send, sendAsync) funnels through the one AUTISTMASK_RESPONSE listener, so one toPageError() there covers all of them.

Pass-through, not a whitelist: whatever code arrived is carried verbatim rather than matched against a list of known values. The background emits 4001, 4100 and 4902 on the RPC path today; a code added later must reach the page without editing the provider. An error the background sent with no code stays a plain Error with no code property at all — a ProviderRpcError whose code is undefined would advertise a conformance it does not have. message is untouched in every case.

Nothing in src/background/index.js or src/content/index.js changes.

Tests: a new tests/inpageErrors.test.js loading the real inpage.js against a stub window, plus flipping the four e2e rejection probes in tests/e2e/run.js from printing the code to requiring it.

Plan, on branch `fix/issue-274-eip1193-error-codes` (base `next`): Shape: an EIP-1193 `ProviderRpcError extends Error` defined inside the `inpage.js` IIFE, with `name`, `code` and optional `data`. A class rather than properties bolted onto an `Error` because the object crosses no boundary after construction — it is built in the page's own realm and handed straight to the caller's `catch` — so the prototype survives and `name` is a stable thing for a dApp to see. Single choke point: every entry point (`request`, `enable`, `send`, `sendAsync`) funnels through the one `AUTISTMASK_RESPONSE` listener, so one `toPageError()` there covers all of them. Pass-through, not a whitelist: whatever `code` arrived is carried verbatim rather than matched against a list of known values. The background emits 4001, 4100 and 4902 on the RPC path today; a code added later must reach the page without editing the provider. An error the background sent with no code stays a plain `Error` with no `code` property at all — a `ProviderRpcError` whose `code` is `undefined` would advertise a conformance it does not have. `message` is untouched in every case. Nothing in `src/background/index.js` or `src/content/index.js` changes. Tests: a new `tests/inpageErrors.test.js` loading the real `inpage.js` against a stub window, plus flipping the four e2e rejection probes in `tests/e2e/run.js` from printing the code to requiring it.
Author
Collaborator

Built and pushed as #278. Shape rationale and the full before/after output are in the PR body; this is the definition of done against what was verified.

  • A user rejection reaches the page as code === 4001 — asserted on the page's own Error, not just on the wire, for all four flows (eth_requestAccounts, personal_sign, eth_signTypedData_v4, eth_sendTransaction) in make test-e2e, and in tests/inpageErrors.test.js.
  • Other codes reach the page intact — the background emits 4001, 4100 and 4902 on the RPC path; all three are covered. The provider passes any code through verbatim rather than matching a list, tested with a 4900 the codebase never produces, so a code added later needs no change here. 4200/4901 are produced nowhere today; nothing was invented for them.
  • message unchanged — the fallback stays "Request failed", and the e2e records "message":"User rejected the request." byte-identically before and after. An error the background sent with no code stays a plain Error with no code property at all, asserted both ways.
  • The e2e probe requires the code and fails without the fixmake test-e2e exits 1 with 33/37 and four not ok lines reading reached the page as an error with no code property at all, exit 0 with 37/37 after. Full captured output in the PR.
  • Unit test on the providertests/inpageErrors.test.js, 17 cases, loading the real inpage.js; 12 of them fail with the provider change stashed.
  • Every request pathrequest, enable, send(method, params), send({method, params}) and sendAsync, each driven separately.
  • TODO.md updated in the same commit. make check exit 0: 681 tests, 28 suites, test-verify-build 18/18, prettier clean. Both gates re-run after rebasing onto next at c755a5e.

Confined to src/content/inpage.js and the tests — nothing the background produces changes. One adjacent gap found and filed rather than fixed here: handleRpc() reports an unsupported method with no code where EIP-1193 defines 4200 (#279).

Built and pushed as https://git.eeqj.de/sneak/AutistMask/pulls/278. Shape rationale and the full before/after output are in the PR body; this is the definition of done against what was verified. - **A user rejection reaches the page as `code === 4001`** — asserted on the page's own Error, not just on the wire, for all four flows (`eth_requestAccounts`, `personal_sign`, `eth_signTypedData_v4`, `eth_sendTransaction`) in `make test-e2e`, and in `tests/inpageErrors.test.js`. - **Other codes reach the page intact** — the background emits `4001`, `4100` and `4902` on the RPC path; all three are covered. The provider passes any code through verbatim rather than matching a list, tested with a `4900` the codebase never produces, so a code added later needs no change here. `4200`/`4901` are produced nowhere today; nothing was invented for them. - **`message` unchanged** — the fallback stays `"Request failed"`, and the e2e records `"message":"User rejected the request."` byte-identically before and after. An error the background sent with no code stays a plain `Error` with no `code` property at all, asserted both ways. - **The e2e probe requires the code and fails without the fix** — `make test-e2e` exits 1 with 33/37 and four `not ok` lines reading `reached the page as an error with no code property at all`, exit 0 with 37/37 after. Full captured output in the PR. - **Unit test on the provider** — `tests/inpageErrors.test.js`, 17 cases, loading the real `inpage.js`; 12 of them fail with the provider change stashed. - **Every request path** — `request`, `enable`, `send(method, params)`, `send({method, params})` and `sendAsync`, each driven separately. - **`TODO.md`** updated in the same commit. **`make check`** exit 0: 681 tests, 28 suites, `test-verify-build` 18/18, prettier clean. Both gates re-run after rebasing onto `next` at `c755a5e`. Confined to `src/content/inpage.js` and the tests — nothing the background produces changes. One adjacent gap found and filed rather than fixed here: `handleRpc()` reports an unsupported method with no code where EIP-1193 defines `4200` (https://git.eeqj.de/sneak/AutistMask/issues/279).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#274