An RPC stub was not disposed properly using meta frameworks

I wanted to open a thread here regarding this issue. I have seen multiple people inlucuding myself that gets this error. I absolutely understand where this error is comming from, finding the solution here is the issue. Small explanation: If you use a meta framework like sveltekit etc and you use RPC on the server (service binding from a normal worker) you will run into this issue. Sadly using or [Symbol.dispose]() dosent seem to work in those enviroments. You will run into framework errors like TypeError: products[Symbol.dispose] is not a function (and if you check if Symbol.dispose exists before trying to dispose it never exists and you still get the error) So i was wondering if there is a solution? If this is a known issue that is being worked on? There was a small discussion here but without any solution sadly https://discord.com/channels/595317990191398933/773219443911819284/1360010131462557879 Would be nice to know if RPC isnt recommended to use in meta frameworks due to this issue
17 Replies
Murder Chicken
I had encountered this as well and was able to fix it through use of a helper function on the consumer side (i.e., when I make the RPC request).
/**
* Executes an RPC call and ensures proper cleanup.
* @param {Function} rpcCall - A function that performs the RPC call.
* @returns {Promise<any>} - The result of the RPC call.
*/
export async function executeRpcWithCleanup(rpcCall) {
let rpcResult = null;
try {
rpcResult = await rpcCall();
return rpcResult;
} finally {
if (rpcResult && typeof rpcResult[Symbol.dispose] === 'function') {
// console.info('Disposing RPC result');
rpcResult[Symbol.dispose]();
} else {
// console.info('No Symbol.dispose method on rpcResult');
}
}
}
/**
* Executes an RPC call and ensures proper cleanup.
* @param {Function} rpcCall - A function that performs the RPC call.
* @returns {Promise<any>} - The result of the RPC call.
*/
export async function executeRpcWithCleanup(rpcCall) {
let rpcResult = null;
try {
rpcResult = await rpcCall();
return rpcResult;
} finally {
if (rpcResult && typeof rpcResult[Symbol.dispose] === 'function') {
// console.info('Disposing RPC result');
rpcResult[Symbol.dispose]();
} else {
// console.info('No Symbol.dispose method on rpcResult');
}
}
}
await executeRpcWithCleanup(() => env.SERVICE.myFunction());
await executeRpcWithCleanup(() => env.SERVICE.myFunction());
Your mileage may vary but hopefully this works for you.
Silvan
SilvanOP2d ago
thanks i will check it out! yep sadly as expected. i still get the error. Symbol.dispose seems to never exist. at least in sveltekit
Murder Chicken
What's your RPC method look like? How are you implementing it?
Silvan
SilvanOP2d ago
well curretly i have the silly setup where my do has a rpcTarget and my worker returns that so i can basically use DO RPCs in local dev but yea i also ran into the issue with a more normal setup but its weird i made a minimal reproduction repo with that setup and i just dont get the error in there
Murder Chicken
You're using a WorkerEntrypoint? The methods are async?
Silvan
SilvanOP2d ago
yes im using WorkerEntrypoint. i played around with them being async and not but it dosent seem to make any difference but i might be wrong here it looks something like this index.ts
import { WorkerEntrypoint } from 'cloudflare:workers';
import { WorkerDO } from './worker-do';

export { WorkerDO };

export default class Worker extends WorkerEntrypoint<Env> {
async fetch(request: Request) {
return new Response('Hello');
}
async getWorkerRPC(doId: string) {
const id = this.env.WORKER_DO.idFromName(doId);
const stub = this.env.WORKER_DO.get(id);
return stub.getRpcTarget();
}
}
import { WorkerEntrypoint } from 'cloudflare:workers';
import { WorkerDO } from './worker-do';

export { WorkerDO };

export default class Worker extends WorkerEntrypoint<Env> {
async fetch(request: Request) {
return new Response('Hello');
}
async getWorkerRPC(doId: string) {
const id = this.env.WORKER_DO.idFromName(doId);
const stub = this.env.WORKER_DO.get(id);
return stub.getRpcTarget();
}
}
do.ts
import { DurableObject, RpcTarget } from 'cloudflare:workers';

type Product = { id: string; name: string };

export class WorkerDO extends DurableObject<Env> {
products: Product[] = [];

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);

ctx.blockConcurrencyWhile(async () => {
this.products = (await ctx.storage.get('products')) || [];
});
}

async getProducts() {
return this.products;
}

async getProductById(id: string) {
return this.products.find((product) => product.id === id);
}

async addProduct(productData: Omit<Product, 'id'>) {
const newProduct: Product = { ...productData, id: crypto.randomUUID() };
this.products.push(newProduct);
await this.ctx.storage.put('products', this.products);
return newProduct;
}

async deleteProduct(productId: string): Promise<boolean> {
const initialLength = this.products.length;
this.products = this.products.filter((p) => p.id !== productId);
if (this.products.length === initialLength) return false; // Not found
await this.ctx.storage.put('products', this.products);
return true;
}

private rpcTarget = class extends RpcTarget {
private stub: WorkerDO;

constructor(stub: WorkerDO) {
super();
this.stub = stub;
}
async getProducts() {
return this.stub.getProducts();
}
async getProductById(id: string) {
return this.stub.getProductById(id);
}
async addProduct(productData: Omit<Product, 'id'>) {
return this.stub.addProduct(productData);
}
async deleteProduct(productId: string) {
return this.stub.deleteProduct(productId);
}
};

async getRpcTarget() {
return new this.rpcTarget(this);
}
}
import { DurableObject, RpcTarget } from 'cloudflare:workers';

type Product = { id: string; name: string };

export class WorkerDO extends DurableObject<Env> {
products: Product[] = [];

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);

ctx.blockConcurrencyWhile(async () => {
this.products = (await ctx.storage.get('products')) || [];
});
}

async getProducts() {
return this.products;
}

async getProductById(id: string) {
return this.products.find((product) => product.id === id);
}

async addProduct(productData: Omit<Product, 'id'>) {
const newProduct: Product = { ...productData, id: crypto.randomUUID() };
this.products.push(newProduct);
await this.ctx.storage.put('products', this.products);
return newProduct;
}

async deleteProduct(productId: string): Promise<boolean> {
const initialLength = this.products.length;
this.products = this.products.filter((p) => p.id !== productId);
if (this.products.length === initialLength) return false; // Not found
await this.ctx.storage.put('products', this.products);
return true;
}

private rpcTarget = class extends RpcTarget {
private stub: WorkerDO;

constructor(stub: WorkerDO) {
super();
this.stub = stub;
}
async getProducts() {
return this.stub.getProducts();
}
async getProductById(id: string) {
return this.stub.getProductById(id);
}
async addProduct(productData: Omit<Product, 'id'>) {
return this.stub.addProduct(productData);
}
async deleteProduct(productId: string) {
return this.stub.deleteProduct(productId);
}
};

async getRpcTarget() {
return new this.rpcTarget(this);
}
}
with this minimal setup i dont ever get the stub error weirdly
Murder Chicken
Service Worker
export default {
async fetch(request, env, ctx) {
// ... stuff
},
};

export class ServiceLayerEntryPoint extends WorkerEntrypoint {
async getKvValue(key: string) {
return await this.env.KV.get(key);
}
}
export default {
async fetch(request, env, ctx) {
// ... stuff
},
};

export class ServiceLayerEntryPoint extends WorkerEntrypoint {
async getKvValue(key: string) {
return await this.env.KV.get(key);
}
}
Consumer Worker
export default {
async fetch(request, env, ctx) {
const kvKey = 'somevalue';
const kvValueFromService = await executeRpcWithCleanup(() => env.SERVICE.getKvValue(kvKey));
},
};
export default {
async fetch(request, env, ctx) {
const kvKey = 'somevalue';
const kvValueFromService = await executeRpcWithCleanup(() => env.SERVICE.getKvValue(kvKey));
},
};
This would be a minimal example worker-to-service worker.
Silvan
SilvanOP2d ago
but in a worker to worker context couldnt you just use using?
Murder Chicken
Not unless I use the experimental worker. AFAIK
Silvan
SilvanOP2d ago
no afaik wrangler 4 is polyfilling or w/e automatically
Murder Chicken
I hadn't seen that update. I also have limited DO experience so, my examples might not be 1:1.
Silvan
SilvanOP2d ago
No description
Silvan
SilvanOP2d ago
Cloudflare Docs
Lifecycle
Memory management, resource management, and the lifecycle of RPC stubs.
Murder Chicken
That's awesome. I hated wrapping method calls to bypass the issue.
Silvan
SilvanOP2d ago
i see. well with meta frameworks using breaks everything thats why i am not using it oh well i will keep digging because even if the error is rare and random, it dosent make sense that it never pops up in my minimal code base
Murder Chicken
I noticed it was inconsistent as well. Usually when I was making a buttload of RPC calls in a short period of time.
Silvan
SilvanOP2d ago
yea must be something like that but even then its super weird. i think for me it happens the most often when i do an rpc call and refresh my page but even then happens like 1 out of 15 times

Did you find this page helpful?