fix(stalker): rediscover after benign html endpoints

This commit is contained in:
4gray committed 2026-07-27 12:13:17 +02:00
1 parent c1f7306ed5
commit 4317380137
2 files changed
+89 -7

No files matched your search

@@ -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(
'<!doctype html><html><body>Portal landing</body></html>',
{
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('<html><body>Not found</body></html>', {
contentType: 'text/html',
finalUrl: request.url,
status: unsupportedStatus,
})
: success(
{},
{
finalUrl: request.url,
status: unsupportedStatus,
}
);
}
if (action === 'handshake') {
return success(
@@ -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<Uint8Array>,
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('<!doctype html') ||
prefix.startsWith('<html') ||
prefix.startsWith('<?xml')
);
}
function boundedNonEmptyString(value: unknown): string | undefined {
if (
typeof value !== 'string' ||