fix(server-storage): refuse user state for deleted playlists

PUT /v1/user-state/:namespace/:playlistId answers 404 playlist_missing
when the server has no such playlist, so an edit queued on another device
(including its conflict replay with If-None-Match: *) cannot recreate the
state of a playlist deleted meanwhile. The client drops such an edit and
its cache entry instead of retrying; page-hide writes report it as gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 23:43:43 +02:00
1 parent 716580cb16
commit f956758cce
8 files changed
+114 -8

No files matched your search

@@ -205,6 +205,25 @@ describe('server storage routes', () => {
})
));
it('refuses user state for a playlist it does not have', () =>
withTempStorage((storage) =>
withServer(app(storage), async (baseUrl) => {
const entry = `${baseUrl}/v1/user-state/xtream-favorites/gone`;
const put = await fetch(
entry,
json({ value: ['live:1'] }, { method: 'PUT' })
);
expect(put.status).toBe(404);
await expect(put.json()).resolves.toMatchObject({
code: 'playlist_missing',
});
// Removing what is not there stays harmless.
expect((await fetch(entry, { method: 'DELETE' })).status).toBe(
200
);
})
));
it('refuses a playlist write built on a replaced version', () =>
withTempStorage((storage) =>
withServer(app(storage), async (baseUrl) => {
@@ -235,6 +254,10 @@ describe('server storage routes', () => {
it('writes user state only on the tag it was read at', () =>
withTempStorage((storage) =>
withServer(app(storage), async (baseUrl) => {
await fetch(
`${baseUrl}/v1/playlists/p1`,
json(m3uDocument('p1'), { method: 'PUT' })
);
const entry = `${baseUrl}/v1/user-state/xtream-favorites/p1`;
// Created only while absent.
const created = await fetch(
@@ -25,9 +25,20 @@ export function createUserStateRouter(storage: ServerStorage): Router {
});
const write = (req: Request, res: Response, value: unknown) => {
const namespace = requireNamespace(String(req.params['namespace']));
const playlistId = String(req.params['playlistId']);
// An edit another device queued before the playlist was deleted
// must not bring its user state back.
if (value !== undefined && !storage.playlists.exists(playlistId)) {
throw new StorageRequestError(
404,
'playlist_missing',
'The playlist does not exist.'
);
}
const result = storage.userState.writeIf(
requireNamespace(String(req.params['namespace'])),
String(req.params['playlistId']),
namespace,
playlistId,
value,
expectedTag(req)
);