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:
2026-09-16 23:25:21 -07:00
parent 93fb4eff3a
commit ca40cd6267
23 changed files with 653 additions and 149 deletions
+44
View File
@@ -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 });
+19 -6
View File
@@ -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;
}