Skip to content

Commit 7e80152

Browse files
authored
Merge pull request #2077 from unclecode/release/v0.9.2
release: Crawl4AI v0.9.2
2 parents bd8b130 + 055e2ec commit 7e80152

15 files changed

Lines changed: 472 additions & 28 deletions

File tree

CONTRIBUTORS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
We would like to thank the following people for their contributions to Crawl4AI:
44

5-
## Core Team
5+
## Maintainers
66

77
- [Unclecode](https://github.com/unclecode) - Project Creator and Main Developer
88
- [Nasrin](https://github.com/ntohidi) - Project Manager and Developer
@@ -58,9 +58,11 @@ We would like to thank the following people for their contributions to Crawl4AI:
5858
- [microHoffman](https://github.com/microHoffman) - feat: add --json-ensure-ascii CLI flag for Unicode handling [#1668](https://github.com/unclecode/crawl4ai/pull/1668)
5959
- [fstark96](https://github.com/fstark96) - fix: skip channel='chromium' on Windows to prevent Chrome exit code 0 [#2051](https://github.com/unclecode/crawl4ai/pull/2051)
6060
- [TobiasWallura-xitaso](https://github.com/TobiasWallura-xitaso) - fix: use writable directories for supervisord pid and redis data in Docker [#2047](https://github.com/unclecode/crawl4ai/pull/2047)
61-
- [harshmathurx](https://github.com/harshmathurx) - fix: authenticate rate limit Redis storage in Docker [#2043](https://github.com/unclecode/crawl4ai/pull/2043)
61+
- [harshmathurx](https://github.com/harshmathurx) - fix: authenticate rate limit Redis storage in Docker [#2043](https://github.com/unclecode/crawl4ai/pull/2043), fix: add non-free apt source for nvidia-cuda-toolkit GPU builds [#2057](https://github.com/unclecode/crawl4ai/pull/2057)
6262
- [RajanChavada](https://github.com/RajanChavada) - chore: remove dead normalize_url definitions and accidental adaptive_crawler copy [#2042](https://github.com/unclecode/crawl4ai/pull/2042)
6363
- [bibi-creator](https://github.com/bibi-creator) - docs: split sponsors into Strategic Partners and Enterprise Sponsors [#2056](https://github.com/unclecode/crawl4ai/pull/2056), fix: update sponsor logos [#2052](https://github.com/unclecode/crawl4ai/pull/2052)
64+
- [reallav0](https://github.com/reallav0) - fix: clean up MemoryAdaptiveDispatcher tasks on stream close [#2072](https://github.com/unclecode/crawl4ai/pull/2072), fix: include Playwright headless shell in Docker image [#2067](https://github.com/unclecode/crawl4ai/pull/2067)
65+
- [Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) - fix: Docker playground config type and monitor WebSocket auth [#2063](https://github.com/unclecode/crawl4ai/pull/2063)
6466

6567
#### Feb-Alpha-1
6668
- [sufianuddin](https://github.com/sufianuddin) - fix: [Documentation for JsonCssExtractionStrategy](https://github.com/unclecode/crawl4ai/issues/651)

Dockerfile

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM python:3.12-slim-bookworm AS build
22

33
# C4ai version
4-
ARG C4AI_VER=0.9.1
4+
ARG C4AI_VER=0.9.2
55
ENV C4AI_VERSION=$C4AI_VER
66
LABEL c4ai.version=$C4AI_VER
77

@@ -90,9 +90,10 @@ RUN apt-get update && apt-get dist-upgrade -y \
9090
&& rm -rf /var/lib/apt/lists/*
9191

9292
RUN if [ "$ENABLE_GPU" = "true" ] && [ "$TARGETARCH" = "amd64" ] ; then \
93-
apt-get update && apt-get install -y --no-install-recommends \
93+
echo "deb http://deb.debian.org/debian bookworm contrib non-free" >> /etc/apt/sources.list \
94+
&& apt-get update && apt-get install -y --no-install-recommends \
9495
nvidia-cuda-toolkit \
95-
&& apt-get clean \
96+
&& apt-get clean \
9697
&& rm -rf /var/lib/apt/lists/* ; \
9798
else \
9899
echo "Skipping NVIDIA CUDA Toolkit installation (unsupported platform or GPU disabled)"; \
@@ -177,7 +178,9 @@ RUN crawl4ai-setup
177178
RUN playwright install --with-deps
178179

179180
RUN mkdir -p /home/appuser/.cache/ms-playwright \
180-
&& cp -r /root/.cache/ms-playwright/chromium-* /home/appuser/.cache/ms-playwright/ \
181+
&& cp -r /root/.cache/ms-playwright/chromium-* \
182+
/root/.cache/ms-playwright/chromium_headless_shell-* \
183+
/home/appuser/.cache/ms-playwright/ \
181184
&& chown -R appuser:appuser /home/appuser/.cache/ms-playwright
182185

183186
RUN crawl4ai-doctor
@@ -226,4 +229,4 @@ ENV PYTHON_ENV=production
226229
# Start via entrypoint.sh, which resolves the socket-level auth/egress posture
227230
# (loopback unless a credential is present) and the redis password, then execs
228231
# supervisord.
229-
CMD ["bash", "entrypoint.sh"]
232+
CMD ["bash", "entrypoint.sh"]

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ Limited slots._
3737

3838
Crawl4AI turns the web into clean, LLM ready Markdown for RAG, agents, and data pipelines. Fast, controllable, battle tested by a 50k+ star community.
3939

40-
[✨ Check out latest update v0.9.1](#-recent-updates)
40+
[✨ Check out latest update v0.9.2](#-recent-updates)
4141

42-
**New in v0.9.1**: Patch release with 12 bug fixes across Docker, browser, and core. Adds `preserve_classes`/`preserve_tags` whitelist for PruningContentFilter, fixes Windows browser crash, Docker auth gate UI, HTTP timeout unit mismatch, and more. [Release notes →](https://github.com/unclecode/crawl4ai/blob/main/docs/blog/release-v0.9.1.md)
42+
**New in v0.9.2**: Maintenance patch release. Fixes a `MemoryAdaptiveDispatcher` task/page leak when a streaming crawl is closed, Docker Playground "Advanced Config" and Monitor WebSocket auth, Playwright headless-shell packaging, and GPU (`ENABLE_GPU=true`) Docker builds. [Release notes →](https://github.com/unclecode/crawl4ai/blob/main/docs/blog/release-v0.9.2.md)
4343

4444
✨ Recent v0.9.0: Major secure-by-default release of the Docker API server. Auth is on by default, the server binds loopback unless given a token, and the request body is now an untrusted trust boundary. [Release notes →](https://github.com/unclecode/crawl4ai/blob/main/docs/blog/release-v0.9.0.md)
4545

@@ -565,6 +565,21 @@ async def test_news_crawl():
565565
## ✨ Recent Updates
566566

567567
<details open>
568+
<summary><strong>Version 0.9.2 Release Highlights - Maintenance Bug Fixes</strong></summary>
569+
570+
A maintenance patch release with bug fixes across the dispatcher, Docker, and GPU builds. `MemoryAdaptiveDispatcher` no longer leaks crawl tasks and browser pages when a streaming crawl is closed. Docker fixes cover the Playground "Advanced Config" 400, the Monitor WebSocket 500 under JWT auth, and Playwright headless-shell packaging. `ENABLE_GPU=true` Docker builds no longer fail on the CUDA toolkit.
571+
572+
No new features, no breaking changes.
573+
574+
```bash
575+
pip install -U crawl4ai
576+
```
577+
578+
[Full v0.9.2 Release Notes →](https://github.com/unclecode/crawl4ai/blob/main/docs/blog/release-v0.9.2.md)
579+
580+
</details>
581+
582+
<details>
568583
<summary><strong>Version 0.9.1 Release Highlights - Bug Fixes & PruningContentFilter Whitelist</strong></summary>
569584

570585
A patch release with 12 bug fixes and one new feature. The new `preserve_classes` / `preserve_tags` parameters for `PruningContentFilter` let you whitelist CSS classes or HTML tags that should never be pruned — useful for protecting short metadata elements like author names and timestamps.

crawl4ai/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# crawl4ai/__version__.py
22

33
# This is the version that will be used for stable releases
4-
__version__ = "0.9.1"
4+
__version__ = "0.9.2"
55

66
# For nightly builds, this gets set during build process
77
__nightly_version__ = None

crawl4ai/async_dispatcher.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ async def run_urls_stream(
534534
config: Union[CrawlerRunConfig, List[CrawlerRunConfig]],
535535
) -> AsyncGenerator[CrawlerTaskResult, None]:
536536
self.crawler = crawler
537+
active_tasks = []
537538

538539
# Start the memory monitor task
539540
memory_monitor = asyncio.create_task(self._memory_monitor_task())
@@ -550,7 +551,6 @@ async def run_urls_stream(
550551
# Add to queue with initial priority 0, retry count 0, and current time
551552
await self.task_queue.put((0, (url, task_id, 0, time.time())))
552553

553-
active_tasks = []
554554
completed_count = 0
555555
total_urls = len(urls)
556556

@@ -614,8 +614,24 @@ async def run_urls_stream(
614614
await self._update_queue_priorities()
615615

616616
finally:
617-
# Clean up
617+
# Cancel and await every task owned by this stream before returning
618+
# control to the caller. Otherwise a closed stream can leave crawls
619+
# using browser pages and contexts in the background.
620+
for task in active_tasks:
621+
if not task.done():
622+
task.cancel()
623+
if active_tasks:
624+
await asyncio.gather(*active_tasks, return_exceptions=True)
625+
626+
# Discard URLs that were queued by this stream but never started.
627+
while True:
628+
try:
629+
self.task_queue.get_nowait()
630+
except asyncio.QueueEmpty:
631+
break
632+
618633
memory_monitor.cancel()
634+
await asyncio.gather(memory_monitor, return_exceptions=True)
619635
if self.monitor:
620636
self.monitor.stop()
621637

@@ -769,4 +785,4 @@ async def run_urls(
769785
return await asyncio.gather(*tasks, return_exceptions=True)
770786
finally:
771787
if self.monitor:
772-
self.monitor.stop()
788+
self.monitor.stop()

deploy/docker/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ Pull and run images directly from Docker Hub without building locally.
5959

6060
#### 1. Pull the Image
6161

62-
Our latest stable release is `0.8.6`. Images are built with multi-arch manifests, so Docker automatically pulls the correct version for your system.
62+
Our latest stable release is `0.9.2`. Images are built with multi-arch manifests, so Docker automatically pulls the correct version for your system.
6363

6464
```bash
65-
# Pull the latest stable version (0.8.6)
65+
# Pull the latest stable version (0.9.2)
6666
docker pull unclecode/crawl4ai:0.8.6
6767

6868
# Or use the latest tag

deploy/docker/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,12 @@ def _config_from_json(data: dict) -> dict:
482482
app.include_router(init_job_router(redis, config, token_dep))
483483

484484
# ── monitor router ──────────────────────────────────────────
485+
# Do not attach token_dep at router level: it is HTTP Request-only and breaks
486+
# the WebSocket upgrade on /monitor/ws (TypeError: _principal() missing 'request').
487+
# AuthGateMiddleware already authenticates HTTP + WS; destructive monitor
488+
# actions keep their own Depends(require_admin).
485489
from monitor_routes import router as monitor_router
486-
app.include_router(monitor_router, dependencies=[Depends(token_dep)])
490+
app.include_router(monitor_router)
487491

488492
logger = logging.getLogger(__name__)
489493

deploy/docker/static/playground/index.html

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,13 @@ <h2 class="font-medium text-accent">🔥 Stress Test</h2>
563563
const code = cm.getValue().trim();
564564
if (!code) return {};
565565

566+
// Server requires `type` alongside `code` (CrawlerRunConfig | BrowserConfig).
567+
// The UI already tracks this in #cfg-type; omitting it always 400s Advanced Config.
568+
const cfgType = document.getElementById('cfg-type').value;
566569
const res = await authFetch('/config/dump', {
567570
method: 'POST',
568571
headers: { 'Content-Type': 'application/json' },
569-
body: JSON.stringify({ code }),
572+
body: JSON.stringify({ type: cfgType, code }),
570573
});
571574

572575
const statusEl = document.getElementById('cfg-status');
@@ -652,9 +655,12 @@ <h2 class="font-medium text-accent">🔥 Stress Test</h2>
652655
// Detect if stream is requested inside payload
653656
function shouldUseStream(payload) {
654657
const toBool = (v) => v === true || (typeof v === 'string' && v.toLowerCase() === 'true');
655-
const fromCrawler = payload && payload.crawler_config && payload.crawler_config.params && payload.crawler_config.params.stream;
658+
// Successful /config/dump returns { type, params: { stream, ... } }
659+
const fromCrawlerParams = payload && payload.crawler_config && payload.crawler_config.params && payload.crawler_config.params.stream;
660+
// Fallback / minimal shapes may set stream at the crawler_config top level
661+
const fromCrawlerTop = payload && payload.crawler_config && payload.crawler_config.stream;
656662
const direct = payload && payload.stream;
657-
return toBool(fromCrawler) || toBool(direct);
663+
return toBool(fromCrawlerParams) || toBool(fromCrawlerTop) || toBool(direct);
658664
}
659665

660666
// Main run function
@@ -676,8 +682,8 @@ <h2 class="font-medium text-accent">🔥 Stress Test</h2>
676682
const streamFlag = /stream\s*=\s*True/i.test(codeText);
677683
const isCrawlEndpoint = document.getElementById('endpoint').value === 'crawl';
678684
if (isCrawlEndpoint && streamFlag) {
679-
// Fallback: proceed with minimal config only for stream
680-
advConfig = { crawler_config: { stream: true } };
685+
// Fallback: minimal dump-shaped config so shouldUseStream + server load agree
686+
advConfig = { crawler_config: { type: 'CrawlerRunConfig', params: { stream: true } } };
681687
} else {
682688
updateStatus('error');
683689
document.querySelector('#response-content code').textContent =

deploy/docker/tests/test_security_2026_04.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,17 @@ def test_traversal_validator_removed(self):
138138

139139
class TestMonitorAuthStructural(unittest.TestCase):
140140

141-
def test_monitor_router_has_auth(self):
141+
def test_monitor_router_has_no_http_only_dep(self):
142+
"""Router-level token_dep was removed because it is HTTP-only and broke
143+
the WebSocket upgrade on /monitor/ws. Auth is enforced by
144+
AuthGateMiddleware; destructive routes keep Depends(require_admin)."""
142145
with open(os.path.join(DEPLOY_DIR, "server.py")) as f:
143146
source = f.read()
144-
# Find the line with monitor_router
145147
for line in source.splitlines():
146148
if "monitor_router" in line and "include_router" in line:
147-
self.assertIn("dependencies=", line,
148-
"Monitor router must have dependencies=[Depends(token_dep)]")
149+
self.assertNotIn("dependencies=", line,
150+
"Monitor router must NOT have router-level token_dep "
151+
"(breaks WS); auth is enforced by AuthGateMiddleware")
149152
return
150153
self.fail("Could not find monitor_router include_router line")
151154

deploy/docker/tests/test_security_container_posture.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ def test_artifact_dir_created_0700(self, dockerfile):
5757
assert "/var/lib/crawl4ai/outputs" in dockerfile
5858
assert "chmod 700 /var/lib/crawl4ai/outputs" in dockerfile
5959

60+
def test_playwright_headless_shell_is_copied_to_runtime_cache(self, dockerfile):
61+
# Playwright launches chromium_headless_shell for headless crawls. The
62+
# image runs as appuser, so both Chromium artifacts must be copied out
63+
# of root's install cache.
64+
cache_copy = re.search(
65+
r"cp -r(?P<artifacts>(?:(?!&&).)*)/home/appuser/\.cache/ms-playwright/",
66+
dockerfile,
67+
re.DOTALL,
68+
)
69+
assert cache_copy, "Dockerfile must copy Playwright artifacts into appuser's cache"
70+
assert "chromium-*" in cache_copy.group("artifacts")
71+
assert "chromium_headless_shell-*" in cache_copy.group("artifacts")
72+
6073
def test_runs_as_non_root(self, dockerfile):
6174
assert re.search(r"^USER\s+appuser", dockerfile, re.MULTILINE)
6275

0 commit comments

Comments
 (0)