High-performance web content extraction engine.
4.2K
High-performance web content extraction engine built in Rust. Primary purpose is serving as an external web loader for OpenWebUI, but it's flexible enough for any use case that needs clean content extraction from web pages - RAG pipelines, content indexing, web scraping, archiving, and more.
HTTPS_PROXY/HTTP_PROXY/NO_PROXY for both HTTP client and Chromium browser trafficdocker build -t web-loader-engine .
docker run -d -p 14786:14786 --name web-loader web-loader-engine
services:
web-loader:
build: .
ports:
- "14786:14786"
environment:
- BROWSER_POOL_SIZE=10
- CACHE_TTL=3600
# - API_KEY=your-secret-key
volumes:
- screenshots:/app/screenshots
restart: unless-stopped
volumes:
screenshots:
docker-compose up -d
services:
web-loader:
image: edgaras0x4e/web-loader-engine:latest
ports:
- "14786:14786"
environment:
- BROWSER_POOL_SIZE=10
- CACHE_TTL=3600
# - API_KEY=your-secret-key
volumes:
- screenshots:/app/screenshots
restart: unless-stopped
volumes:
screenshots:
docker-compose up -d
Then set OpenWebUI's web loader URL to http://web-loader:14786
Requires Rust 1.70+ and Chrome/Chromium installed.
cp .env.example .env # Configure settings
cargo build --release
./target/release/web-loader-engine
Copy the example environment file and adjust as needed:
cp .env.example .env
Environment variables:
| Variable | Default | Description |
|---|---|---|
API_PORT | 14786 | Server port |
API_KEY | - | Optional API key for authentication |
CHROME_PATH | /usr/bin/chromium | Path to Chrome/Chromium binary |
BROWSER_POOL_SIZE | 10 | Concurrent browser pages |
REQUEST_TIMEOUT | 30 | Default timeout in seconds |
CACHE_TTL | 3600 | Cache lifetime in seconds |
SCREENSHOT_DIR | /app/screenshots | Screenshot storage path |
BROWSER_LOG_LEVEL | error | Log level for the headless browser driver (chromiumoxide). Silences noisy CDP deserialization warnings by default. Accepts off, error, warn, info, debug, trace |
DEFAULT_USER_AGENT | Chrome 120 on Windows | User agent used when no override is provided and rotation is disabled |
USER_AGENT_ROTATION | off | Rotation strategy: off, round_robin, random |
USER_AGENT_POOL | - | Inline pool of UAs separated by | or newlines |
USER_AGENT_POOL_FILE | - | Path to a file with one UA per line (lines starting with # are comments). Takes precedence over USER_AGENT_POOL |
HTTPS_PROXY / HTTP_PROXY | - | Egress proxy URL (e.g. http://proxy:3128). When set, routes both HTTP client and Chromium traffic through the proxy |
NO_PROXY | - | Comma-separated list of hosts/domains to bypass the proxy (e.g. localhost,127.0.0.1,*.internal.example.com) |
POST /
{"urls": ["https://example.com/article"]}
Returns:
[
{
"page_content": "# Article Title\n\nContent...",
"metadata": {
"source": "https://example.com/article",
"title": "Article Title"
}
}
]
POST /load
{"url": "https://example.com"}
Response:
{
"url": "https://example.com",
"title": "Example Domain",
"content": "# Example Domain\n\nThis domain is for examples...",
"metadata": {
"processing_time_ms": 1234,
"cached": false
}
}
POST /load/batch
{"urls": ["https://example.com/1", "https://example.com/2"]}
Response:
{
"results": [
{
"url": "https://example.com/1",
"response": {
"url": "https://example.com/1",
"title": "Page Title",
"content": "...",
"metadata": {"processing_time_ms": 500, "cached": false}
}
}
],
"total_processing_time_ms": 1234
}
GET /health
| Header | Values | Description |
|---|---|---|
x-respond-with | markdown, html, text, screenshot, pageshot | Output format |
x-wait-for-selector | CSS selector | Wait for element before extraction |
x-target-selector | CSS selector | Extract only matching content |
x-remove-selector | CSS selector | Remove elements before extraction |
x-timeout | seconds | Request timeout |
x-set-cookie | name=value | Set cookies |
x-no-cache | true | Bypass cache |
x-with-images-summary | true | Include images list |
x-with-links-summary | true | Include links list |
x-user-agent | UA string, rotate, default | Override the user agent for this request. rotate forces rotation from the pool even when USER_AGENT_ROTATION=off; default forces the configured default |
Authorization | Bearer <key> | API key (if configured) |
{
"url": "https://example.com",
"options": {
"wait_for_selector": "#content",
"target_selector": "article",
"remove_selector": ".ads",
"timeout": 60
}
}
Set x-respond-with to either screenshot (viewport only) or pageshot (full scrolling page). The API renders the page in headless Chromium, saves the PNG to SCREENSHOT_DIR, and returns a relative URL you can fetch from the same server.
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-respond-with: screenshot" \
-d '{"url": "https://example.com"}'
Response:
{
"url": "https://example.com",
"title": null,
"content": "",
"screenshot_url": "/screenshots/httpsexamplecom_441d3714-d010-4eb4-a729-606873b081d9.png",
"metadata": {"processing_time_ms": 1064, "cached": false}
}
Fetch the PNG:
curl -o page.png \
http://localhost:14786/screenshots/httpsexamplecom_441d3714-d010-4eb4-a729-606873b081d9.png
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-respond-with: pageshot" \
-d '{"url": "https://example.com"}'
Combine with x-wait-for-selector so the screenshot is only taken once a specific element has rendered:
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-respond-with: screenshot" \
-H "x-wait-for-selector: article h1" \
-d '{"url": "https://example.com/post/123"}'
curl -X POST http://localhost:14786/load \
-H "Authorization: Bearer your-secret-key" \
-H "Content-Type: application/json" \
-H "x-respond-with: screenshot" \
-d '{"url": "https://example.com"}'
curl -H "Authorization: Bearer your-secret-key" \
-o page.png \
http://localhost:14786/screenshots/httpsexamplecom_441d3714-d010-4eb4-a729-606873b081d9.png
SCREENSHOT_DIR (default /app/screenshots in Docker, configurable via env)/app/screenshots to persist captures across container restartsThree ways to control which User-Agent is sent with a request:
DEFAULT_USER_AGENT in the environment. Used when rotation is off and no header is provided.USER_AGENT_ROTATION=round_robin or random plus USER_AGENT_POOL (or USER_AGENT_POOL_FILE). The server picks a different UA per request.x-user-agent on the individual call.Resolution order per request: explicit header > rotation (if enabled) > configured default.
Separate UAs with | or newlines:
USER_AGENT_ROTATION=round_robin
USER_AGENT_POOL="Mozilla/5.0 ...Chrome/120...|Mozilla/5.0 ...Firefox/121..."
One UA per line, # lines are comments. Takes precedence over USER_AGENT_POOL if both are set.
USER_AGENT_ROTATION=random
USER_AGENT_POOL_FILE=/etc/web-loader/user-agents.txt
Sample user-agents.txt:
# Desktop Chrome
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
# Desktop Firefox
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-user-agent: MyBot/1.0 (+https://example.com/bot)" \
-d '{"url": "https://httpbin.org/user-agent"}'
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-user-agent: rotate" \
-d '{"url": "https://httpbin.org/user-agent"}'
curl -X POST http://localhost:14786/load \
-H "Content-Type: application/json" \
-H "x-user-agent: default" \
-d '{"url": "https://httpbin.org/user-agent"}'
While built for OpenWebUI, this works for:
User Agent Rotation & Browser Log Control - Added configurable user agents and a way to silence warnings.
USER_AGENT_ROTATION env var with strategies off (default), round_robin, random - rotates per requestUSER_AGENT_POOL (inline, |- or newline-separated) or USER_AGENT_POOL_FILE (path to a file, one UA per line, # comments supported). The file takes precedence when both are setDEFAULT_USER_AGENT overrides the hardcoded default used when rotation is off and no header is setx-user-agent header now accepts special values: rotate forces rotation even when USER_AGENT_ROTATION=off, and default forces the configured defaultBROWSER_LOG_LEVEL env var (default error) silences chromiumoxide's noisy WS Invalid message warnings emitted when Chromium sends CDP events the driver doesn't yet model. Accepts off, error, warn, info, debug, trace - operates independently of RUST_LOGChromium Egress Proxy Support - Chromium now honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY from the environment so the browser's outbound traffic can be routed through an egress proxy.
HTTPS_PROXY (or HTTP_PROXY as fallback) is set, Chromium is started with --proxy-server=<url>NO_PROXY is set, its value is translated to Chrome's bypass-list syntax and passed via --proxy-bypass-list=<list> (commas → semicolons, *.domain → .domain)Screenshot Delivery Fix - Screenshot URLs returned by the API are now actually reachable.
/load responses advertised a screenshot_url that returned 404 when fetchedSCREENSHOT_DIR/screenshots/../etc/passwd) return 404Browser Pool Resilience - Fixed critical issue where dead browser connections would cause requests to hang indefinitely.
Ws(AlreadyClosed) and related WebSocket errorshealthy status and recreation_count for monitoringHealth response now includes:
{
"status": "ok",
"version": "0.1.4",
"browser_pool": {
"available": 10,
"total": 10,
"healthy": true,
"recreation_count": 1
}
}
Monitor recreation_count increasing to track browser recovery events.
MIT
Content type
Image
Digest
sha256:3df5300e7…
Size
443.2 MB
Last updated
5 months ago
docker pull edgaras0x4e/web-loader-engine