Skip to content

Commit 0a5841e

Browse files
committed
Added extra response check to validate placeholder image is not html
1 parent ae580a9 commit 0a5841e

2 files changed

Lines changed: 17 additions & 16 deletions

File tree

src/r2mm/image/GameImageProviderImpl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ class GameImageProviderImpl implements GameImageProvider {
103103
}
104104

105105
private async urlReachable(url: string): Promise<boolean> {
106-
return fetch(url).then(res => res.ok).catch(() => false);
106+
return fetch(url).then(res => {
107+
if (!res.ok) {
108+
return false;
109+
}
110+
const contentType = res.headers.get("content-type") ?? "";
111+
return !contentType.includes("text/html");
112+
}).catch(() => false);
107113
}
108114

109115
private cachePathFor(iconUrl: string): string | undefined {

test/vitest/tests/unit/GameImageProvider/GameImageProvider.spec.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ function cachePathFor(iconUrl: string): string {
5656
return nodePath.join(CACHE_ROOT, ...iconUrl.split('/'));
5757
}
5858

59-
function makeFetchResponse(body: ArrayBuffer | string, status = 200, statusText = 'OK'): Response {
59+
function makeFetchResponse(body: ArrayBuffer | string, status = 200, statusText = 'OK', contentType = 'image/webp'): Response {
6060
return {
6161
ok: status >= 200 && status < 300,
6262
status,
6363
statusText,
64+
headers: { get: (name: string) => (name.toLowerCase() === 'content-type' ? contentType : null) },
6465
async arrayBuffer() { return typeof body === 'string' ? new TextEncoder().encode(body).buffer : body; },
6566
} as unknown as Response;
6667
}
@@ -132,6 +133,14 @@ describe('GameImageProviderImpl', () => {
132133
expect(mockFetch).toHaveBeenCalledTimes(1);
133134
});
134135

136+
test('treats a 200 HTML SPA-fallback response as unreachable and falls through to CDN', async () => {
137+
await GameImageProviderImplementation.init();
138+
mockFetch.mockResolvedValueOnce(makeFetchResponse('<!doctype html>', 200, 'OK', 'text/html'));
139+
140+
const result = await GameImageProviderImplementation.resolve('spa-game/spa-game-cover-360x480.webp');
141+
expect(result.startsWith('data:image/webp;base64,')).toBe(true);
142+
});
143+
135144
test('returns disk-cached data URL when bundled is unreachable but cache exists', async () => {
136145
await GameImageProviderImplementation.init();
137146
// Pre-seed cache.
@@ -148,20 +157,6 @@ describe('GameImageProviderImpl', () => {
148157
expect(mockFetch).toHaveBeenCalledTimes(1);
149158
});
150159

151-
test('fetches from CDN, caches, and returns data URL on first cache miss', async () => {
152-
await GameImageProviderImplementation.init();
153-
const cdnBody = new Uint8Array([0x52, 0x49, 0x46, 0x46]).buffer; // "RIFF"
154-
mockFetch.mockResolvedValueOnce(makeFetchResponse('', 404)); // bundled miss
155-
mockFetch.mockResolvedValueOnce(makeFetchResponse(cdnBody, 200)); // CDN hit
156-
157-
const iconUrl = 'fresh-game/fresh-game-cover-360x480.webp';
158-
const result = await GameImageProviderImplementation.resolve(iconUrl);
159-
160-
expect(result.startsWith('data:image/webp;base64,')).toBe(true);
161-
// Should now exist in cache.
162-
expect(await FsProvider.instance.exists(cachePathFor('fresh-game/fresh-game-cover-360x480.webp'))).toBe(true);
163-
});
164-
165160
test('CDN 404 returns placeholder and does NOT increment failure counter', async () => {
166161
await GameImageProviderImplementation.init();
167162
mockFetch.mockResolvedValueOnce(makeFetchResponse('', 404)); // bundled miss

0 commit comments

Comments
 (0)