Skip to content

Export download filename is stamped in the process-local timezone, not the business timezone #8484

Description

@os-zhuang

Found while implementing #8373 (export cell rendering); filed rather than fixed there, because it changes a different user-visible surface (the suggested download filename) and #8373's fix direction named only the cell formatter.

What

exportContentDisposition() in packages/rest/src/export-format.ts builds the download's filename timestamp with local-time getters:

const stamp =
  `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
  `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;

getFullYear() / getHours() read the process TZ, which is a deployment-host fact, not the caller's business timezone. The route resolves the business timezone one frame up (resolveExecCtx gives an ExecutionContext carrying timezone, the platform-default to global to tenant cascade) and does not pass it here.

Why it matters now

Before #8373 this was the quieter half of a self-contradiction the issue reported as evidence: the filename read the host clock while the file's contents read UTC. #8373 moves the contents onto the business timezone. The filename is now the only part of an export still on the host clock, so the two disagree exactly when TZ is not the business timezone — a container at TZ=UTC serving an Asia/Shanghai tenant downloads orders-20260731-220000.csv whose first row is 2026-08-01 06:00:00. The reported deployment happened to set TZ=Asia/Shanghai, which is why the filename looked right there.

Not a regression introduced by #8373 — the local-time stamp predates it — but #8373 is what makes it the odd one out.

Suggested direction

Thread the same ExecutionContext.timezone into exportContentDisposition() and take the stamp's calendar components from it, falling back to today's behaviour when no timezone is resolved. Note the fallback question is worth deciding explicitly rather than assuming: for the contents the compatible fallback is UTC, but this function has always used process-local, so "no timezone" here means keeping local, not switching to UTC — otherwise every deployment without a configured timezone sees its filenames change.

Repro

Unit level, no server needed:

TZ=UTC       node -e "console.log(new Date('2026-08-01T02:00:00Z').getHours())"   # 2
TZ=Asia/Shanghai node -e "console.log(new Date('2026-08-01T02:00:00Z').getHours())"   # 10

exportContentDisposition('orders', undefined, 'csv', new Date('2026-08-01T02:00:00Z')) yields a different stamp per host TZ for the same instant and the same tenant.

Scope

  • packages/rest/src/export-format.ts — exportContentDisposition
  • its call site in the export route in packages/rest/src/rest-server.ts
  • packages/rest/src/export-format.test.ts pins the current local-time behaviour (const NOW = new Date(2026, 6, 14, 15, 30, 45)), so those expectations move with the fix.

Related: #8373.


Generated by Claude Code

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/rest/src/export-format.ts (exportContentDisposition) ⇒ domain:cli. Queued as Bug: after #8373 the filename is the only export surface still on the process clock, so it disagrees with the file's own contents whenever host TZ ≠ business timezone. Note the card's own fallback caution — "no resolved timezone" here means keep process-local, not switch to UTC. Sibling of #8485 (import half); same packages/rest surface, serial or same-dispatch per the lane seat's call.

    Size/model suggestion: S, mechanical with the pinned test moving.

    本评论来自分诊座位 Routine。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions