8000 Make rpc non-thenable by calintje · Pull Request #508 · anza-xyz/kit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Make rpc non-thenable #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8000
Merged
merged 9 commits into from
Jun 1, 2025

Conversation

calintje
Copy link
Contributor
@calintje calintje commented May 30, 2025

Problem

RPC objects created by Solana Kit use a proxy that converts all property access into methods. When JavaScript's Promise machinery checks for a .then property (which happens when you async await an RPC object), the proxy intercepts this and returns a method { send: [AsyncFunction: send] } instead of undefined or the proper Promise interface, making the RPC object appear thenable when it isn't. This causes JavaScript/TypeScript to silently terminate the program when you try to await the RPC object as if it were a Promise.

Summary of Changes

  • Modified makeProxy function to specifically intercept then property access and return undefined
  • Prevents the proxy from treating .then like any other RPC method
  • Added test case to verify RPC objects are not thenable

Copy link
changeset-bot bot commented May 30, 2025

🦋 Changeset detected

Latest commit: 6b7e878

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 40 packages
Name Type
@solana/rpc-subscriptions-spec Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-api Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/rpc-subscriptions Patch
@solana/accounts Patch
@solana/rpc-api Patch
@solana/rpc-transport-http Patch
@solana/rpc Patch
@solana/sysvars Patch
@solana/kit Patch
@solana/transaction-confirmation Patch
@solana/rpc-graphql Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-data-structures Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/codecs Patch
@solana/compat Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/functional Patch
@solana/instructions Patch
@solana/keys Patch
@solana/nominal-types Patch
@solana/options Patch
@solana/programs Patch
@solana/promises Patch
@solana/react Patch
@solana/rpc-parsed-types Patch
@solana/rpc-spec-types Patch
@solana/rpc-transformers Patch
@solana/rpc-types Patch
@solana/signers Patch
@solana/subscribable Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
bundlemon bot commented May 30, 2025

BundleMon

Files updated (4)
Status Path Size Limits
rpc-spec/dist/index.browser.mjs
852B (+23B +2.77%) -
rpc-spec/dist/index.native.mjs
851B (+22B +2.65%) -
rpc-spec/dist/index.node.mjs
850B (+22B +2.66%) -
rpc-subscriptions-spec/dist/index.node.mjs
2.14KB (+11B +0.5%) -
Unchanged files (123)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
34.39KB -
rpc-graphql/dist/index.browser.mjs
18.78KB -
rpc-graphql/dist/index.native.mjs
18.78KB -
rpc-graphql/dist/index.node.mjs
18.78KB -
errors/dist/index.node.mjs
14.54KB -
errors/dist/index.browser.mjs
14.52KB -
errors/dist/index.native.mjs
14.52KB -
transaction-messages/dist/index.browser.mjs
7.24KB -
transaction-messages/dist/index.native.mjs
7.24KB -
transaction-messages/dist/index.node.mjs
7.24KB -
codecs-data-structures/dist/index.native.mjs
4.77KB -
codecs-data-structures/dist/index.browser.mjs
4.77KB -
codecs-data-structures/dist/index.node.mjs
4.77KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.57KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.56KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.54KB -
rpc-subscriptions/dist/index.browser.mjs
3.38KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
codecs-core/dist/index.browser.mjs
3.3KB -
codecs-core/dist/index.native.mjs
3.3KB -
codecs-core/dist/index.node.mjs
3.3KB -
rpc-transformers/dist/index.browser.mjs
2.93KB -
rpc-transformers/dist/index.native.mjs
2.93KB -
rpc-transformers/dist/index.node.mjs
2.93KB -
addresses/dist/index.browser.mjs
2.86KB -
addresses/dist/index.native.mjs
2.86KB -
addresses/dist/index.node.mjs
2.86KB -
kit/dist/index.browser.mjs
2.71KB -
kit/dist/index.native.mjs
2.71KB -
kit/dist/index.node.mjs
2.71KB -
signers/dist/index.browser.mjs
2.63KB -
signers/dist/index.native.mjs
2.63KB -
signers/dist/index.node.mjs
2.62KB -
codecs-strings/dist/index.browser.mjs
2.53KB -
codecs-strings/dist/index.node.mjs
2.48KB -
codecs-strings/dist/index.native.mjs
2.45KB -
transaction-confirmation/dist/index.node.mjs
2.4KB -
sysvars/dist/index.browser.mjs
2.35KB -
sysvars/dist/index.native.mjs
2.34KB -
transaction-confirmation/dist/index.native.mj
s
2.34KB -
sysvars/dist/index.node.mjs
2.34KB -
transaction-confirmation/dist/index.browser.m
js
2.34KB -
transactions/dist/index.browser.mjs
2.27KB -
transactions/dist/index.native.mjs
2.27KB -
transactions/dist/index.node.mjs
2.26KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.09KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.09KB -
keys/dist/index.browser.mjs
2.02KB -
keys/dist/index.native.mjs
2.02KB -
keys/dist/index.node.mjs
2.02KB -
codecs-numbers/dist/index.native.mjs
2.01KB -
codecs-numbers/dist/index.browser.mjs
2.01KB -
codecs-numbers/dist/index.node.mjs
2.01KB -
react/dist/index.native.mjs
1.99KB -
react/dist/index.browser.mjs
1.99KB -
react/dist/index.node.mjs
1.99KB -
rpc/dist/index.node.mjs
1.95KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.91KB -
rpc/dist/index.native.mjs
1.8KB -
subscribable/dist/index.node.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
subscribable/dist/index.native.mjs
1.75KB -
subscribable/dist/index.browser.mjs
1.74KB -
rpc-transport-http/dist/index.node.mjs
1.73KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.13KB -
accounts/dist/index.native.mjs
1.12KB -
accounts/dist/index.node.mjs
1.12KB -
compat/dist/index.browser.mjs
971B -
compat/dist/index.native.mjs
970B -
compat/dist/index.node.mjs
968B -
rpc-spec-types/dist/index.browser.mjs
964B -
rpc-api/dist/index.browser.mjs
963B -
rpc-api/dist/index.native.mjs
962B -
rpc-spec-types/dist/index.native.mjs
962B -
rpc-api/dist/index.node.mjs
961B -
rpc-spec-types/dist/index.node.mjs
961B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
promises/dist/index.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
769B -
instructions/dist/index.native.mjs
768B -
instructions/dist/index.node.mjs
767B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
event-target-impl/dist/index.node.mjs
233B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
text-encoding-impl/dist/index.node.mjs
119B -
crypto-impl/dist/index.node.mjs
114B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +101B +0.03%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

Copy link
Member
@mcintyre94 mcintyre94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this makes a lot of sense!

Would you be able to make the same change to the subscription RPC which uses the same pattern?

Separately to this PR I'll also open an issue to check if there are any other properties we should be intercepting to stop JS mis-interpreting our proxy objects like this.

@calintje
Copy link
Contributor Author
calintje commented May 30, 2025

That sounds good. As an additional safeguard, we might consider restricting the proxy to only expose known Solana RPC method names. That way, any unexpected property access (like then or others) would default to undefined, avoiding misinterpretation by JavaScript and keeping the surface of the proxy more predictable. I can look into implementing that if it seems worthwhile, but it could also be part of the task you just created.

@mcintyre94
Copy link
Member

For now let's push anything else to that task. We should definitely do then, but when we understand what else (if anything) might cause issues we can figure out what makes the most sense. There would be some tradeoffs here, like not being able to add new methods without a custom proxy, and increasing the bundle size a bit (and for every new method).

Comment on lines 38 to 41
? (
...args: Parameters<TSubscriptionMethodImplementation>
) => PendingRpcSubscriptionsRequest<ReturnType<TSubscriptionMethodImplementation>>
: never;
Copy link
Member
8000

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like something has tweaked the formatting here. If you run pnpm style:fix at the root of the repo it'll apply our formatting/prettier and should make the CI happy!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed!

Copy link
Collaborator

…we might consider restricting the proxy to only expose known Solana RPC method names.

We don't do this on purpose, because to do so would require that we enumerate every method name, which would require that we ship all of those strings in the JavaScript bundle.

Copy link
Collaborator
@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Super interesting.

@@ -72,6 +72,9 @@ function makeProxy<TRpcMethods, TRpcTransport extends RpcTransport>(
return false;
},
get(target, p, receiver) {
if (p === 'then') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for myself: this doesn't have to be p.toString() because there's no way of calling the then method of a Promise using a Symbol.

calintje and others added 2 commits May 30, 2025 21:38
Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
…s-test.ts

Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
@calintje
Copy link
Contributor Author

Thanks for the review!

I learned something new, but i wish this wasn't something that had to be learned 😆

calintje and others added 2 commits May 30, 2025 23:01
Co-authored-by: Callum McIntyre <mcintyre1994@gmail.com>
@steveluscher
Copy link
Collaborator

One last question: are we convinced that we don't also need to blocklist catch and finally, and why not?

@mcintyre94
Copy link
Member

@steveluscher I don't think so - the MDN docs call out thenable as the thing the language allows in place of promises: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables

@calintje
Copy link
Contributor Author

Hmm, I didn't think about that when I encountered this issue.

After looking a bit online, it seems that both catch() and finallly() internally call then()

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch#description

    catch() internally calls then() on the object upon which it was called, passing undefined and onRejected as arguments.

  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally

    Like catch(), finally() internally calls the then method on the object upon which it was called

However, catch would be called on the rpc object and go through the get trap first. I wrote a test to verify the behavior, and it passed (ie. it doesn't reach the internal implementation of catch()):

it('treats catch as an RPC method name and fails appropriately', () => {
    const getPropertySpy = jest.spyOn(Reflect, 'get');

    // @ts-expect-error We're intentionally testing runtime behavior that TypeScript prevents
    const catchFunction = rpcSubscriptions.catch;

    // The function should throw when called
    expect(() => {
        catchFunction(() => { });
    }).toThrow(new SolanaError(SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN, {
        notificationName: 'catch',
    }));

    // Verify it tried to look up 'catch' as an RPC method name
    const spyCalls = getPropertySpy.mock.calls;
    expect(spyCalls.some(call => call[1] === 'catch')).toBe(true);

    getPropertySpy.mockRestore();
});

Given this behavior, I can make the following changes:

  1. Extend the proxy's get trap to explicitly handle all Promise-related methods:
   if (p === 'then' || p === 'catch' || p === 'finally') {
       return undefined;
   }
  1. Update the tests to verify this behavior for all Promise methods:

@calintje
Copy link
Contributor Author

Oh @mcintyre94, good find. So JavaScript only treats an object as Promise-like if it has a .then() method, that's what makes it "thenable". My test was actually just confirming that catch is treated like any other invalid RPC method name, not testing any Promise-related behavior.

Copy link
Collaborator
@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added via Giphy

@steveluscher steveluscher merged commit 304a44f into anza-xyz:main Jun 1, 2025
9 of 10 checks passed
@github-actions github-actions bot mentioned this pull request Jun 1, 2025
Copy link
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 16, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0