跳到主要内容

2 篇博文 含有标签「REST API」

查看所有标签

Airflow 触发 dagRun 静默失败?logical_date 唯一约束在作怪

· 阅读需 5 分钟

在用 Airflow REST API 重复触发同一个 DAG 做灰度验证时,请求返回了 4xx 但响应体里没有 dag_run_id,DAG 实际根本没有运行——而脚本却把它当成了成功。

在开发 AI 运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。广告决策链路的灰度切换需要在 Airflow 上反复触发同一次分析做对照,结果部分触发悄无声息地失败了。

TL;DR

Airflow 对每个 DAG 的 logical_date 有唯一约束dag_run_id 同样必须唯一)。用相同的 logical_date 重复 POST /dags/{dag_id}/dagRuns,Airflow 会拒绝并返回 4xx,响应体里没有 dag_run_id。如果你只检查 HTTP 状态码、不检查返回的 dag_run_id,就会误以为触发成功。解法:每次触发用不同的 logical_date(和不同的 dag_run_id)。

问题现象

为了对照测试,用固定日期 2026-01-01 连续触发同一个 DAG:

$ curl -s -X POST "$AIRFLOW/api/v2/dags/my_dag/dagRuns" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dag_run_id": "manual-run-1",
"logical_date": "2026-01-01T00:00:00Z"
}'
# 第一次:返回正常的 dag_run 对象,包含 dag_run_id ✅

$ curl -s -X POST "$AIRFLOW/api/v2/dags/my_dag/dagRuns" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dag_run_id": "manual-run-2",
"logical_date": "2026-01-01T00:00:00Z" # ⚠️ 同一个 logical_date
}'
# 第二次:返回错误对象,没有 dag_run_id ❌
{
"detail": "...",
"status": 400,
"title": "Bad Request",
"type": "https://airflow.apache.org/docs/apache-airflow/2/stable-rest-api-ref.html#/default/Error"
}

如果调用方只判断「HTTP 是否 2xx」就停止解析,或者直接读 JSON 不校验 dag_run_id 字段,第二次失败就会被静默吞掉——日志里看不到异常,Airflow UI 里也找不到这次 run。

根因

Airflow 用 dag_run_id 作为每次运行的主键,同时在 metadata 数据库的 dag_run 表上对 (dag_id, logical_date) 维护唯一性。logical_date 是调度的「逻辑时间」——调度器按它判断某个调度槽位是否已经跑过。一旦同一个 DAG 下已存在某 logical_date 的 run,再用相同值触发,Airflow 就会拒绝,避免重复执行。

问题在于这个失败是 HTTP 4xx + 错误 JSON,不是连接错误或 5xx。很多脚本只做 response.status_code == 200 的粗判断,或者拿到 JSON 后直接取字段而不校验是否存在 dag_run_id,于是把「拒绝创建」当成了「创建成功」。

解决方案

核心:每次触发用不同的 logical_date(以及不同的 dag_run_id)。 灰度/回放场景下,给每次触发拼一个不重复的日期即可:

# 每次循环用不同的 logical_date(2026-01-01 / 02 / 03 …)
for i in 1 2 3; do
curl -s -X POST "$AIRFLOW/api/v2/dags/my_dag/dagRuns" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"dag_run_id\": \"manual-run-$i\",
\"logical_date\": \"2026-01-0${i}T00:00:00Z\"
}"
done

更稳妥的是用递增时间戳,保证 logical_datedag_run_id 永不重复。更重要的是:必须校验响应体里的 dag_run_id 字段,把它当成「触发真正成功」的唯一证据:

import requests

def trigger_dag(dag_id: str, logical_date: str, conf: dict | None = None) -> str:
resp = requests.post(
f"{AIRFLOW}/api/v2/dags/{dag_id}/dagRuns",
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
json={"dag_run_id": f"manual-{logical_date}", "logical_date": logical_date, "conf": conf or {}},
)
# ❌ 不够:只看状态码,4xx 会被当异常但容易漏判
# resp.raise_for_status()
data = resp.json()
# ✅ 正确:dag_run_id 存在才算真正创建成功
if "dag_run_id" not in data:
raise RuntimeError(f"Trigger failed: {resp.status_code} {data}")
return data["dag_run_id"]

# 每次用不同 logical_date,重复触发安全
for i in range(1, 4):
trigger_dag("my_dag", f"2026-01-0{i}T00:00:00Z")

dag_run_id 同样要保持唯一——它是主键,重复会被直接拒绝。用「前缀 + logical_date」组合是常见做法,既唯一又能在 UI 里一眼识别。

常见问题

airflow trigger_dagrun 怎么通过 REST API 触发 DAG?

POST /api/v2/dags/{dag_id}/dagRuns,请求体至少包含 dag_run_idlogical_date 两个字段(可选 conf 传参数)。这两个字段在同一个 DAG 下都必须唯一,否则 Airflow 返回 4xx。代码里推荐用 TriggerDagRunOperator,它内部也会生成唯一的 run id。

airflow 重复触发同一个 DAG 为什么失败?

因为 Airflow 在 metadata 数据库的 dag_run 表上对 (dag_id, logical_date) 维护唯一约束,dag_run_id 本身也是主键。重复的 logical_datedag_run_id 都会被拒绝并返回 4xx。要做回放或灰度对照,每次触发换一个新的 logical_date(或递增时间戳)即可。

注意事项

  • 校验 dag_run_id,不要只看状态码:4xx + 错误 JSON 是 Airflow 表达「拒绝创建」的正常方式,只判断 status_code 容易把失败误判为成功。
  • logical_date 用过去时间:填未来时间会被当成定时调度,不会立即执行;要立即跑用过去日期。
  • API 版本差异:Airflow 2.x 是 /api/v2/dags/{dag_id}/dagRuns,3.x 路径和字段有调整,迁移时务必核对对应版本的 REST API 文档。
  • 回放优先用 CLI 的 --logical-dateairflow dags trigger 支持指定日期,但对 logical_date 的唯一约束同样生效,重复日期一样会失败。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

WordPress REST API 上传图片返回 405?检查你的 Hostinger CDN

· 阅读需 4 分钟

在为客户构建 WooCommerce 产品导入工具时,调用 /wp-json/wp/v2/media 上传图片,前几张成功后突然全部返回 405 Not Allowed。

TL;DR

Hostinger CDN(hcdn)默认拦截了 POST /wp-json/wp/v2/media 请求。响应头 server: hcdn + x-hcdn-request-id 是关键证据。关闭 CDN 或联系 Hostinger 客服放行 /wp-json/* POST 请求即可解决。

问题现象

通过 WP REST API 批量上传图片到 WordPress Media Library:

curl -X POST 'https://example.com/wp-json/wp/v2/media' \
-u 'user:app_password' \
-H 'Content-Disposition: attachment; filename="product-01.jpg"' \
-H 'Content-Type: image/jpeg' \
--data-binary @image.jpg

前 2-4 张图片返回 201 Created,之后的请求全部返回:

<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx</center>
</body>
</html>

"部分成功"这个现象容易误导判断——看起来像是频率限制(Rate Limiting),但实际原因完全不同。

根因

curl -v 查看完整的 response header:

< HTTP/2 405
< server: hcdn
< x-hcdn-request-id: cfc5ad1198938cd9f1e02ce71ed0ae61-kul-edge1

关键信息:

  • server: hcdn — 这是 Hostinger 自研 CDN(hcdn),不是源站 nginx
  • x-hcdn-request-id — CDN 边缘节点 ID(kul-edge1 = 吉隆坡),说明请求在 CDN 层就被拦截了,根本没有到达 WordPress

Hostinger CDN 默认安全规则拦截了 /wp-json/wp/v2/media 的 POST 方法。前几张成功可能是因为 CDN 规则存在短暂的冷启动窗口或缓存未命中。

解决方案

方案 1:关闭 CDN(快速验证)

在 Hostinger hPanel → Website → CDN → 关闭 CDN。

关闭后立即生效,但会失去 CDN 加速能力。适合 staging 环境或紧急修复。

方案 2:联系 Hostinger 客服放行 API 路径(推荐)

提交工单要求放行 /wp-json/* 的 POST 请求。Hostinger Manage 页面目前不提供自定义 CDN 规则选项,必须通过客服操作。

方案 3:代码层增加重试与延迟(防御性措施)

即使 CDN 配置正确,加入重试逻辑也能应对偶发的 CDN 限流:

import time
import random

def upload_image(url, image_bytes, filename, auth, max_retries=3):
for attempt in range(max_retries):
resp = httpx.post(
url,
content=image_bytes,
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Type": "image/jpeg",
},
auth=auth,
timeout=30,
)
if resp.status_code != 405:
return resp
delay = 3 * (attempt + 1) + random.uniform(0, 2)
time.sleep(delay)
resp.raise_for_status()

排查过程回顾

这个问题绕了不少弯路,记录排查路径供参考:

排查方向操作结果
WP 插件拦截停用 Speed Optimizer / Auto Upload Images仍 405,排除
请求频率限制图片间加 2-5s 延迟 + 重试仍 405,排除
REST API 禁用GET /wp-json/wp/v2/settings正常返回,排除
凭证错误WC Test Connection成功,排除
CDN 拦截curl -v 查看 response headerserver: hcdn 确认 CDN 拦截

关键转折点是用 curl -v 看到了 server: hcdn,才知道请求根本没到达 WordPress 层。

注意事项

  • 关闭 CDN 后 DNS 缓存可能需要几分钟刷新,不要立刻重试
  • 如果你的站点在 Hostinger 且使用 REST API 做批量操作,上线前务必测试 CDN 是否会拦截
  • WooCommerce 的 WC API (/wc/v3/products) 走的是不同的认证机制(Consumer Key),通常不受此影响;受影响的主要是 WP REST API (/wp-json/wp/v2/*) 的写操作

常见问题

WordPress REST API 上传图片返回 405 Not Allowed 怎么办?

先检查 response header 中的 server 字段。如果值为 hcdn(Hostinger CDN)或其他 CDN 标识,说明请求被 CDN 拦截,未到达 WordPress。关闭 CDN 或联系服务商放行即可。

如何判断 405 是 CDN 拦截还是 WordPress 返回的?

curl -v 查看 response header:server 值为 hcdncloudflare 等 CDN 标识说明是 CDN 层拦截;server 值为 nginx/apache 且包含 X-WP-*X-RateLimit-* 头说明请求已到达 WordPress。


在为 LightCT 构建 WooCommerce 产品导入工具时遇到此问题。如果你也在用 Hostinger 做 WordPress 开发,遇到类似的 REST API 问题,欢迎联系交流

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询