跳到主要内容

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

查看所有标签

WordPress 插件 PDF 下载 500?部署与 REST 上下文的三个坑

· 阅读需 5 分钟

在商品页点击 PDF 目录下载时,页面先弹出 WordPress 的 "There has been a critical error on this website";把缺库问题修完,同一个端点又改报 HTTP 500——三个独立的坑,排成了同一条故障链。

在开发 CCLEE B2B(WooCommerce B2B 增强插件,覆盖企业用户管理、差异化定价、批量下单)的 PDF 目录下载功能时,三个坑挨个踩了一遍。

TL;DR​

坑根因修复
critical errorvendor 未进 git,rsync 部署缺库.gitignore 例外 + 强制提交 vendor
REST 端点 500admin-only 函数在 REST 上下文不存在function_exists 判断 + tempnam() 兜底
修完仍 500mPDF 默认临时目录容器内不可写tempDir 显式指向 sys_get_temp_dir()

三个坑共享一个背景:CI + rsync 的容器化部署链路。排完这条链的插件,在任何标准 LAMP 环境都不会复现——它们几乎全是容器化与 REST 上下文的组合产物。

场景一:下载报 "There has been a critical error"——vendor 没上服务器​

现象:本地一切正常,部署后点下载即见 WordPress 通用致命错误页。

根因:vendor/(mPDF 库)没到服务器。链路是双重的:插件自己的 .gitignore 排除了 /vendor/;仓库根 .gitignore 又有 vendor/ 与 plugins/ 两条规则,即使文件被跟踪也会被顶层规则遮蔽。CI 的 checkout 跳过未跟踪文件,rsync 如实镜像了一个缺库目录,服务器上 require_once vendor/autoload.php 直接 Fatal。

修复:插件 .gitignore 移除 /vendor/,仓库根添加例外规则,再强制提交:

git add -f wp/plugins/cclee-b2b/vendor/

WordPress 插件走 CI + rsync 部署时,服务器不会执行 composer install——vendor 必须当作源码的一部分进 git。

场景二:wp eval 正常、REST 端点 500——admin 函数缺席​

现象:vendor 齐了之后,wp eval 直接调 PDF 生成方法一切正常;REST 端点 /wp-json/cclee-b2b/v1/pdf-catalog 却返回 500。

根因:两处不兼容 REST 上下文的调用。wp_tempnam() 定义在 wp-admin/includes/file.php——REST 请求不加载这个目录,调用即 Fatal;WC()->shop_management 也非 WooCommerce 公开属性,fallback 路径一触发同样 Fatal。wp eval 不触发,恰恰因为 CLI 加载了包含 admin 文件的完整 WordPress。

修复:admin 函数加存在性判断并给原生兜底:

$tmp = function_exists( 'wp_tempnam' )
? wp_tempnam( $filename )
: tempnam( sys_get_temp_dir(), 'pdf' );

店铺名改走公开 API:get_bloginfo( 'name' )。REST 上下文里,任何 wp-admin/includes/ 下的函数都当不存在来写。

场景三:依赖齐了仍 500——mPDF 临时目录不可写​

现象:类加载正常、代码不再 Fatal,REST 依然 500。服务器 access log 只留下一行 858 字节的响应。

根因:mPDF 默认用 vendor/mpdf/mpdf/tmp/ 当临时目录;rsync 保留源机器属主 UID 1001,容器内 PHP-FPM 以 www-data(UID 82)运行,目录不可写,mPDF 构造函数抛异常。容器里想 chown 补救,卷挂载的文件系统又不允许。

修复:构造时显式指定临时目录到系统 /tmp:

$mpdf_tmp = sys_get_temp_dir() . '/mpdf';
if ( ! is_dir( $mpdf_tmp ) ) {
wp_mkdir_p( $mpdf_tmp );
}

$mpdf = new \Mpdf\Mpdf( array(
'mode' => 'utf-8',
'format' => 'A4',
'tempDir' => $mpdf_tmp,
) );

排查这类 500 有个捷径:wp eval-file 直接调用内部方法,绕过 REST 路由与权限检查,异常堆栈一步到位。同类静默坑在 WooCommerce 定制里不止一处——PPCP 按钮静默跳过是另一例。

注意事项

三个修复要按顺序验证:vendor 到位后再测 REST 上下文,最后才轮到临时目录——跳步验证会让 500 的归因错位。另外 function_exists 兜底不是长期形态,若插件支持非 admin 上下文运行,自定义的临时文件逻辑应完全不依赖 wp-admin/includes/。

常见问题​

为什么 wp eval 能跑、REST 端点却 500?​

CLI 环境加载完整 WordPress(含 wp-admin/includes/),REST 请求不加载这个目录。依赖 admin-only 函数(如 wp_tempnam)的代码在 wp eval 下一切正常,进 REST 就 Fatal Error——两个上下文装载的文件集不同。

怎么快速定位 REST 500 背后的具体异常?​

用 wp eval-file 直接调用插件的内部方法,绕过 REST 路由与权限检查,异常堆栈一步到位;比在 REST 响应里只看到一个 858 字节的 500 页面高效得多。

容器化部署 WordPress 插件要在服务器上跑 composer install 吗?​

不要指望它——CI + rsync 部署链路里服务器不会执行 composer install,vendor 目录必须进 git、随 rsync 落到服务器。插件自己的 .gitignore 若排除了 /vendor/,部署出的插件就是缺库的。

第三方库自带的临时目录为什么在容器里不可写?​

rsync 同步时保留源机器的属主 UID(如 1001),容器内 PHP-FPM 以另一身份运行(如 www-data 的 82),vendor 内的默认临时目录随之不可写;容器卷挂载的文件系统还常常不允许 chown。显式把 tempDir 指到 sys_get_temp_dir() 即可。

CCLEE

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

合作咨询

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_date 和 dag_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_id 和 logical_date 两个字段(可选 conf 传参数)。这两个字段在同一个 DAG 下都必须唯一,否则 Airflow 返回 4xx。代码里推荐用 TriggerDagRunOperator,它内部也会生成唯一的 run id。

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

因为 Airflow 在 metadata 数据库的 dag_run 表上对 (dag_id, logical_date) 维护唯一约束,dag_run_id 本身也是主键。重复的 logical_date 或 dag_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-date:airflow 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 值为 hcdn、cloudflare 等 CDN 标识说明是 CDN 层拦截;server 值为 nginx/apache 且包含 X-WP-* 或 X-RateLimit-* 头说明请求已到达 WordPress。


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

CCLEE

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

合作咨询