Skip to content

fix: 音击设置页导出的是 index.html 而不是玩家 JSON(rawFetch 拿到相对路径,请求被解析到不存在的 /ongeki/api/...) #56

Description

@LittlePenhu

在 /ongeki/settings(音击设置)点"导出档案",下载到的 ongeki_<extId>_exported.json 里是网页源码,不是玩家 JSON。同一功能在 /chuni/v2/setting 和 /mai2/setting 都是正常的。

复现(main @ 90ed95b,网站线上版本):

  1. 打开 /ongeki/settings,点底部"导出";
  2. 得到一个 1796 字节的 ongeki_<extId>_exported.json;
  3. 打开看,内容以 <!doctype html> 开头,是 SPA 的 index.html。

我拿两份真实下载文件比了一遍:

文件 字节 开头 能否 JSON.parse
ongeki_*_exported.json 1796 <!doctype html> 否(Unexpected token '<')
chusan_*_exported.json 68100 {"gameId":"SDHD","userData":{… 是

ongeki 那份的字节数与仓库 dist/index.html 完全一致(都是 1796),\r\n / 裸 \n / 裸 \r 的出现次数也逐项相同(38 / 40 / 39),唯一差别是 vite 的产物哈希(线上 index-bhne9qTR.js,我本地构建 index-B2FdUPgg.js)。也就是说服务端返回的就是那份 SPA 外壳。

再直接打了一遍服务器确认(不带任何凭据的 GET,只看响应):

请求(https://portal.naominet.live) 响应
/api/game/ongeki/export 401 + application/json ← 接口存在,只是要 Bearer
/ongeki/api/game/ongeki/export 200 + text/html + 1796 字节 ← 就是上面那条 bug URL
/no/such/path/at/all 同一份 1796 字节的 HTML

三点因此确定:音击导出接口本身没问题(401 说明它存在),这一条完全在前端;那 1796 字节的外壳对任意未知路径都一样,这正是请求打错地方还能拿到 200、一点报错都没有的原因;以及我手上那份导出文件与服务器对 bug URL 的响应逐字节相同(1796 / CRLF 38 / LF 40 / CR 39,两侧 bundle 哈希同为 bhne9qTR,与当前线上构建一致)。

根因在 src/features/ongeki/OngekiSettingPage.tsx:57:

const resp = await rawFetch('api/game/ongeki/export');

rawFetch(src/lib/api/client.ts:64-66)不做任何前缀拼接,是"原样透传"的:

export async function rawFetch(url: string, init: RequestInit = {}): Promise<Response> {
  return fetch(url, { ...init, headers: { ...authHeaders(), ...(init.headers as object) } });
}

前缀拼接在 buildUrl() 里(client.ts:34-43,API_SERVER = '/',见 :14),只有 api.get/post/put/delete 和 api.blob 会经过它:

blob: async (path: string, params?: QueryParams): Promise<Blob> => {
  const resp = await rawFetch(buildUrl(path, params));   // ← 先拼成 /api/...
  if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
  return resp.blob();
},

另外两处导出都走了 api.blob,所以路径是对的:

页面 调用
ChuniV2SettingPage.tsx:109 api.blob('api/game/chuni/v2/export', { aimeId })
Maimai2SettingPage.tsx:216 api.blob('api/game/maimai2/export', { aimeId })
OngekiSettingPage.tsx:57 rawFetch('api/game/ongeki/export'),原样交给 fetch

不带前导斜杠的字符串会被当成相对 URL,按当前文档 URL 解析(index.html 里没有 <base> 标签)。当前页是 /ongeki/settings,所以 settings 这一段被替换掉了:

new URL('api/game/ongeki/export', 'https://portal.naominet.live/ongeki/settings')
  → https://portal.naominet.live/ongeki/api/game/ongeki/export

请求打到一个不存在的路径上,服务端按 SPA 规则回了 index.html(HTTP 200)。

之所以一声不吭,是因为 OngekiSettingPage.tsx:58 的守卫只看 HTTP 状态:

if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);

200 + text/html 对它来说是完全成功的响应,于是 resp.blob() 把 HTML 存成了 .json,界面上还照常弹了"正在下载档案"。

这是移植时丢的:旧版 Angular 显式拼了前缀(backup 分支 src/app/sega/ongeki/ongeki-setting/ongeki-setting.component.ts:88):

const url = this.apiServer + 'api/game/ongeki/export';   // apiServer = '/',见 src/environments/environment.prod.ts:2
const headers = {Authorization: `Bearer ${this.accountService.currentAccountValue.accessToken}`};
this.http.get(url, {headers, responseType: 'blob'}).subscribe(...)

新版把 this.apiServer + 去掉了,字符串字面量原封不动地留着,也没改成 api.blob,于是一条绝对路径变成了相对路径。

最小改动是补前导斜杠:

-      const resp = await rawFetch('api/game/ongeki/export');
+      const resp = await rawFetch('/api/game/ongeki/export');

更建议直接对齐另外两处,统一走 api.blob():

-      const resp = await rawFetch('api/game/ongeki/export');
-      if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
-      const blob = await resp.blob();
+      const blob = await api.blob('api/game/ongeki/export');

补一句:不建议顺手把 rawFetch 改成自动拼前缀。Maimai2SettingPage.tsx:112 用它上传头像,传的是 /Maimai2Servlet/... 这种绝对路径;如果 rawFetch 变成 API_SERVER + url,'/' + '/Maimai2Servlet/...' 会拼成协议相对 URL //Maimai2Servlet/...,反而坏掉。它保持原样透传是合理的,问题在调用方没给绝对路径——所以也可以考虑在 rawFetch 上加一行注释把「必须传绝对路径」写清楚,避免下次再踩。

最后说明一下影响面,免得排查时扩大范围:这不是一类问题,全仓所有裸 fetch / rawFetch 调用的字面量路径我都逐条看过了,其余的都带前导斜杠(Maimai2SettingPage.tsx:112 的 /Maimai2Servlet/...、AdminPage.tsx:122 的 /api/auth/signout),ContributorsPage.tsx:47 用的是完整 https URL。舞萌与中二的导出也一并核了:两者走 api.blob → buildUrl,实际请求分别是 /api/game/maimai2/export?aimeId=… 和 /api/game/chuni/v2/export?aimeId=…,这两个接口同样返回 401(存在)。所以坏的只有音击这一处。

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions