如何为 Crawl4AI 自托管 Docker 服务配置 webhook 回调接收爬取完成通知
【免费下载链接】crawl4ai🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Don't be shy, join here: https://discord.gg/jP8KfhDhyN项目地址: https://gitcode.com/GitHub_Trending/craw/crawl4ai
Crawl4AI 的 Docker 版 API 服务(deploy/docker)通过POST /crawl/job提交异步爬取任务。默认用法是客户端拿回task_id后反复轮询GET /crawl/job/{task_id}检查状态;对于长时间运行的爬取或需要避免占用连接的场景,服务内置了 webhook 功能:任务完成或失败后,服务端会主动向你的回调端点 POST 一条通知,不再需要轮询。本文基于deploy/docker目录下的部署文档,完成三件事:配置回调地址(按任务或全局默认两种粒度)、编写一个能接收通知的回调端点、以及验证通知确实送达并排查投递问题。前提条件:API 服务已按 deploy/docker/README.md 启动,默认监听127.0.0.1:11235;回调端点必须能被服务端访问到,并返回 HTTP 2xx 状态码。
webhook 的工作流程
按 README.md 的 “Asynchronous Jobs with Webhooks” 一节,整个过程是五步:
- 提交任务:
POST /crawl/job,请求体中携带可选的webhook_config; - 立即拿到
task_id; - 任务在后台执行;
- 服务端把完成通知 POST 到你的 webhook 地址;
- 如果通知里没有带数据,再用
GET /crawl/job/{task_id}拉取结果。
同一套 webhook 机制同时支持/crawl/job(爬取)和/llm/job(LLM 抽取)两个端点。
全局配置:config.yml 的 webhooks 段
发布版 config.yml 已经自带webhooks段,无需手工创建:
webhooks: enabled: true default_url: null # Optional: default webhook URL for all jobs data_in_payload: false # Optional: default behavior for including data retry: max_attempts: 5 initial_delay_ms: 1000 # 1s, 2s, 4s, 8s, 16s exponential backoff max_delay_ms: 32000 timeout_ms: 30000 # 30s timeout per webhook call headers: # Optional: default headers to include User-Agent: "Crawl4AI-Webhook/1.0"各字段的作用:
enabled:总开关。设为false时任务照常运行,但不会发送任何通知;default_url:全局默认回调地址,null表示不设置。只有当请求体没带webhook_config(或其中没有webhook_url)时才会用到它;data_in_payload:通知里是否携带爬取结果的默认值,可被单个任务的webhook_data_in_payload覆盖;retry:重试策略,默认最多 5 次,指数退避 1s → 2s → 4s → 8s → 16s,单次调用超时 30 秒;headers:每次投递默认附加的请求头,任务级webhook_headers会与它合并后一起发送。
如果希望所有没单独指定回调的任务都通知到同一地址,只需把default_url改成你的回调端点:
webhooks: enabled: true default_url: "https://myapp.com/webhooks/default" data_in_payload: false之后不带webhook_config的任务也会自动发 webhook。配置改动后需要让服务重新加载config.yml。
主路径:按任务提交 webhook_config
最常用的是在提交任务的请求体里直接带上webhook_config,粒度是单个任务:
curl -X POST http://localhost:11235/crawl/job \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://example.com"], "webhook_config": { "webhook_url": "https://your-app.example.com/webhooks/crawl-complete", "webhook_data_in_payload": false } }'其中webhook_url需要替换成你自己的回调端点(文档示例中的https://myapp.com/webhooks/crawl-complete只是样例值)。webhook_url是必填字段,会被校验为合法 URL;webhook_data_in_payload默认false,决定通知里是否携带完整结果;webhook_headers可选,见下文。
提交成功后,响应立即返回任务 ID(文档示例输出):
{ "task_id": "crawl_a1b2c3d4" }模式一:只发通知,凭 task_id 拉结果
webhook_data_in_payload: false时,回调收到的通知体(文档示例):
{ "task_id": "crawl_a1b2c3d4", "task_type": "crawl", "status": "completed", "timestamp": "2025-10-21T10:30:00.000000+00:00", "urls": ["https://example.com"] }你的回调处理器拿到task_id后,再调用结果接口取数据:
curl http://localhost:11235/crawl/job/crawl_a1b2c3d4模式二:把爬取结果直接放进通知
把webhook_data_in_payload设为true,通知体会多出一个data字段,包含完整结果(文档示例):
{ "task_id": "crawl_a1b2c3d4", "task_type": "crawl", "status": "completed", "timestamp": "2025-10-21T10:30:00.000000+00:00", "urls": ["https://example.com"], "data": { "markdown": "...", "html": "...", "links": {...}, "metadata": {...} } }两种模式按团队习惯选:只发通知、处理器再拉取,实现最简单;直接带数据则省去一次往返,但通知体可能较大。
可选:自定义请求头做鉴权
webhook_headers用于在通知里附加鉴权或标识头,服务端会把它与config.yml的默认头合并后发送:
{ "urls": ["https://example.com"], "webhook_config": { "webhook_url": "https://myapp.com/webhooks/crawl", "webhook_data_in_payload": false, "webhook_headers": { "X-Webhook-Secret": "your-secret-token", "X-Service-ID": "crawl4ai-prod" } } }头字段有校验限制(schemas.py 中WebhookConfig在提交时执行,违规请求会被 422 拒绝):最多 20 个头;头名只允许字母、数字、连字符且不超过 64 字符;host、content-length、transfer-encoding、connection、content-type、proxy-authorization、authorization、cookie、expect、upgrade、te、trailer这些名称被禁止;头值不超过 2048 字符且不能包含\r、\n、\0。
回调端点示例
WEBHOOK_EXAMPLES.md 提供了一个 Flask 处理器示例,下面保留其中处理爬取任务的部分(文档中的完整版本还处理 LLM 抽取任务):
from flask import Flask, request, jsonify import requests app = Flask(__name__) @app.route('/webhooks/crawl-complete', methods=['POST']) def handle_crawl_webhook(): payload = request.json task_id = payload['task_id'] status = payload['status'] if status == 'completed': # If data not in payload, fetch it if 'data' not in payload: response = requests.get(f'http://localhost:11235/crawl/job/{task_id}') data = response.json() else: data = payload['data'] results = data.get('results', []) for result in results: print(f" - {result.get('url')}: {len(result.get('markdown', ''))} chars") elif status == 'failed': error = payload.get('error', 'Unknown error') print(f"crawl job {task_id} failed: {error}") return jsonify({"status": "received"}), 200 if __name__ == '__main__': app.run(port=8080)两个注意点:
- 示例里用
http://localhost:11235拉取结果,只在回调服务与 API 服务运行在同一主机上成立;回调端点在别的机器时,要替换成实际可达的 API 地址; - 最后一行返回 200 是关键:只有收到 2xx,服务端才认为投递成功。返回 4xx 会被视为拒绝且不再重试,见下文重试机制。
验证通知是否送达
1. 提交返回 task_id,回调端点收到 POST
任务完成(或失败)后,你的端点应收到一次 POST。status只有completed和failed两种值;失败时通知体会多一个error字段(文档示例):
{ "task_id": "crawl_a1b2c3d4", "task_type": "crawl", "status": "failed", "timestamp": "2025-10-21T10:30:00.000000+00:00", "urls": ["https://example.com"], "error": "Connection timeout after 30s" }2. 检查服务端投递日志
投递过程会写入应用日志(INFO 级别记录成功投递、重试及最终失败)。按文档给出的方式过滤:
docker logs crawl4ai-container | grep -i webhook其中crawl4ai-container需替换为你的实际容器名。webhook.py 中对应的日志信息包括:Webhook delivered successfully(投递成功)、Webhook rejected with status {status}(被 4xx 拒绝,不重试)、Webhook failed with status {status}, will retry(5xx 将重试)、Webhook blocked (SSRF protection)(出站校验拦截,不重试)。如果两端都没收到通知,先确认webhooks.enabled为true,且任务请求带了webhook_config或全局配置了default_url——两者都没有时服务会静默跳过通知(任务本身照常运行)。
3. 重试机制决定了“收不到”的语义
webhook 投递采用指数退避重试(WEBHOOK_EXAMPLES.md “Retry Logic” 一节):
| 项 | 值 |
|---|---|
| 尝试次数 | 默认最多 5 次 |
| 退避间隔 | 1s → 2s → 4s → 8s → 16s |
| 单次超时 | 30 秒 |
| 会重试的情况 | 5xx 状态码、网络错误、超时 |
| 不重试的情况 | 4xx 状态码(记为拒绝)、2xx(投递成功) |
也就是说,回调端点如果暂时不可用,服务端最长会在约 31 秒内尝试 5 次;如果端点返回 4xx,服务端会立即放弃,不会再次投递。
回调地址可达性:出站校验限制
服务端对 webhook 目标有出站校验(webhook.py 与 egress_broker.py):发送前会解析目标主机名,若解析结果不是全局可路由地址(回环、私有网段、链路本地等),或主机名属于localhost、metadata、kubernetes.default、host.docker.internal等被拒名单,通知会被直接丢弃并记录Webhook blocked (SSRF protection),且不重试。重定向会被逐跳重新校验,最多跟随 5 跳。
实际影响:回调端点应部署在可被公网(或至少是全局可路由地址)访问的位置;把webhook_url指向内网 IP 会静默收不到通知,只能从日志里发现。源码中提供了CRAWL4AI_ALLOW_INTERNAL_URLS环境变量(默认false)可跳过该限制,注释明确它只适用于受信任的内部部署。
可选:LLM 抽取任务
/llm/job端点使用同一套webhook_config(WEBHOOK_EXAMPLES.md Example 6):
curl -X POST http://localhost:11235/llm/job \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/article", "q": "Extract the article title, author, and publication date", "schema": "{\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}, \"author\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}}}", "cache": false, "provider": "openai/gpt-4o-mini", "webhook_config": { "webhook_url": "https://myapp.com/webhooks/llm-complete", "webhook_data_in_payload": true } }'与爬取任务的区别:task_type为"llm_extraction";url是单值而非数组;抽取结果在data.extracted_content中。文档特别提醒:webhook 通知里的键是extracted_content,而通过 API 拉取结果时对应的键是result,处理器里两者都要兼容。
限制与注意事项
- 任务数据存放在 Redis 中,
config.yml默认task_ttl_seconds: 3600(1 小时)后过期。采用“只发通知、稍后再拉结果”的模式时,拉取应发生在 TTL 之内,否则GET /crawl/job/{task_id}可能已经查不到数据。 - 不配置 webhook 时,轮询方式依然可用:请求体省略
webhook_config,直接轮询GET /crawl/job/{task_id},响应中status字段取值为"processing"、"completed"或"failed"。 - 若回调端点在别的机器上,示例中的
http://localhost:11235结果拉取地址、以及服务自身监听地址(默认127.0.0.1:11235,对外暴露需配置CRAWL4AI_API_TOKEN并加反向代理,见config.yml注释)都要按实际部署调整。
配置完成后,用一次真实任务闭环验证:提交带webhook_config的任务 → 回调端点收到status: "completed"的通知 →docker logs中能看到Webhook delivered successfully。这三点都满足,说明 webhook 回调链路已经打通。更完整的处理器示例(含 TypeScript 客户端、完整 Flask 代码)可参考 WEBHOOK_EXAMPLES.md。
【免费下载链接】crawl4ai🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Don't be shy, join here: https://discord.gg/jP8KfhDhyN项目地址: https://gitcode.com/GitHub_Trending/craw/crawl4ai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考