fix: more corner-case audit — partial-failure UX + defensive parsing

Three small reliability fixes from re-auditing:

1. Playlist.vue used Promise.all, so a flaky /detail endpoint would
   blank out the whole page even though /songs returned just fine.
   Switch to allSettled and synthesize a stub playlist header from
   the song list when only detail fails. User can still play the
   playlist; just loses the description/cover.

2. sourceTabs.readAll: typeof null === 'object' AND typeof [] ===
   'object', so a corrupted localStorage value (e.g. an array) would
   be treated as a record and its missing keys would silently fall
   back. Reject explicitly so the failure mode is "clean defaults"
   instead of "wrong shape that almost works".

3. Settings.vue loadProfileConfig: a 200 response with empty/wrong
   body would set profileConfigs[botId] to a falsy/wrong-shape value,
   leaving the row stuck on "加载中..." (because the v-if uses
   !profileConfigs[id]). Validate the shape; surface "响应格式异常"
   so the retry link is reachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.7 committed 2026-05-06 15:52:49 +08:00
1 parent 892d9f7959
commit 221079b89c
3 files changed
+40 -10

No files matched your search

+7 -1
View File
@@ -13,7 +13,13 @@ function readAll(): Partial<Record<TabKey, Source>> {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
return typeof parsed === 'object' && parsed !== null ? parsed : {};
// typeof null === 'object' and typeof [] === 'object' — both pass a
// naive check but neither is a valid record. Reject explicitly so a
// corrupted / manipulated value can't smuggle wrong shapes through.
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return {};
}
return parsed as Partial<Record<TabKey, Source>>;
} catch {
return {};
}
+26 -9
View File
@@ -77,20 +77,37 @@ onMounted(async () => {
const id = route.params.id as string;
const platform = (route.query.platform as string) || 'netease';
try {
const [detailRes, songsRes] = await Promise.all([
// allSettled, not Promise.all — if detail 404s but songs is fine
// (e.g., a QQ playlist whose detail endpoint flaked but the song
// list resolved), we still want to show the songs rather than
// the "歌单不存在" empty state.
const [detailRes, songsRes] = await Promise.allSettled([
axios.get(`/api/music/playlist/${id}/detail`, { params: { platform } }),
axios.get(`/api/music/playlist/${id}`, { params: { platform } }),
]);
playlist.value = detailRes.data.playlist;
songs.value = songsRes.data.songs;
} catch (err: any) {
console.error('Failed to load playlist:', err?.response?.status, err?.message);
const detail = detailRes.status === 'fulfilled' ? detailRes.value.data?.playlist : null;
const songList = songsRes.status === 'fulfilled' ? (songsRes.value.data?.songs ?? []) : [];
if (detail) {
playlist.value = detail;
} else if (songList.length > 0) {
// Fall back to a stub built from the route + first song's cover.
playlist.value = {
id,
name: '歌单',
description: '',
coverUrl: songList[0]?.coverUrl ?? '',
songCount: songList.length,
};
} else {
playlist.value = null;
songs.value = [];
} finally {
loading.value = false;
if (detailRes.status === 'rejected') {
console.error('Failed to load playlist detail:', (detailRes.reason as any)?.response?.status, (detailRes.reason as any)?.message);
}
}
songs.value = songList;
loading.value = false;
});
</script>
+7
View File
@@ -782,6 +782,13 @@ async function loadProfileConfig(botId: string) {
profileLoadError[botId] = null;
try {
const res = await axios.get(`/api/player/${botId}/profile`);
// Defensive: a 200 response with non-object body (empty / proxy
// injection / etc.) would otherwise leave the row stuck on
// "加载中..." because profileConfigs[botId] would be falsy.
if (!res.data || typeof res.data !== 'object' || typeof res.data.avatarEnabled !== 'boolean') {
profileLoadError[botId] = '响应格式异常';
return;
}
profileConfigs[botId] = res.data;
} catch (err: any) {
profileLoadError[botId] = err?.response?.status === 404