fix: carry EIP-1193 error codes through to the page (closes #274) #278
Reference in New Issue
Block a user
Delete Branch "fix/issue-274-eip1193-error-codes"
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?
Closes #274.
src/content/inpage.jsrebuilt every failure asnew Error(error.message || "Request failed"), droppingcode. The code is produced correctly by the background and relayed intact by the content script — it was lost in the last hop, in the provider the page actually talks to. A dApp checkingerr.code === 4001sawundefined, so a wallet the user deliberately declined was indistinguishable from a wallet that broke, and a well-behaved site shows an error or retries instead of accepting the refusal.RULES.mdrequires the code.The error shape, and why
EIP-1193 specifies a
ProviderRpcErrorwithcode,messageand optionaldata. This defines the class rather than bolting properties onto a bareError:The object crosses no boundary after this point — it is constructed in the page's own realm and handed straight to the caller's
catch— so the prototype survives,instanceofworks for anything holding a reference to the class, anderror.nameis a stable thing for a dApp to branch on. A property bag on anErrorwould give the samecodewith none of that, for no saving.Two deliberate limits on the shape:
codearrived is carried verbatim rather than matched against a list of known values. A code the provider has never heard of is still the truth about what happened, and a code added to the background later must reach the page without editing this file. Covered by a test that feeds it 4900, which nothing emits today.Error. Several background paths report{message}with no code. Those keep exactly today's behaviour, with nocodeproperty at all — notcode: undefined. AProviderRpcErrorwhosecodeis undefined would advertise a conformance it does not have, and'code' in erris exactly what a careful dApp asks. Asserted both ways.messageis untouched in every case, including the"Request failed"fallback for an error that arrives without one. Nothing the background produces changes.Codes the background emits, and which now reach the page
Read out of
src/background/index.js; every one of them now arrives on the page'sError, because the provider passes them through rather than enumerating them.40014100personal_sign/eth_sign/eth_signTypedData_v4from a site that is not connected, or naming an address that is not the active one (6 sites)4902wallet_switchEthereumChain/wallet_addEthereumChainfor a chain that is not Mainnet or Sepolia (2 sites)"No accounts available","Unsupported method: …", a failed proxy RPC, a failed tx population, an address that changed mid-preparationError, same message4200,4900and4901are named by EIP-1193 but are not produced anywhere in this codebase today, so nothing is invented for them; the pass-through means they need no change here if they ever are. The code-less row is untouched on purpose — this unit is confined to what the provider surfaces, not to what the background produces. That"Unsupported method"arguably wants4200is filed separately as #279.Every request path
request,enable,send(method, params),send({method, params})andsendAsyncall funnel through the singleAUTISTMASK_RESPONSElistener, so one conversion point covers all of them.tests/inpageErrors.test.jsdrives each entry point separately and asserts the code on each, so the claim is tested rather than argued.Tests
tests/inpageErrors.test.js(new, 17 cases) loads the realsrc/content/inpage.js— a bare IIFE, not a module — by compiling it with its globals as function parameters, so nothing leaks between tests and the source compiles in the test's own realm, which is what makesexpect(err).toBeInstanceOf(Error)meaningful. There is no jsdom in this repo; this follows the hand-rolled stub convention oftests/txStatus.test.js.Not vacuous — with the
inpage.jschange stashed and the test file unchanged, 12 of the 17 fail:The five that pass unchanged are the ones asserting behaviour this deliberately preserves: the untouched messages, and the absence of
code/datawhere the boundary sent none. That is the point of having them.The e2e probe, flipped
#273 landed four rejection cases that printed
page Error.code=undefined page Error carries a code=falserather than asserting, so this unit could flip them on.assertUserRejection()intests/e2e/run.jsnow requires the code on the page'sErroras well as on the wire, and requires theProviderRpcErrorname; the fixture intests/e2e/network.jsrecordsnamealongside thecodeandhasCodeit already captured.Failing before the provider change, with the flipped assertion in place —
make test-e2e, exit 1:Note the recorded
"message":"User rejected the request."in the failing output above and in the passing run below: byte-identical, which is themessage-unchanged claim shown rather than asserted in prose.Passing after, on the rebased branch —
make test-e2e, exit 0:Verification
Both re-run after rebasing onto
nextatc755a5e.make check— exit 0. 681 tests in 28 suites,test-verify-build: 18 case(s) passed, prettier clean.make test-e2e— exit 0, 37/37, in the pinned Playwright container.Scope
src/content/inpage.jsand the tests only.src/content/index.js,src/background/index.jsandsrc/popup/views/approval.jsare untouched, so this does not collide with the work on #153. Thewindow.close()accommodation the harness makes for #275 is left exactly as it was.PASS — independently verified:
make check28 suites / 681 tests /test-verify-build18 cases / prettier clean (exit 0),make test-e2e37/37 (exit 0), reverting the one production line reproduces 12/17 unit failures and e2e 33/37 on cases 29/32/34/36, CI green on9317d43, fast-forwardable ontonextatc755a5e, scope confined tosrc/content/inpage.jsplus tests.Anomalies and disclosures, none of them blocking:
src/background/index.jscan produce, plus hostile shapes (codeas a string,null,0, negative,MAX_SAFE_INTEGER, an object, an array;messageabsent, empty,null, a number;erroras a bare string, number, boolean, array). All 27 messages identical. This was a standalone node script against the shipped file, not a jest run.4001at 6 sites (319, 364, 701, 843, 939, 1068),4100at 6,4902at 2, and 5 distinct code-less messages across 8 return sites. The PR body says "7 sites" for4001and "5 sites" for the code-less class — the first is one over, the second counts message classes rather than return sites. Prose only; the pass-through carries whatever arrives, so the miscount changes nothing.error instanceof Error, the prototype chain, andtypeof error.stack; all four rejected flows returnedisError:true,ProviderRpcError.prototype -> Error.prototype,stacka string, with"message":"User rejected the request."byte-identical to the pre-fix run. Instrumentation reverted, tree clean at9317d43.codepasses through verbatim anderr.code === 4001is simplyfalse— nothing throws, nothing coerces.datawithoutcodeis dropped, since such an error falls to the plain-Errorbranch. Unreachable today: no background path emitsdataat all. Noted, not a defect.src/background/index.js:868callshandleRpc(...).then(...)with no.catch(), so a throw inside it never callssendResponseand the page's promise hangs. Untouched by this PR.