Replies: 4 comments
|
机制完全复现,且比「backward 链接全被拒」更精确——我在本机 Node 26.5.0(ICU 78.3)实测: 即:ICU 78.3 的 tzdata 里其它中国 backward 链接都在(会被规范化到 canonical 名),唯独 Asia/Beijing 不在——它没有作为可解析别名进 ICU,所以服务端直接 RangeError。UOS/Deepin 的系统时区文件指向 Asia/Beijing,恰是这个唯一缺失的名字,命中概率 100%。 完整链路(对照 4e84901 = alpha.4):
修复缝:在 这是客户端/服务端 ICU tzdata 不对称:浏览器系 Chromium 自带 tzdata 认这个别名(或走系统 zoneinfo 文件),而服务端 Node 的这个 ICU 构建不认——同一 OS 时区名客户端能报、服务端会拒。你用的 0.1.1-rc.2 与 alpha.4 代码同形,升级不解决,需要 util-time 补别名解析。 |
|
我已在本地把仓库源码跑起来,这里是源码级根因和一个本地实测过的候选修复,供维护者参考。 根因(源码级)校验断点在 该函数本来就通过 ICU 规范化并返回规范名,所以「接受别名并存规范名」是它的设计意图(注释也说明了 canonical name 的原因)。真正的 bug 在 catch 分支:
注意一个细节:ICU 能接受并规范化它的兄弟 backward 链接 候选修复(本地已验证)思路:ICU 抛异常时不要直接放弃,先查一张「已知被 ICU 拒绝的 tzdata backward 链接」小表(从 IANA --- a/packages/util/time/src/index.ts
+++ b/packages/util/time/src/index.ts
@@ (IANA_TIME_ZONE 常量之后)
+/**
+ * tzdata `backward` links that this runtime's ICU rejects but real clients
+ * report. Values are canonical IANA Area/Location names. Derived from the IANA
+ * tzdata `backward` file; add a link only when a real client is known to send
+ * it and ICU rejects it, so the table stays minimal.
+ *
+ * UOS / Deepin desktop default the system zone to the backward link
+ * Asia/Beijing, which the browser reports verbatim; Node's ICU rejects it.
+ */
+const BACKWARD_ALIASES: Readonly<Record<string, string>> = Object.freeze({
+ 'Asia/Beijing': 'Asia/Shanghai',
+})
+
export function canonicalClientTimeZone(value: string): string | undefined {
if (value.length === 0 || value.trim() !== value
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined
+ const candidate = resolveIcu(value)
+ if (candidate !== undefined) return candidate
+ // ICU rejects a small set of tzdata backward links (e.g. Asia/Beijing);
+ // resolve them through their canonical target and validate once more.
+ const target = BACKWARD_ALIASES[value]
+ return target === undefined || target === value ? undefined : resolveIcu(target)
+}
+
+/** Resolve one IANA name through ICU, or undefined when ICU rejects or uncanonicalizes. */
+function resolveIcu(zone: string): string | undefined {
try {
- const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value })
+ const canonical = new Intl.DateTimeFormat('en-US', { timeZone: zone })
.resolvedOptions().timeZone
/* v8 ignore next -- Intl returns UTC or a canonical IANA Area/Location for accepted input. */
if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return undefined
return canonical
} catch {
- // Intl rejects unsupported zone names; the caller maps that parser rejection.
return undefined
}
}本地对构建产物验证(修复版逻辑)结果: 备选方案如果不想维护小表,等价的更通用做法是运行时读取宿主 tzdb 的 外部 PR 开放后我很乐意把它做成正式补丁;在此之前先贴在这里供参考。 |
|
你这个补丁形状我完全认可——alias 表 + 二次 ICU 验证(表只当提示、ICU 当权威)是正确的不变量:表里条目写错也永远返回不了 ICU 拒绝的名字;将来某版 ICU/tzdata 若学会解析 Asia/Beijing,该条目自然变成死代码而不是错答案。补充几个刚从 master 核实的事实,让补丁可以直接落: 1. master(49a606bc5 = alpha.5)上该函数与你引的 alpha.4 逐字节一致自 4e84901 后该文件只有两次提交(5af9eec51c / 804b1ff),都是 api 网关失败面的重构,未触碰 2. 共享函数修复自动覆盖两个调用点(不止网关一条路径)master 上
3. 姊妹实现有一个漏网:schedule 域(你的补丁覆盖不到)
4. 测试建议在 另外你这轮的两个报告(#5403 修复 + #5499 locale 沙箱判定)是同一类价值的两个样本:UOS/zh_CN 平台盲区,英文 CI 永远测不到。若后续要提 PR(通道开放时),建议两案互相引用,正文都点明"上游英文 CI 无法覆盖、需要 locale 固定的回归测试"。 |
|
补丁完成(含单测,已在当前 master 验证: 补丁(diff 基于当前 master): diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts
index 1867965cf2..acc75478d9 100644
--- a/packages/util/time/src/index.ts
+++ b/packages/util/time/src/index.ts
@@ -8,6 +8,20 @@
/** Strict browser-zone profile: UTC or an IANA Area/Location-style identifier. */
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
+/**
+ * tzdata `backward` links this runtime's ICU rejects outright but real clients
+ * report. Values are canonical IANA Area/Location names, derived from the IANA
+ * tzdata `backward` file. Add a link only when a real client is known to send
+ * it AND the bundled ICU rejects it, so the table stays minimal.
+ *
+ * UOS / Deepin desktops default the system zone to the backward link
+ * Asia/Beijing, which the browser reports verbatim; the bundled ICU rejects it
+ * while accepting its sibling links (Asia/Chongqing, Asia/Harbin).
+ */
+const BACKWARD_ALIASES: Readonly<Record<string, string>> = Object.freeze({
+ 'Asia/Beijing': 'Asia/Shanghai',
+})
+
/**
* Validate and canonicalize one caller-supplied IANA zone at a wire boundary.
*
@@ -20,8 +34,19 @@ const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
export function canonicalClientTimeZone(value: string): string | undefined {
if (value.length === 0 || value.trim() !== value
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined
+ const canonical = resolveIcu(value)
+ if (canonical !== undefined) return canonical
+ // ICU rejects a small set of tzdata backward links rather than aliasing them;
+ // resolve each through its canonical target and validate that target once
+ // more before accepting it — never trust the mapping alone.
+ const target = BACKWARD_ALIASES[value]
+ return target === undefined ? undefined : resolveIcu(target)
+}
+
+/** Resolve one IANA name through ICU; `undefined` when ICU rejects or uncanonicalizes it. */
+function resolveIcu(zone: string): string | undefined {
try {
- const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value })
+ const canonical = new Intl.DateTimeFormat('en-US', { timeZone: zone })
.resolvedOptions().timeZone
/* v8 ignore next -- Intl returns UTC or a canonical IANA Area/Location for accepted input. */
if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return undefined
diff --git a/packages/util/time/tests/time.spec.ts b/packages/util/time/tests/time.spec.ts
index 905c555700..fdbc3925b6 100644
--- a/packages/util/time/tests/time.spec.ts
+++ b/packages/util/time/tests/time.spec.ts
@@ -17,6 +17,16 @@ describe('canonicalClientTimeZone', () => {
expect(canonicalClientTimeZone(canonical ?? '')).toBe(canonical)
})
+ it('resolves the ICU-rejected backward link Asia/Beijing through its canonical target', () => {
+ // UOS / Deepin desktops default the system zone to this tzdata backward
+ // link and the browser reports it verbatim; the bundled ICU rejects it
+ // (while accepting its siblings Asia/Chongqing and Asia/Harbin). Like any
+ // other alias, Asia/Beijing must not survive the boundary — a durable
+ // record compares against the canonical zone a reader derives.
+ expect(canonicalClientTimeZone('Asia/Beijing')).toBe('Asia/Shanghai')
+ expect(canonicalClientTimeZone('Asia/Shanghai')).toBe('Asia/Shanghai')
+ })
+
it('refuses blank, padded, abbreviated, and single-segment names', () => {
for (const value of ['', ' ', ' UTC', 'UTC ', 'CST', 'GMT+8', 'Asia', 'utc']) {
expect(canonicalClientTimeZone(value)).toBeUndefined()
|
Uh oh!
There was an error while loading. Please reload this page.
环境
0.1.1-rc.2(npx @deepseek-ai/dsh web)Linux 4.19.0-amd64-desktop)Asia/Beijing(UOS 默认,/etc/localtime -> ../usr/share/zoneinfo/Asia/Beijing)复现步骤
dsh web;根因分析
UOS/Deepin 默认时区名是
Asia/Beijing,它不是 IANA 时区数据库的规范名(只是Asia/Shanghai的一个 backward 兼容符号链接,
zoneinfo/Asia/Beijing -> ../PRC)。浏览器把系统时区Asia/Beijing作为clientTimeZone发给服务端;服务端(dsh-host-apiproxy)校验时用Intl.DateTimeFormat("en-US", { timeZone: "Asia/Beijing" })做规范化,该调用直接抛RangeError: Invalid time zone specified: Asia/Beijing,于是请求被拒绝。本机 Node 验证:
实测佐证(修复后)
将系统时区改为
Asia/Shanghai(sudo timedatectl set-timezone Asia/Shanghai)并重启浏览器后:Intl.DateTimeFormat().resolvedOptions().timeZone返回"Asia/Shanghai";进一步印证根因是
Asia/Beijing这一非规范别名被服务端拒绝。期望
服务端能兼容这类 tzdata backward link 别名(
Asia/Beijing、Asia/Chongqing、Asia/Harbin等),或在拒绝时给出明确指引(例如提示把系统时区改为
Asia/Shanghai)。UOS/Deepin 默认就是Asia/Beijing,会让相当一批国内用户一上来就卡住。临时绕过
改完后完全退出并重启浏览器再访问即可。
All reactions