cloudflare / cloudflare/workers-sdk

Date objects in forwarded tail events arrive as strings in local dev

Open
#15,180 2 comments 0 reactions 0 assignees View on GitHub
package:miniflare
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
186

Description

### What versions & operating system are you using?

`main` (fb6b51b8), Node v22.17.1, macOS 15. Reproduced against `packages/miniflare` in the monorepo; the core of it reproduces in plain Node with no Workers involved.

### Please provide a link to a minimal reproduction

Inlined below — it's six lines of plain JS against the replacer as it's currently written, so a linked repo would only add noise.

```js
const SERIALIZED_DATE = "___serialized_date___";
const replacer = (_, value) =>
value instanceof Date ? { [SERIALIZED_DATE]: value.toISOString() } : value;

console.log(JSON.stringify({ scheduledTime: new Date(0) }, replacer));
// {"scheduledTime":"1970-01-01T00:00:00.000Z"} <- no tag, so nothing to revive
```

### Describe the Bug

When tail events are forwarded between two local dev sessions through the dev registry, `Date` values arrive at the tail consumer as ISO strings rather than `Date` objects. `event.scheduledTime.getTime()` throws in local dev, while the same tail handler works in production. `TraceItemAlarmEventInfo.scheduledTime` is typed `Date`, so the types promise something local dev doesn't deliver.

The forwarding path serialises with `JSON.stringify(events, tailEventsReplacer)` and revives with `tailEventsReviver` (`packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts`). The replacer is meant to tag `Date`s so the reviver can rebuild them:

```ts
export function tailEventsReplacer(_: string, value: any) {
if (value instanceof Date) {
return { [SERIALIZED_DATE]: value.toISOString() };
} else if (typeof value === "bigint") {
return { [SERIALIZED_BIGINT]: value.toString() };
}
return value;
}
```

`JSON.stringify()` applies `Date.prototype.toJSON()` before it calls the replacer, so a real `Date` reaches the replacer already flattened to an ISO string. `value instanceof Date` is therefore never true for it, no tag is written, and the reviver has nothing to restore. The `bigint` arm is unaffected, which is why that half of the round-trip works.

Worth noting the `instanceof` check isn't dead: a value whose own `toJSON()` returns a `Date` reaches the replacer unconverted and is tagged correctly today, so that behaviour should be preserved rather than swapped out.

`packages/vite-plugin-cloudflare/src/workers/vite-proxy-worker/index.ts` carries its own copy of the pair with the same `Date` behaviour. The two aren't identical — the vite copy has no `bigint` handling — so they're the same `Date` issue in two places rather than one shared implementation.

This is a local-dev fidelity problem (dev diverging from production), not a production issue.

### Please provide any relevant error logs

```
AssertionError: expected '2025-05-01T12:34:56.000Z' to be an instance of Date
```

I have a fix with tests and will open a PR shortly.

Contributor guide

Open the contributing guide

Research direction

Start with packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts and the duplicated pair in packages/vite-plugin-cloudflare/src/workers/vite-proxy-worker/index.ts; run the six-line plain Node reproduction and inspect their forwarding round trips. Done means forwarded Date values are restored as Date objects in local dev, bigint behavior remains intact where supported, and values with a custom toJSON() retain their current handling, with regression tests added.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.