Skip to content

Commit adc92f7

Browse files
VinciGit00claude
andcommitted
fix(fetch): surface HTTP errors and missing content instead of answering NA
A page that could not be scraped as intended was indistinguishable from one that could. FetchNode's default path (ChromiumLoader -> ascrape_playwright) dropped the Response returned by page.goto(), so a 404, 403, 500, captcha wall or login redirect reached the LLM as ordinary content and the model answered "NA" with nothing in the logs to explain why. Reported in #1102, where en.wikipedia.org/wiki/Timpson_(company) 404s (the article is at Timpson_(retailer)) and the run still looked clean. Two deterministic, LLM-free guards, both warnings so existing behaviour is unchanged for anyone deliberately scraping error pages: - ChromiumLoader keeps the Response from every page.goto() call site (ascrape_playwright, ascrape_playwright_scroll, ascrape_with_js_support) and warns on status >= 400. This mirrors what the opt-in use_soup=True path in FetchNode has always done. - ParseNode warns when the parsed content contains none of the terms the user asked about — schema field names plus the significant words of the prompt. A 200 response can still reach the LLM without the requested data: content behind JavaScript that never rendered, a field inside a <script> blob the parser drops, or a document truncated beyond the model window. Zero matches is a deliberately conservative bar, so the warning stays quiet when the page simply phrases the answer differently. The graphs now pass their schema to ParseNode so it has the field names available. Verified against the URLs from the issue: the 404 now logs "Received HTTP 404 for .../Timpson_(company); the scraped content is likely an error page" before returning NA, while the corrected URL stays silent and answers 1865. Also drops three dead imports from smart_scraper_multi_batch_graph.py, which ruff blocks on now that the file is touched. Fixes #1102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 083c54f commit adc92f7

12 files changed

Lines changed: 640 additions & 11 deletions

.github/workflows/test-suite.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
tests/test_batch_api.py
4343
tests/test_csv_scraper_multi_graph.py
4444
tests/test_depth_search_graph.py
45+
tests/test_error_page_detection.py
4546
tests/test_json_scraper_graph.py
4647
tests/test_minimax_models.py
4748
tests/test_scrape_do.py

scrapegraphai/docloaders/chromium.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@
1010
logger = get_logger("web-loader")
1111

1212

13+
def _warn_on_error_status(response: Any, url: str) -> None:
14+
"""Log a warning when a navigation returned an HTTP error status.
15+
16+
Playwright's ``page.goto()`` returns the main-frame ``Response``, but the
17+
scrapers only keep ``page.content()``. Without this check an error page
18+
(404, 403, 500, a captcha wall, a login redirect) is indistinguishable
19+
from the intended document once it reaches the LLM, which then produces a
20+
confidently wrong answer with no signal that anything went wrong.
21+
22+
This mirrors the behaviour of the ``use_soup=True`` path in ``FetchNode``:
23+
it warns rather than raising, so scraping error pages on purpose keeps
24+
working.
25+
26+
Args:
27+
response: The ``Response`` returned by ``page.goto()``; may be ``None``
28+
(for example on a same-document navigation) or lack a usable status.
29+
url: The URL that was requested, used in the warning message.
30+
"""
31+
status = getattr(response, "status", None)
32+
if isinstance(status, int) and status >= 400:
33+
logger.warning(
34+
f"Received HTTP {status} for {url}; the scraped content is likely "
35+
"an error page, not the intended document."
36+
)
37+
38+
1339
class ChromiumLoader:
1440
"""Scrapes HTML pages from URLs using a (headless) instance of the
1541
Chromium web driver with proxy protection.
@@ -251,7 +277,8 @@ async def ascrape_playwright_scroll(
251277
context = await browser.new_context()
252278
await Malenia.apply_stealth(context)
253279
page = await context.new_page()
254-
await page.goto(url, wait_until="domcontentloaded")
280+
response = await page.goto(url, wait_until="domcontentloaded")
281+
_warn_on_error_status(response, url)
255282
await page.wait_for_load_state(self.load_state)
256283

257284
previous_height = None
@@ -364,7 +391,8 @@ async def ascrape_playwright(self, url: str, browser_name: str = "chromium") ->
364391
)
365392
await Malenia.apply_stealth(context)
366393
page = await context.new_page()
367-
await page.goto(url, wait_until="domcontentloaded")
394+
response = await page.goto(url, wait_until="domcontentloaded")
395+
_warn_on_error_status(response, url)
368396
await page.wait_for_load_state(self.load_state)
369397
results = await page.content()
370398
logger.info("Content scraped")
@@ -421,7 +449,8 @@ async def ascrape_with_js_support(
421449
storage_state=self.storage_state
422450
)
423451
page = await context.new_page()
424-
await page.goto(url, wait_until="networkidle")
452+
response = await page.goto(url, wait_until="networkidle")
453+
_warn_on_error_status(response, url)
425454
results = await page.content()
426455
logger.info("Content scraped after JavaScript rendering")
427456
return results

scrapegraphai/graphs/code_generator_graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ def _create_graph(self) -> BaseGraph:
9393
parse_node = ParseNode(
9494
input="doc",
9595
output=["parsed_doc"],
96-
node_config={"llm_model": self.llm_model, "chunk_size": self.model_token},
96+
node_config={
97+
"llm_model": self.llm_model,
98+
"chunk_size": self.model_token,
99+
"schema": self.schema,
100+
},
97101
)
98102

99103
generate_validation_answer_node = GenerateAnswerNode(

scrapegraphai/graphs/document_scraper_graph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def _create_graph(self) -> BaseGraph:
7676
"parse_html": False,
7777
"chunk_size": self.model_token,
7878
"llm_model": self.llm_model,
79+
"schema": self.schema,
7980
},
8081
)
8182
generate_answer_node = GenerateAnswerNode(

scrapegraphai/graphs/omni_scraper_graph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def _create_graph(self) -> BaseGraph:
8383
"chunk_size": self.model_token,
8484
"parse_urls": True,
8585
"llm_model": self.llm_model,
86+
"schema": self.schema,
8687
},
8788
)
8889

scrapegraphai/graphs/script_creator_graph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def _create_graph(self) -> BaseGraph:
8282
"chunk_size": self.model_token,
8383
"parse_html": False,
8484
"llm_model": self.llm_model,
85+
"schema": self.schema,
8586
},
8687
)
8788

scrapegraphai/graphs/smart_scraper_graph.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ def _create_graph(self) -> BaseGraph:
110110
parse_node = ParseNode(
111111
input="doc",
112112
output=["parsed_doc"],
113-
node_config={"llm_model": self.llm_model, "chunk_size": self.model_token},
113+
node_config={
114+
"llm_model": self.llm_model,
115+
"chunk_size": self.model_token,
116+
"schema": self.schema,
117+
},
114118
)
115119

116120
generate_answer_node = GenerateAnswerNode(
@@ -152,6 +156,7 @@ def _create_graph(self) -> BaseGraph:
152156
node_config={
153157
"llm_model": self.llm_model,
154158
"chunk_size": self.model_token,
159+
"schema": self.schema,
155160
},
156161
)
157162

scrapegraphai/graphs/smart_scraper_lite_graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ def _create_graph(self) -> BaseGraph:
7474
parse_node = ParseNode(
7575
input="doc",
7676
output=["parsed_doc"],
77-
node_config={"llm_model": self.llm_model, "chunk_size": self.model_token},
77+
node_config={
78+
"llm_model": self.llm_model,
79+
"chunk_size": self.model_token,
80+
"schema": self.schema,
81+
},
7882
)
7983

8084
return BaseGraph(

scrapegraphai/graphs/smart_scraper_multi_batch_graph.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
providing 50% cost savings compared to real-time API calls.
66
"""
77

8-
import asyncio
98
from copy import deepcopy
10-
from typing import Dict, List, Optional, Type
9+
from typing import List, Optional, Type
1110

1211
from pydantic import BaseModel
1312

@@ -17,7 +16,6 @@
1716
from ..utils.copy import safe_deepcopy
1817
from .abstract_graph import AbstractGraph
1918
from .base_graph import BaseGraph
20-
from .smart_scraper_graph import SmartScraperGraph
2119

2220

2321
class _FetchParseOnlyGraph(AbstractGraph):
@@ -57,6 +55,7 @@ def _create_graph(self) -> BaseGraph:
5755
node_config={
5856
"llm_model": self.llm_model,
5957
"chunk_size": self.model_token,
58+
"schema": self.schema,
6059
},
6160
)
6261

scrapegraphai/graphs/speech_graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def _create_graph(self) -> BaseGraph:
7070
parse_node = ParseNode(
7171
input="doc",
7272
output=["parsed_doc"],
73-
node_config={"chunk_size": self.model_token, "llm_model": self.llm_model},
73+
node_config={
74+
"chunk_size": self.model_token,
75+
"llm_model": self.llm_model,
76+
"schema": self.schema,
77+
},
7478
)
7579

7680
generate_answer_node = GenerateAnswerNode(

0 commit comments

Comments
 (0)