mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
fix(anime): clarify bridge failures and update guidance
- Show actionable bridge errors and preserve episodes when details fail - Check external bridges for updates without offering unsafe in-app installs - Complete cleanup and ignore callbacks from closed Tsukihime sessions
This commit is contained in:
@@ -168,6 +168,50 @@ test('searchAnime sends a 1-based page and returns the page payload', async () =
|
||||
assert.equal(page.animes?.length, 1);
|
||||
});
|
||||
|
||||
test('HTTP failures preserve the bridge error and status for diagnosis', async () => {
|
||||
for (const detail of [
|
||||
"'java.lang.Object eu.kanade.tachiyomi.animesource.online.AnimeHttpSource.getHosterList(eu.kanade.tachiyomi.animesource.model.SEpisode, kotlin.coroutines.Continuation)'",
|
||||
'lateinit property url has not been initialized',
|
||||
]) {
|
||||
const { fetchImpl } = stubFetch(
|
||||
() => new Response(JSON.stringify({ error: detail, code: 500 }), { status: 500 }),
|
||||
);
|
||||
const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', fetchImpl });
|
||||
await assert.rejects(
|
||||
() => client.getVideoList(source, '/episode/301'),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof BridgeExtensionError);
|
||||
assert.equal(error.code, 500);
|
||||
assert.equal(error.message, `Anime bridge getVideoList failed (500). ${detail}`);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('non-JSON and invalid bridge errors keep the HTTP fallback without exposing response bodies', async () => {
|
||||
for (const body of ['<html>Proxy error</html>', '', '{"error":{}}', '{"error":" "}', 'null']) {
|
||||
const { fetchImpl } = stubFetch(() => new Response(body, { status: 502 }));
|
||||
const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', fetchImpl });
|
||||
await assert.rejects(() => client.getAnimeDetails(source, '/anime/1'), {
|
||||
message: 'Anime bridge getDetailsAnime failed (502).',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('bridge diagnostics normalize whitespace and bound long messages', async () => {
|
||||
const { fetchImpl } = stubFetch(
|
||||
() =>
|
||||
new Response(JSON.stringify({ error: ` Missing field\n\t${'x'.repeat(3_000)}` }), {
|
||||
status: 500,
|
||||
}),
|
||||
);
|
||||
const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', fetchImpl });
|
||||
await assert.rejects(() => client.getAnimeDetails(source, '/anime/1'), {
|
||||
message: `Anime bridge getDetailsAnime failed (500). ${`Missing field ${'x'.repeat(3_000)}`.slice(0, 1_999)}…`,
|
||||
});
|
||||
});
|
||||
|
||||
test('getEpisodeList wraps the anime url in animeData', async () => {
|
||||
const { fetchImpl, calls } = stubFetch(() => jsonResponse([{ name: 'Episode 1', url: '/ep/1' }]));
|
||||
const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', fetchImpl });
|
||||
|
||||
@@ -45,7 +45,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
||||
/** The readiness probe is a local health check; it should answer at once. */
|
||||
const CAPABILITIES_TIMEOUT_MS = 5_000;
|
||||
|
||||
/** The bridge reports extension failures as HTTP 200 with an error body. */
|
||||
/** Extension failures may arrive as HTTP errors or HTTP 200 with an error body. */
|
||||
export class BridgeExtensionError extends Error {
|
||||
readonly code?: number;
|
||||
constructor(message: string, code?: number) {
|
||||
@@ -194,7 +194,12 @@ export class AnimeBridgeClient {
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Anime bridge ${method} failed (${response.status}).`);
|
||||
const body: unknown = await response.json().catch(() => null);
|
||||
const detail = extensionErrorMessage(body);
|
||||
throw new BridgeExtensionError(
|
||||
`Anime bridge ${method} failed (${response.status}).${detail ? ` ${detail}` : ''}`,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
const returnedId = response.headers.get(EXTENSION_ID_HEADER)?.trim();
|
||||
@@ -234,12 +239,20 @@ export class AnimeBridgeClient {
|
||||
}
|
||||
|
||||
function assertNoExtensionError(body: unknown, method: string): void {
|
||||
if (body === null || typeof body !== 'object' || Array.isArray(body)) return;
|
||||
const error = (body as { error?: unknown }).error;
|
||||
if (typeof error !== 'string') return;
|
||||
const code = (body as { code?: unknown }).code;
|
||||
const error = extensionErrorMessage(body);
|
||||
if (error === null) return;
|
||||
const code = body !== null && typeof body === 'object' && 'code' in body ? body.code : undefined;
|
||||
throw new BridgeExtensionError(
|
||||
`Anime bridge ${method} failed: ${error}`,
|
||||
typeof code === 'number' ? code : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** Only expose the bridge's JSON error field, never an HTML error page or stack object. */
|
||||
function extensionErrorMessage(body: unknown): string | null {
|
||||
if (body === null || typeof body !== 'object' || Array.isArray(body)) return null;
|
||||
if (!('error' in body) || typeof body.error !== 'string') return null;
|
||||
const message = body.error.replace(/\s+/g, ' ').trim();
|
||||
if (!message) return null;
|
||||
return message.length > 2_000 ? `${message.slice(0, 1_999)}…` : message;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user