fix: answer the page when a background handler throws (closes #280)
All checks were successful
check / check (push) Successful in 28s
All checks were successful
check / check (push) Successful in 28s
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.
This commit is contained in:
@@ -166,6 +166,16 @@ function approvedNonce(approvedTx) {
|
||||
}
|
||||
}
|
||||
|
||||
// What the page is told when a request failed in a way the wallet has no
|
||||
// specific answer for. -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 one is not invented here. The cause is
|
||||
// logged rather than put in the message: the page gets a stable sentence, the
|
||||
// background console gets the throw.
|
||||
const INTERNAL_ERROR_CODE = -32603;
|
||||
const INTERNAL_ERROR_MESSAGE =
|
||||
"AutistMask could not complete this request because of an internal error.";
|
||||
|
||||
async function getState() {
|
||||
const result = await storageApi.get("autistmask");
|
||||
return (
|
||||
@@ -1079,9 +1089,26 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// keep fallback
|
||||
}
|
||||
}
|
||||
handleRpc(msg.method, msg.params, trustedOrigin).then((response) => {
|
||||
sendResponse(response);
|
||||
});
|
||||
handleRpc(msg.method, msg.params, trustedOrigin)
|
||||
.then((response) => {
|
||||
sendResponse(response);
|
||||
})
|
||||
.catch((err) => {
|
||||
// Without this the page's window.ethereum.request() promise
|
||||
// stays pending forever: no response is sent, the content
|
||||
// script posts nothing back, and the dApp cannot tell the
|
||||
// failure from a slow wallet. handleRpc does real work —
|
||||
// state loads, provider calls, transaction population — so
|
||||
// "it does not throw today" is not a property anyone is
|
||||
// maintaining.
|
||||
log.errorf("RPC request failed:", msg.method, err);
|
||||
sendResponse({
|
||||
error: {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: INTERNAL_ERROR_MESSAGE,
|
||||
},
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1188,6 +1215,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Which phase the last-resort .catch() below reports. Everything up to
|
||||
// the broadcastTransaction() call provably never reached the network,
|
||||
// so an escape from there must not tell the user it might have.
|
||||
let lastResortStage = TX_STAGE_VERIFY;
|
||||
(async () => {
|
||||
// The chain this attempt is on, read once. Verification below
|
||||
// refuses an artifact signed for any other chain, and the nonce
|
||||
@@ -1271,6 +1302,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
|
||||
try {
|
||||
const provider = getProvider(state.rpcUrl);
|
||||
lastResortStage = TX_STAGE_BROADCAST;
|
||||
const tx = await provider.broadcastTransaction(msg.rawSignedTx);
|
||||
if (nonce !== null) spent.add(nonce);
|
||||
settleApproval(
|
||||
@@ -1302,7 +1334,28 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
stage: outcome.stage,
|
||||
});
|
||||
}
|
||||
})();
|
||||
})().catch((e) => {
|
||||
// Every statement above 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 ever answered. Settle both, through
|
||||
// the same chokepoint as every other retirement.
|
||||
log.errorf("transaction approval response failed:", e);
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{
|
||||
error: {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: INTERNAL_ERROR_MESSAGE,
|
||||
},
|
||||
},
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
sendResponse({
|
||||
error: INTERNAL_ERROR_MESSAGE,
|
||||
retryable: false,
|
||||
stage: lastResortStage,
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1386,7 +1439,25 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
}
|
||||
sendResponse({ error: errMsg, retryable });
|
||||
}
|
||||
})();
|
||||
})().catch((e) => {
|
||||
// Same shape as the transaction path: a throw out of the catch
|
||||
// block above would leave the popup and the page both waiting.
|
||||
log.errorf("sign approval response failed:", e);
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{
|
||||
error: {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: INTERNAL_ERROR_MESSAGE,
|
||||
},
|
||||
},
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
sendResponse({
|
||||
error: INTERNAL_ERROR_MESSAGE,
|
||||
retryable: false,
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user