mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-01 21:46:52 +08:00
fix(api): 修复无效 envelope 响应刷屏并加固 SW API 缓存
- 去重缓存仅在 envelope 校验通过后清空,避免坏响应命中时逐条弹窗 - SW API 缓存只写入 JSON 响应,排除 opaque 响应与连接探测心跳
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { corsSafeCachePlugin, selectCorsSafeCachedResponse } from '../serviceWorkerCache'
|
||||
import {
|
||||
corsSafeCachePlugin,
|
||||
jsonOnlyCachePlugin,
|
||||
selectCorsSafeCachedResponse,
|
||||
shouldCacheJsonResponse,
|
||||
} from '../serviceWorkerCache'
|
||||
|
||||
function createResponse(type: ResponseType) {
|
||||
const response = new Response('image')
|
||||
@@ -51,3 +56,53 @@ describe('Service Worker cache CORS boundary', () => {
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Service Worker API cache JSON boundary', () => {
|
||||
function createJsonResponse(contentType: string) {
|
||||
return new Response('{"success":true,"message":"","data":null}', {
|
||||
headers: { 'content-type': contentType },
|
||||
})
|
||||
}
|
||||
|
||||
it('accepts a standard JSON API response', () => {
|
||||
expect(shouldCacheJsonResponse(createJsonResponse('application/json; charset=utf-8'))).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an HTML response that would break the envelope contract', () => {
|
||||
const html = new Response('<html><body>offline shell</body></html>', {
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' },
|
||||
})
|
||||
|
||||
expect(shouldCacheJsonResponse(html)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a binary response such as an image or blob', () => {
|
||||
const image = new Response(new Blob(['\x89PNG']), { headers: { 'content-type': 'image/png' } })
|
||||
|
||||
expect(shouldCacheJsonResponse(image)).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes the boundary through the Workbox cache update lifecycle', async () => {
|
||||
const response = createJsonResponse('application/json')
|
||||
|
||||
await expect(
|
||||
jsonOnlyCachePlugin.cacheWillUpdate?.({
|
||||
event: new Event('fetch') as ExtendableEvent,
|
||||
request: new Request('https://moviepilot/api/v1/resource'),
|
||||
response,
|
||||
}),
|
||||
).resolves.toBe(response)
|
||||
})
|
||||
|
||||
it('skips caching a non-JSON response through the Workbox lifecycle', async () => {
|
||||
const response = new Response('<html></html>', { headers: { 'content-type': 'text/html' } })
|
||||
|
||||
await expect(
|
||||
jsonOnlyCachePlugin.cacheWillUpdate?.({
|
||||
event: new Event('fetch') as ExtendableEvent,
|
||||
request: new Request('https://moviepilot/api/v1/resource'),
|
||||
response,
|
||||
}),
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,3 +19,22 @@ export const corsSafeCachePlugin: WorkboxPlugin = {
|
||||
return selectCorsSafeCachedResponse(request, cachedResponse)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定响应是否允许写入 API 运行时缓存。
|
||||
*
|
||||
* API 缓存的消费方要求 JSON envelope;SPA 回退页等 HTML 或损坏体一旦被写入,
|
||||
* NetworkFirst 超时回退时会以 200 返回坏体,前端 envelope 校验失败后逐条弹
|
||||
* “服务器返回了无效响应”(仅清浏览器缓存可解)。写入前校验 Content-Type
|
||||
* 可以从源头阻断非 JSON 响应进入缓存。
|
||||
*/
|
||||
export function shouldCacheJsonResponse(response: Response): boolean {
|
||||
return (response.headers.get('content-type') ?? '').includes('json')
|
||||
}
|
||||
|
||||
/** API 缓存专用:只写入声明为 JSON 的响应,HTML、图片等一律不落缓存。 */
|
||||
export const jsonOnlyCachePlugin: WorkboxPlugin = {
|
||||
async cacheWillUpdate({ response }) {
|
||||
return shouldCacheJsonResponse(response) ? response : null
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user