Navigation Error Handling — Fast-Fail bei 404/500 + Retry mit Backoff + Challenge-Detection #114

Closed
opened 2026-06-19 20:55:56 +00:00 by Artur · 0 comments
Owner

Problembeschreibung

Aktuell macht page.goto() keinen Unterschied zwischen 200 OK, 404 Not Found, 500 Server Error oder Timeout. Bei 5XX oder Timeout wartet der Stabilizer bis zum Timeout (30s) — sinnlos.

Beispiel: page.goto('https://example.com/nicht-existente-seite') → HTTP 404 → response.text() gibt leeren String → Parser parsed nichts → Stabilizer wartet 30s → Fehlschlag.

Aktueller Flow (schlecht)

goto(url)
 → fetch(url) → response.text() → parser.start(html)
 → EventLoop.waitForStable()  ← hängt bei 404/500 30s

Gewünschter Flow

goto(url)
 → fetch(url)
 → status >= 400? → throw NavigationError (fast-fail, ~500ms)
 → status 200-399 → response.text() → parser.start(html)
 → EventLoop.waitForStable() ← nur bei validem HTML

Option A: Fast-Fail bei HTTP Error

class NavigationError extends Error {
  url: string;
  statusCode: number;
  statusText: string;
  headers: Record<string, string>;
}

page.onNavigationError = (err: NavigationError) => {
  if (err.statusCode === 404) return 'continue'; // manche 404 haben Content
  if (err.statusCode >= 500) return 'abort';     // 5XX never has content
  return 'abort';
};

Option B: Retry mit Backoff

class Page {
  async goto(url, opts?: {
    retryCount?: 2,
    retryDelay?: 1000,        // ms before first retry
    retryBackoff?: 'linear' | 'exponential',
    retryOnStatus?: [429, 503, 502],
    fastFailStatus?: [404, 410, 500],
  }): Promise<void> {
    // ...
  }
}

Direkt in goto() eingebaut: Retry-Logic + Fast-Fail + Timeout-Kontrolle.

Option C: Challenge-Detection + Auto-Proxy-Wechsel

Erweiterung von B:

const page = new Page({
  retry: {
    maxRetries: 3,
    backoff: 'exponential',
    onBlocked: (url, status) => {
      proxyPool.rotate();
      return true; // retry with new proxy
    },
  },
});

Empfehlung: Option B (Retry mit Backoff) als erste Phase. Option C später.

Akzeptanzkriterien (Phase 1 — Fast-Fail)

  • NavigationError Klasse mit url, statusCode, statusText
  • goto() wirft NavigationError bei status >= 400 innerhalb von 1s
  • Konfigurierbar via goto(url, { failOnStatus: [404, 500, 502, 503] })
  • Leerer Body bei Fast-Fail (kein Parser-Start)
  • Test: 404 → NavigationError in <1s
  • Test: 500 → NavigationError in <1s
  • Test: 200 → kein Error (normaler Flow)
  • Test: 301 redirect → kein Error (followed redirect)

Akzeptanzkriterien (Phase 2 — Retry)

  • goto(url, { retry: { max: 3, delay: 1000, backoff: 'exponential' } })
  • Retry nur bei konfigurierten Status (default: [429, 502, 503])
  • Exponential Backoff: 1s, 2s, 4s
  • onRetry Callback: (attempt, error) => boolean
  • Alle Retries erschöpft → finaler NavigationError
  • Test: 503 → retry 3x → final error
  • Test: 503 → retry 2x → 200 OK → success
  • Test: Backoff wartet korrekt

Akzeptanzkriterien (Phase 3 — Challenge)

  • Challenge-Detection: AWS WAF, Cloudflare, Turnstile
  • onChallenge Callback: (type, url) => Promise
  • Auto-Proxy-Wechsel bei Challenge
  • Test: Challenge-Seite → retry mit neuem Proxy

Betroffene Dateien

Datei Änderung Aufwand
src/pages/page.ts goto() refactorn: Status-Check vor Parser ~30 Zeilen
src/pages/page.ts NavigationError Klasse + Retry-Logic ~50 Zeilen
src/pages/page.ts Challenge-Detection Hook ~20 Zeilen
src/pages/stabilizer.ts Fast-Fail: Stabilizer abbrechen bei Error ~10 Zeilen
tests/unit/navigation-error.test.ts Neu: 20 Tests ~400 Zeilen

Risiken

Risk Impact Mitigation
404 mit Content (custom 404 pages) Mittel Option A: onNavigationError erlaubt continue
Redirect-Zyklen (301 → 301 → 301) Niedrig Max 10 redirects, dann Error
Retry-Loop bei dauerhaftem Error Hoch MaxRetries-Hardcap bei 5
Timeout < 500ms bei langsamen Verbindungen Mittel Configurable timeout, default 30s

Testplan (20 Tests)

# Test Phase
NE01 NavigationError bei 404 1
NE02 NavigationError bei 500 1
NE03 Kein Error bei 200 1
NE04 Kein Error bei 301 1
NE05 failOnStatus Array filtert richtig 1
NE06 Fast-Fail < 1s 1
NE07 Retry bei 503 → final error 2
NE08 Retry bei 503 → 200 OK 2
NE09 Exponential Backoff Timing 2
NE10 onRetry Callback 2
NE11 MaxRetries Hardcap 2
NE12 Kein Retry bei 200 2
NE13 Challenge-Detection AWS WAF 3
NE14 Challenge-Detection Cloudflare 3
NE15 Challenge → Proxy-Rotation 3
NE16 onChallenge Callback 3
NE17 Timeout < 30s bei Hanging 1
NE18 Redirect-Zyklus erkennen 1
NE19 Leerer Body bei Fast-Fail 1
NE20 Concurrent Retry Safety 2
## Problembeschreibung Aktuell macht `page.goto()` **keinen Unterschied** zwischen 200 OK, 404 Not Found, 500 Server Error oder Timeout. Bei 5XX oder Timeout wartet der Stabilizer bis zum Timeout (30s) — sinnlos. Beispiel: `page.goto('https://example.com/nicht-existente-seite')` → HTTP 404 → `response.text()` gibt leeren String → Parser parsed nichts → Stabilizer wartet 30s → Fehlschlag. ### Aktueller Flow (schlecht) ``` goto(url) → fetch(url) → response.text() → parser.start(html) → EventLoop.waitForStable() ← hängt bei 404/500 30s ``` ### Gewünschter Flow ``` goto(url) → fetch(url) → status >= 400? → throw NavigationError (fast-fail, ~500ms) → status 200-399 → response.text() → parser.start(html) → EventLoop.waitForStable() ← nur bei validem HTML ``` ## Option A: Fast-Fail bei HTTP Error ```ts class NavigationError extends Error { url: string; statusCode: number; statusText: string; headers: Record<string, string>; } page.onNavigationError = (err: NavigationError) => { if (err.statusCode === 404) return 'continue'; // manche 404 haben Content if (err.statusCode >= 500) return 'abort'; // 5XX never has content return 'abort'; }; ``` ## Option B: Retry mit Backoff ```ts class Page { async goto(url, opts?: { retryCount?: 2, retryDelay?: 1000, // ms before first retry retryBackoff?: 'linear' | 'exponential', retryOnStatus?: [429, 503, 502], fastFailStatus?: [404, 410, 500], }): Promise<void> { // ... } } ``` Direkt in goto() eingebaut: Retry-Logic + Fast-Fail + Timeout-Kontrolle. ## Option C: Challenge-Detection + Auto-Proxy-Wechsel Erweiterung von B: ```ts const page = new Page({ retry: { maxRetries: 3, backoff: 'exponential', onBlocked: (url, status) => { proxyPool.rotate(); return true; // retry with new proxy }, }, }); ``` **Empfehlung: Option B** (Retry mit Backoff) als erste Phase. Option C später. ## Akzeptanzkriterien (Phase 1 — Fast-Fail) - [ ] `NavigationError` Klasse mit url, statusCode, statusText - [ ] `goto()` wirft NavigationError bei status >= 400 innerhalb von 1s - [ ] Konfigurierbar via `goto(url, { failOnStatus: [404, 500, 502, 503] })` - [ ] Leerer Body bei Fast-Fail (kein Parser-Start) - [ ] Test: 404 → NavigationError in <1s - [ ] Test: 500 → NavigationError in <1s - [ ] Test: 200 → kein Error (normaler Flow) - [ ] Test: 301 redirect → kein Error (followed redirect) ## Akzeptanzkriterien (Phase 2 — Retry) - [ ] `goto(url, { retry: { max: 3, delay: 1000, backoff: 'exponential' } })` - [ ] Retry nur bei konfigurierten Status (default: [429, 502, 503]) - [ ] Exponential Backoff: 1s, 2s, 4s - [ ] `onRetry` Callback: (attempt, error) => boolean - [ ] Alle Retries erschöpft → finaler NavigationError - [ ] Test: 503 → retry 3x → final error - [ ] Test: 503 → retry 2x → 200 OK → success - [ ] Test: Backoff wartet korrekt ## Akzeptanzkriterien (Phase 3 — Challenge) - [ ] Challenge-Detection: AWS WAF, Cloudflare, Turnstile - [ ] `onChallenge` Callback: (type, url) => Promise<boolean> - [ ] Auto-Proxy-Wechsel bei Challenge - [ ] Test: Challenge-Seite → retry mit neuem Proxy ## Betroffene Dateien | Datei | Änderung | Aufwand | |---|---|---| | `src/pages/page.ts` | `goto()` refactorn: Status-Check vor Parser | ~30 Zeilen | | `src/pages/page.ts` | NavigationError Klasse + Retry-Logic | ~50 Zeilen | | `src/pages/page.ts` | Challenge-Detection Hook | ~20 Zeilen | | `src/pages/stabilizer.ts` | Fast-Fail: Stabilizer abbrechen bei Error | ~10 Zeilen | | `tests/unit/navigation-error.test.ts` | Neu: 20 Tests | ~400 Zeilen | ## Risiken | Risk | Impact | Mitigation | |---|---|---| | 404 mit Content (custom 404 pages) | Mittel | Option A: `onNavigationError` erlaubt continue | | Redirect-Zyklen (301 → 301 → 301) | Niedrig | Max 10 redirects, dann Error | | Retry-Loop bei dauerhaftem Error | Hoch | MaxRetries-Hardcap bei 5 | | Timeout < 500ms bei langsamen Verbindungen | Mittel | Configurable timeout, default 30s | ## Testplan (20 Tests) | # | Test | Phase | |---|---|---| | NE01 | NavigationError bei 404 | 1 | | NE02 | NavigationError bei 500 | 1 | | NE03 | Kein Error bei 200 | 1 | | NE04 | Kein Error bei 301 | 1 | | NE05 | `failOnStatus` Array filtert richtig | 1 | | NE06 | Fast-Fail < 1s | 1 | | NE07 | Retry bei 503 → final error | 2 | | NE08 | Retry bei 503 → 200 OK | 2 | | NE09 | Exponential Backoff Timing | 2 | | NE10 | `onRetry` Callback | 2 | | NE11 | MaxRetries Hardcap | 2 | | NE12 | Kein Retry bei 200 | 2 | | NE13 | Challenge-Detection AWS WAF | 3 | | NE14 | Challenge-Detection Cloudflare | 3 | | NE15 | Challenge → Proxy-Rotation | 3 | | NE16 | `onChallenge` Callback | 3 | | NE17 | Timeout < 30s bei Hanging | 1 | | NE18 | Redirect-Zyklus erkennen | 1 | | NE19 | Leerer Body bei Fast-Fail | 1 | | NE20 | Concurrent Retry Safety | 2 |
Artur closed this issue 2026-06-19 21:27:04 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#114
No description provided.