Skip to content

Commit dec4302

Browse files
author
dev-pd
committed
Degrade gracefully on blocked page: SERP-only audit + clean note, no 502
1 parent 3f42f9f commit dec4302

7 files changed

Lines changed: 64 additions & 19 deletions

File tree

backend/app/api/run.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ class RunRequest(BaseModel):
3232
class RunResponse(BaseModel):
3333
a: AgentRunResult
3434
b: AgentRunResult
35+
note: str | None = (
36+
None # set when the page body couldn't be fetched (SERP-only run)
37+
)
3538

3639

3740
@router.post("", response_model=RunResponse)
3841
async def run(req: RunRequest) -> RunResponse:
3942
try:
40-
# Fail fast if the page can't be fetched — don't burn LLM tokens first.
41-
await FetchPageTool(replay=False).run(FetchPageInput(url=req.url))
43+
# Probe the page once. A bot-block no longer fails the run — fetch_page degrades
44+
# to a SERP-only audit and tells the user via `note`.
45+
probe = await FetchPageTool(replay=False).run(FetchPageInput(url=req.url))
4246
a = await run_agent_a(
4347
req.url,
4448
req.keyword,
@@ -57,24 +61,17 @@ async def run(req: RunRequest) -> RunResponse:
5761
)
5862
# …then consolidate the diary into the playbook so the NEXT run sees it.
5963
await run_janitor()
60-
except httpx.HTTPStatusError as exc:
61-
code = exc.response.status_code
62-
raise HTTPException(
63-
status_code=502,
64-
detail=(
65-
f"Couldn't fetch {req.url} (HTTP {code}). The site likely blocks "
66-
"automated requests (bot protection). Try a different page."
67-
),
68-
) from exc
6964
except httpx.HTTPError as exc:
65+
# A page bot-block degrades gracefully (see fetch_page); this only fires for
66+
# other live HTTP failures, e.g. the SERP call.
7067
raise HTTPException(
7168
status_code=502,
72-
detail=f"Couldn't reach {req.url}: {exc.__class__.__name__}. Try a different page.",
69+
detail=f"Live request failed: {exc.__class__.__name__}. Try again or a different page.",
7370
) from exc
7471
except ReplayMissError as exc:
7572
raise HTTPException(status_code=409, detail=str(exc)) from exc
7673
except RuntimeError as exc:
7774
# e.g. live SERP needs SERP_API_KEY
7875
raise HTTPException(status_code=502, detail=str(exc)) from exc
7976

80-
return RunResponse(a=a, b=b)
77+
return RunResponse(a=a, b=b, note=None if probe.fetch_ok else probe.fetch_note)

backend/app/models/page.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class Page(BaseModel):
2020
meta_description: str | None = None
2121
headings: list[Heading] = Field(default_factory=list)
2222
body_text: str = ""
23+
# False when the body couldn't be fetched (e.g. bot-blocked 403). On-page checks
24+
# are skipped in that case so we never hallucinate defects from an empty page; the
25+
# SERP-driven stages (keyword, rank, gap) still run.
26+
fetch_ok: bool = True
27+
fetch_note: str = ""
2328

2429
@property
2530
def word_count(self) -> int:

backend/app/tools/fetch_page.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,33 @@ async def run(self, payload: FetchPageInput) -> Page:
5757
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
5858
"Accept-Language": "en-US,en;q=0.9",
5959
}
60-
async with httpx.AsyncClient(
61-
follow_redirects=True, timeout=20.0, headers=headers
62-
) as client:
63-
response = await client.get(payload.url)
64-
response.raise_for_status()
60+
try:
61+
async with httpx.AsyncClient(
62+
follow_redirects=True, timeout=20.0, headers=headers
63+
) as client:
64+
response = await client.get(payload.url)
65+
response.raise_for_status()
66+
except httpx.HTTPStatusError as exc:
67+
# Bot-blocked or missing page: degrade rather than fail the whole audit.
68+
# Not saved — we never want a failure cached as the page snapshot.
69+
return Page(
70+
url=payload.url,
71+
fetch_ok=False,
72+
fetch_note=(
73+
f"Couldn't read the page body (HTTP {exc.response.status_code} — the "
74+
"site blocks automated requests). On-page checks skipped; SERP-based "
75+
"analysis still ran."
76+
),
77+
)
78+
except httpx.HTTPError as exc:
79+
return Page(
80+
url=payload.url,
81+
fetch_ok=False,
82+
fetch_note=(
83+
f"Couldn't reach the page ({exc.__class__.__name__}). On-page checks "
84+
"skipped; SERP-based analysis still ran."
85+
),
86+
)
6587
page = extract_page(payload.url, response.text)
6688
await self._pages.save(page, payload.version)
6789
return page

backend/app/tools/seo_checks.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ def check_has_comparison_table(page: Page, keyword: str) -> CheckResult:
121121
def run_checks(
122122
page: Page, keyword: str, names: list[str] | None = None
123123
) -> list[CheckResult]:
124-
"""Run the named checks (or all of them) against a page."""
124+
"""Run the named checks (or all of them) against a page.
125+
126+
If the page body couldn't be fetched, return nothing: on-page checks on an empty
127+
page would invent defects (missing title, thin content) that aren't real.
128+
"""
129+
if not page.fetch_ok:
130+
return []
125131
selected = names or list(ALL_CHECKS)
126132
return [ALL_CHECKS[name](page, keyword) for name in selected]
127133

frontend/src/App.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@
7575
margin: 0.25rem 0 0;
7676
}
7777

78+
.notice {
79+
border: 1px solid #f5d9a8;
80+
background: #fdf6e8;
81+
border-radius: 8px;
82+
padding: 0.6rem 1rem;
83+
margin: 0.5rem 0 1rem;
84+
color: #7a5a12;
85+
font-size: 0.9rem;
86+
}
87+
7888
.summary {
7989
border: 1px solid var(--border);
8090
border-radius: 8px;

frontend/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ function AuditPage() {
6969
</div>
7070
)}
7171

72+
{run.status === 'done' && run.result.note && (
73+
<div className="notice">{run.result.note}</div>
74+
)}
75+
7276
{run.status === 'done' && (
7377
<main className="split">
7478
<AgentColumn view={run.result.a} />

frontend/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface AgentRunResult {
3535
export interface RunResponse {
3636
a: AgentRunResult
3737
b: AgentRunResult
38+
note?: string | null // set when the page body couldn't be fetched (SERP-only run)
3839
}
3940

4041
export interface PlaybookEntry {

0 commit comments

Comments
 (0)