Skip to content

instrumentDurableObjectStorage Proxy breaks native storage.sql getter (Illegal invocation) #19661

@dmmulroy

Description

@dmmulroy

Problem

Since 10.39.0, instrumentDurableObjectWithSentry breaks any DurableObject that accesses ctx.storage.sql (SQLite storage API). Both production workerd and miniflare are affected.

Error:

SqlError: SQL query failed: Illegal invocation: function called with incorrect `this` reference.

10.38.0 works. 10.39.0+ does not.

Root Cause

instrumentDurableObjectStorage (in packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts) wraps DurableObjectStorage in a Proxy whose get trap uses Reflect.get(target, prop, receiver) where receiver is the proxy:

get(target, prop, receiver) {
  const original = Reflect.get(target, prop, receiver); // receiver = proxy

When accessing storage.sql, the native getter on DurableObjectStorage validates this via internal slots (brand check). Passing the proxy as receiver means the getter executes with this = proxy instead of the real native storage object, failing the brand check.

Non-function properties like sql (a getter returning SqlStorage) hit the typeof original !== 'function' early return and are returned directly — but by then the getter has already been called with the wrong this.

Why KV methods (get/put/delete/list) are unaffected: they are functions, so the proxy explicitly .bind(target)s them or wraps them with Reflect.apply(original, target, args). The this is correct for functions, but not for getters.

Fix

One-line change — use target as the receiver:

- get(target, prop, receiver) {
-   const original = Reflect.get(target, prop, receiver);
+ get(target, prop, _receiver) {
+   const original = Reflect.get(target, prop, target);

This ensures native getters execute with the real storage object as this.

Minimal Reproduction

import { DurableObject } from "cloudflare:workers";
import { instrumentDurableObjectWithSentry } from "@sentry/cloudflare";

class CounterBase extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    // Throws: SqlError: Illegal invocation
    this.ctx.storage.sql.exec(
      "CREATE TABLE IF NOT EXISTS counts (name TEXT PRIMARY KEY, value INTEGER DEFAULT 0)"
    );
  }
}

export const Counter = instrumentDurableObjectWithSentry(
  (env) => ({ dsn: env.SENTRY_DSN }),
  CounterBase,
);

Related

Environment

  • @sentry/cloudflare 10.39.0+ (any version with instrumentDurableObjectStorage)
  • Both production workerd and miniflare
  • Any DurableObject using SQLite storage API (ctx.storage.sql)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions