Skip to content

fix(@Persist): prevent null auto-vivification on read operations#93

Open
apankov1 wants to merge 9 commits intocloudflare:mainfrom
apankov1:fix/null-auto-vivification
Open

fix(@Persist): prevent null auto-vivification on read operations#93
apankov1 wants to merge 9 commits intocloudflare:mainfrom
apankov1:fix/null-auto-vivification

Conversation

@apankov1
Copy link

@apankov1 apankov1 commented Nov 26, 2025

Summary

The @Persist proxy's get() handler was incorrectly auto-vivifying null and undefined values to {} during read operations. This caused:

  • Data corruption: Nullable domain fields became empty objects
  • D1 serialization issues: JSON.stringify({}) produces "[object Object]" strings instead of NULL values in the database
  • Referential transparency violation: Reading a property should never mutate the underlying object

Root Cause

In createDeepProxy, when accessing a property that exists but has a null or undefined value, the proxy was creating a new empty object and assigning it back to the target:

// Before: mutates target[key] from null to {}
const prop = Reflect.get(target, key);
if (typeof prop === 'object' && prop \!== null) { ... }
// Falls through to auto-vivification for null/undefined

Fix

Return null and undefined values as-is without wrapping or auto-vivification:

const prop = Reflect.get(target, key);

// Return null/undefined as-is - these are intentional values
// Do NOT auto-vivify on read operations
if (prop === null || prop === undefined) {
    return prop;
}

Test Plan

  • Added regression test: reading null properties returns null, not {}
  • Added test: spreading proxied objects preserves null values
  • Added test: underlying object is not mutated on read
  • Added test: real-world D1 journaling scenario with nullable fields
  • Added test: undefined values preserved correctly
  • Added test: falsy but valid values (0, false, "") preserved
  • Updated persist example to demonstrate nullable field usage

Related


Code in this PR was AI-assisted and exercised against a pet project that experienced the bug.

The proxy's get() handler was mutating null/undefined properties to {}
when reading them. This corrupted domain values and caused [object Object]
serialization in D1 journaling.

Fix: Return null/undefined as-is instead of auto-vivifying them.

Also updates test to use actual createDeepProxy via __test export.
- Added test for undefined property values (same behavior as null)
- Added test for falsy but valid values (0, false, "")
Add optionalOwner nullable field to persist example showing that
null values are correctly preserved through the @persist proxy.
@apankov1 apankov1 changed the title fix(@Persist): prevent null auto-vivification on read operations fix(@Persist): prevent null auto-vivification on read operations Nov 26, 2025
@apankov1 apankov1 marked this pull request as ready for review November 26, 2025 05:57
The error handler was creating {} and returning a new proxy on ANY error,
which caused:
1. Silent data corruption (original value replaced with {})
2. Infinite proxy recursion leading to heap overflow

Fix: Return undefined on error instead of auto-vivifying.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant