fix: stop calling end in createDupeReqFilterMiddleware.ts, so that dapps get correct response to 'seenRequests'
Description
This PR fixes the following problem:
- Dapp sends a request to MetaMask shortly after the service worker starts up, and before the inpage provider receives the
METAMASK_EXTENSION_CONNECT_CAN_RETRYmessage from the content-script (which happens after the metamask controller initializes and notifies all connections of achainChangedevent) - The request hits the middleware created by
createDupeReqFilterMiddleware.tsand its id is added toseenRequestIds - Before the necessary controller responds to the request, the inpage provider now receives the
METAMASK_EXTENSION_CONNECT_CAN_RETRYmessage from the content-script - The provider now retries the request. This happens without the dapp doing anything; this is part of our MV3 retry logic to ensure requests don't get lost under service worker stoppage / start-up conditions. (The provider does this via the imported
createStreamMiddlewarehttps://github.com/MetaMask/json-rpc-middleware-stream/blob/main/src/createStreamMiddleware.ts#L128-L130) - The new retry of the request hits the middleware created by
createDupeReqFilterMiddleware. (The original request still has not been responded to.) The new retry of the request has the same id as the original, so the middleware hits the} else if (!seenRequestIds.add(req.id)) {condition and callsend() - The original request, which was being awaited by the dapp, now is resolved but without a meaningful response (as the middleware just did an
end()call without the request being handled in any way)
This problem was discovered by some e2e tests which became very flaky under MV3. Tests that involved the Simple Snap Keyring dapp would often send a wallet_requestSnaps request, and then not get the necessary response, due to the issue described above.
The solution to the problem presented in this PR is just to not call end when seeing a duplicate request, because the original request should be responded to eventually.
Related issues
Part of the resolution to https://github.com/MetaMask/metamask-extension/issues/21496
Manual testing steps
The bug is hard to repro manually. You could, perhaps, locally modify the createStreamMiddleware code to call sendToStream a second time for the same request after a short (e.g. 10 millisecond) timeout (perhaps here: https://github.com/MetaMask/json-rpc-middleware-stream/blob/main/src/createStreamMiddleware.ts#L50-L60). On develop that should result in the original request receiving an empty response but on this branch the original request should receive a successful response.
This branch will also contribute to MV3 e2e tests passing, but that will require some other PRs as well.
Pre-merge author checklist
- [ ] I’ve followed MetaMask Coding Standards.
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using JSDoc format if applicable
- [ ] I’ve applied the right labels on the PR (see labeling guidelines). Not required for external contributors.
Pre-merge reviewer checklist
- [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.
Builds ready [4a67825]
- builds: chrome, firefox
- builds (beta): chrome
- builds (flask): chrome, firefox
- builds (MMI): chrome, firefox
- builds (test): chrome, firefox
- builds (test-flask): chrome, firefox
- build viz: Build System
- mv3: Background Module Init Stats
- mv3: UI Init Stats
- mv3: Module Load Stats
- mv3: Bundle Size Stats
- mv2: E2e Actions Stats
- code coverage: Report
- storybook: Storybook
- typescript migration: Dashboard
- all artifacts
Page Load Metrics (1375 ± 632 ms)
| Platform | Page | Metric | Min (ms) | Max (ms) | Average (ms) | StandardDeviation (ms) | MarginOfError (ms) |
|---|---|---|---|---|---|---|---|
| Chrome | Home | firstPaint | 68 | 208 | 112 | 33 | 16 |
| domContentLoaded | 10 | 83 | 20 | 16 | 8 | ||
| load | 55 | 3437 | 1375 | 1316 | 632 | ||
| domInteractive | 10 | 83 | 20 | 16 | 8 |
Bundle size diffs [🚀 Bundle size reduced!]
- background: -15 Bytes (-0.00%)
- ui: 0 Bytes (0.00%)
- common: 0 Bytes (0.00%)
Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 65.67%. Comparing base (
31fc52f) to head (44ac712). Report is 25 commits behind head on develop.
Additional details and impacted files
@@ Coverage Diff @@
## develop #24672 +/- ##
===========================================
- Coverage 65.68% 65.67% -0.01%
===========================================
Files 1360 1360
Lines 54103 54113 +10
Branches 14064 14060 -4
===========================================
+ Hits 35533 35536 +3
- Misses 18570 18577 +7
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
Perhaps it would be simpler to move this "ignore duplicate requests" step to before we pass the request into the JSON-RPC engine. Then we'd have no hung promises, and we wouldn't need to mangle the request or artificially end
Perhaps it would be simpler to move this "ignore duplicate requests" step to before we pass the request into the JSON-RPC engine.
Do you mean we would do that from within the provider? I probably don't understand something here... the retryStuckRequests call happens in the stream middleware after the request has been passed to the JSON-RPC engine. If we move this "ignore duplicate requests" logic to before that, then couldn't duplicate requests make it to the extension background?
Or do you mean that we will handle these duplicate requests within the metamask background / controller, after the message has been received from the dapp, but before it is streamed into the engine?
I believe I understand the problem, but I'm not sure if we can use this solution. While it will create the desired behavior, it will result in a perpetually hung promise for each duplicate request (see
@metamask/json-rpc-enginehere and here).
@rekmarks In case it makes a difference, those links are to latest version of @metamask/json-rpc-engine 8.0.2, while metamask-extension is still using much older version json-rpc-engine 6.1.0.
- Corresponding source file for
[email protected]here. - Pending upgrade to
@metamask/json-rpc-enginein https://github.com/MetaMask/metamask-extension/pull/22875- Blocked by upgrading from
eth-rpc-errorsto@metamask/json-rpc-errors- Blocked by https://github.com/MetaMask/rpc-errors/pull/141
- Blocked by upgrading from
Or do you mean that we will handle these duplicate requests within the metamask background / controller, after the message has been received from the dapp, but before it is streamed into the engine?
@danjm, to the best of my knowledge, it would have to be this. I agree with @Gudahtt that this would be in many ways a more elegant solution. It could essentially be a middleware in the form of a through / Transform stream that we tack on to the stream pipeline before the providerStream on the inbound side only, here.
I just did a force push, but I left the original 3 commits unchanged, I was just handling the rebase on latest develop
this would be in many ways a more elegant solution. It could essentially be a middleware in the form of a through / Transform stream that we tack on to the stream pipeline before the providerStream on the inbound side only,
@Gudahtt @rekmarks I have done this in the latest commit https://github.com/MetaMask/metamask-extension/pull/24672/commits/84e947f965932f516b1a6ae88181f11e20542ac6
@metamaskbot update-policies
Policies updated
@metamaskbot update-policies
Policies updated
Builds ready [7ae9c9f]
- builds: chrome, firefox
- builds (beta): chrome
- builds (flask): chrome, firefox
- builds (MMI): chrome, firefox
- builds (test): chrome, firefox
- builds (test-flask): chrome, firefox
- build viz: Build System
- mv3: Background Module Init Stats
- mv3: UI Init Stats
- mv3: Module Load Stats
- mv3: Bundle Size Stats
- mv2: E2e Actions Stats
- code coverage: Report
- storybook: Storybook
- typescript migration: Dashboard
- all artifacts
Page Load Metrics (313 ± 337 ms)
| Platform | Page | Metric | Min (ms) | Max (ms) | Average (ms) | StandardDeviation (ms) | MarginOfError (ms) |
|---|---|---|---|---|---|---|---|
| Chrome | Home | firstPaint | 69 | 128 | 94 | 15 | 7 |
| domContentLoaded | 9 | 23 | 13 | 4 | 2 | ||
| load | 57 | 2575 | 313 | 702 | 337 | ||
| domInteractive | 9 | 23 | 13 | 4 | 2 |
Bundle size diffs [🚨 Warning! Bundle size has increased!]
- background: 91 Bytes (0.00%)
- ui: 0 Bytes (0.00%)
- common: 0 Bytes (0.00%)
Builds ready [d20349a]
- builds: chrome, firefox
- builds (beta): chrome
- builds (flask): chrome, firefox
- builds (MMI): chrome, firefox
- builds (test): chrome, firefox
- builds (test-flask): chrome, firefox
- build viz: Build System
- mv3: Background Module Init Stats
- mv3: UI Init Stats
- mv3: Module Load Stats
- mv3: Bundle Size Stats
- mv2: E2e Actions Stats
- code coverage: Report
- storybook: Storybook
- typescript migration: Dashboard
- all artifacts
Page Load Metrics (566 ± 465 ms)
| Platform | Page | Metric | Min (ms) | Max (ms) | Average (ms) | StandardDeviation (ms) | MarginOfError (ms) |
|---|---|---|---|---|---|---|---|
| Chrome | Home | firstPaint | 66 | 212 | 110 | 33 | 16 |
| domContentLoaded | 9 | 45 | 17 | 9 | 4 | ||
| load | 54 | 2789 | 566 | 968 | 465 | ||
| domInteractive | 9 | 45 | 17 | 9 | 4 |
Bundle size diffs [🚨 Warning! Bundle size has increased!]
- background: 120 Bytes (0.00%)
- ui: 0 Bytes (0.00%)
- common: 0 Bytes (0.00%)
Builds ready [44ac712]
- builds: chrome, firefox
- builds (beta): chrome
- builds (flask): chrome, firefox
- builds (MMI): chrome, firefox
- builds (test): chrome, firefox
- builds (test-flask): chrome, firefox
- build viz: Build System
- mv3: Background Module Init Stats
- mv3: UI Init Stats
- mv3: Module Load Stats
- mv3: Bundle Size Stats
- mv2: E2e Actions Stats
- code coverage: Report
- storybook: Storybook
- typescript migration: Dashboard
- all artifacts
Page Load Metrics (778 ± 513 ms)
| Platform | Page | Metric | Min (ms) | Max (ms) | Average (ms) | StandardDeviation (ms) | MarginOfError (ms) |
|---|---|---|---|---|---|---|---|
| Chrome | Home | firstPaint | 66 | 150 | 102 | 23 | 11 |
| domContentLoaded | 9 | 62 | 14 | 11 | 5 | ||
| load | 55 | 2636 | 778 | 1069 | 513 | ||
| domInteractive | 9 | 62 | 14 | 11 | 5 |
Bundle size diffs [🚨 Warning! Bundle size has increased!]
- background: 120 Bytes (0.00%)
- ui: 0 Bytes (0.00%)
- common: 0 Bytes (0.00%)
Missing release label release-11.16.6 on PR. Adding release label release-11.16.6 on PR and removing other release labels(release-11.18.0), as PR was cherry-picked in branch 11.16.6.