fix: carry EIP-1193 error codes through to the page (closes #274)
All checks were successful
check / check (push) Successful in 28s

The provider rebuilt every rejection as a bare Error carrying only a message,
so a dApp checking err.code === 4001 saw undefined and could not tell a user's
deliberate refusal from a failure. Well-behaved sites therefore showed an error
or retried instead of accepting the refusal. The code was produced correctly
and did cross the extension boundary; it was lost in the last hop.

Rejections now reach the page as a ProviderRpcError carrying code, and data
where present. The code is passed through verbatim rather than matched against
a whitelist, so a code added upstream later needs no change here. An error that
genuinely has no code stays a plain Error with no code property at all, rather
than advertising code: undefined -- 'code' in err is what a careful dApp asks.

Messages are unchanged for every path, verified byte-for-byte against the
previous provider across every background error shape.

The end-to-end assertion that printed the observed code now requires it.
This commit was merged in pull request #278.
This commit is contained in:
2026-08-12 13:47:33 +02:00
parent c755a5e944
commit 9dcd875dd4
5 changed files with 394 additions and 18 deletions

View File

@@ -86,9 +86,11 @@ const DAPP_URL = DAPP_ORIGIN + "/";
// never drive the popup that has to settle it; start() files the promise
// under a key and settle() collects it once the prompt has been dealt with.
//
// The rejection branch records `code` as it arrives. EIP-1193 says a user
// rejection is a ProviderRpcError carrying code 4001; what the page can
// actually see is recorded here rather than assumed, and asserted in run.js.
// The rejection branch records the whole observable shape of the error as it
// arrives — name, message, and whether a `code` is present at all as distinct
// from its value. EIP-1193 says a user rejection is a ProviderRpcError
// carrying code 4001; what the page can actually see is recorded here rather
// than assumed, and asserted in run.js.
//
// The message log is the page's half of the boundary observation: every
// AUTISTMASK_* message that crosses between this page and the content
@@ -120,6 +122,7 @@ const DAPP_HTML = [
" return {",
" settled: 'rejected',",
" message: String((error && error.message) || error),",
" name: error ? error.name : undefined,",
" hasCode: !!error && 'code' in Object(error),",
" code: error ? error.code : undefined,",
" };",

View File

@@ -1591,15 +1591,14 @@ async function lastResponseError(page) {
}
// A rejected prompt, asserted at both ends: the page's promise rejected
// rather than hanging or resolving, and the response that crossed the
// boundary carried EIP-1193 code 4001.
// rather than hanging or resolving, and EIP-1193 code 4001 is present both
// on the wire and on the Error the calling page catches.
//
// The code is asserted on the wire because that is the only place it
// survives. src/content/inpage.js rebuilds the rejection as `new
// Error(error.message)`, so the Error the calling page catches carries the
// message and no code. That is reported rather than asserted either way —
// locking in the current behaviour would make the gap permanent, and
// asserting the code on the Error would fail today.
// Both ends matter because they used to disagree. The code crossed the
// boundary correctly and src/content/inpage.js then threw it away, rebuilding
// every rejection as `new Error(error.message)` so a dApp branching on
// `err.code === 4001` saw undefined and could not tell a refusal from a
// failure (#274). Asserting only the wire would leave that gap invisible.
async function assertUserRejection(page, key, label) {
const outcome = await settleRequest(page, key);
assert(
@@ -1621,15 +1620,32 @@ async function assertUserRejection(page, key, label) {
" did not carry EIP-1193 code 4001 across the boundary: " +
JSON.stringify(error),
);
assert(
outcome.hasCode,
label +
" reached the page as an error with no code property at all, so a " +
"dApp cannot tell the user's refusal from a failure: " +
JSON.stringify(outcome),
);
assert(
outcome.code === 4001,
label +
" reached the page with code " +
JSON.stringify(outcome.code) +
" rather than EIP-1193 4001",
);
assert(
outcome.name === "ProviderRpcError",
label +
" reached the page as " +
JSON.stringify(outcome.name) +
" rather than an EIP-1193 ProviderRpcError",
);
console.log(
"# " +
label +
": boundary code=" +
error.code +
" page Error.code=" +
JSON.stringify(outcome.code) +
" page Error carries a code=" +
outcome.hasCode,
": code 4001 on the wire and on the page's " +
outcome.name,
);
return outcome;
}