From 4317380137f59d0932f0454cf803aa938ef1685e Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 12:13:17 +0200 Subject: [PATCH] fix(stalker): rediscover after benign html endpoints --- .../stalker-endpoint-resolver.spec.ts | 74 +++++++++++++++++-- .../stalker-endpoint-resolver.ts | 22 ++++++ 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.spec.ts b/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.spec.ts index 248a8e548..527f91c49 100644 --- a/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.spec.ts +++ b/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.spec.ts @@ -335,6 +335,60 @@ describe('StalkerEndpointResolver', () => { ).toHaveLength(1); }); + it('advances after a benign HTML endpoint-shape miss and promotes the next valid endpoint', async () => { + const harness = createHarness((request) => { + const action = request.params?.['action']; + if (request.mode === STALKER_HTTP_REQUEST_MODES.Anonymous) { + return success({}, { finalUrl: request.url }); + } + if ( + request.url.endsWith('/server/load.php') && + action === 'handshake' + ) { + return rawSuccess( + 'Portal landing', + { + contentType: 'text/html; charset=utf-8', + finalUrl: request.url, + } + ); + } + if (action === 'handshake') { + return success( + { js: { token: 'html-fallback-token' } }, + { finalUrl: request.url } + ); + } + if (action === 'get_profile') { + return success( + { js: { status: 0 } }, + { finalUrl: request.url } + ); + } + throw new Error('Unexpected request'); + }); + + const outcome = await harness.resolver.resolve({ + descriptor: descriptor(), + transport, + }); + + expect(outcome).toMatchObject({ + endpoint: 'https://portal.test/portal.php', + kind: 'full-session', + }); + expect( + harness.calls.filter( + (request) => request.params?.['action'] === 'handshake' + ) + ).toHaveLength(2); + expect( + harness.calls.some( + (request) => request.params?.['action'] === 'get_genres' + ) + ).toBe(false); + }); + it('continues after early stateless evidence so a later full endpoint wins', async () => { const harness = createHarness((request) => { const action = request.params?.['action']; @@ -479,13 +533,19 @@ describe('StalkerEndpointResolver', () => { handshakeUrls.push(request.url); } if (request.url.includes('/learned/')) { - return success( - {}, - { - finalUrl: request.url, - status: unsupportedStatus, - } - ); + return unsupportedStatus === 404 + ? rawSuccess('Not found', { + contentType: 'text/html', + finalUrl: request.url, + status: unsupportedStatus, + }) + : success( + {}, + { + finalUrl: request.url, + status: unsupportedStatus, + } + ); } if (action === 'handshake') { return success( diff --git a/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.ts b/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.ts index 0b43ab8c1..065d12086 100644 --- a/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.ts +++ b/apps/electron-backend/src/app/services/stalker-session/stalker-endpoint-resolver.ts @@ -470,6 +470,9 @@ function normalizeProtocolOutcome( return failure(STALKER_FAILURE_REASONS.IncompatibleResponse, false); } if (parsed.kind === 'unparsed') { + if (isBenignHtmlEndpointMiss(outcome.result, parsed.rawBody)) { + return { kind: 'miss' }; + } return failure( parsed.reason, parsed.reason !== STALKER_FAILURE_REASONS.IncompatibleResponse @@ -522,6 +525,25 @@ function parseTransportEnvelope( : { kind: 'unparsed', rawBody, reason: parsed.reason }; } +function isBenignHtmlEndpointMiss( + result: StalkerTransportResult, + rawBody: string | undefined +): boolean { + const mediaType = result.contentType + ?.split(';', 1)[0] + ?.trim() + .toLowerCase(); + if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml') { + return false; + } + const prefix = rawBody?.trimStart().slice(0, 64).toLowerCase() ?? ''; + return ( + prefix.startsWith('