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:

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

getFullYear() / getHours() read the processTZ, 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.tsexportContentDisposition
  • 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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions