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
No fields configured for issues without a type.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions