本文面向使用 Shopify CLI 管理应用配置的开发者,解决一个具体问题:TOML 中写了 Webhook 后,如何证明订阅配置进入了正确的应用版本,而且过滤和载荷裁剪没有让有效事件消失。
资料核验于 2026年10月4日,示例使用 Webhook API 2026-10。需要自己的 CLI 应用项目、已安装授权的开发店和可接收 HTTPS 请求的应用后端。products/update 主题需要 read_products;安装授权应与现有应用 scope 配置一起检查,不覆盖已有权限。示例地址、商品和响应均为假设,Python 检查已离线执行,应用部署与真实投递尚未执行。主题权限、应用 scope 配置。
1. 先确定订阅归谁管理,再决定到哪里读回
应用级订阅由 TOML 管理,在安装该应用的商店间使用相同配置;按店铺改变主题或目标地址的需求,应评估店铺级订阅。通过 Shopify 后台直接创建的自定义应用不能用这条 CLI/TOML 订阅流程。两种订阅方式与适用条件。
| 验收对象 | 应用级 TOML 订阅 | 店铺级 GraphQL 订阅 |
|---|---|---|
| 配置入口 | 应用配置文件与应用版本 | 对应商店的 Admin API |
| 订阅标识 | 配置管理,无订阅 ID | 有订阅 ID |
| 读回入口 | Dev Dashboard 的应用版本配置 | webhookSubscriptions 查询 |
| 查询为空的含义 | 不能据此判定 TOML 订阅丢失 | 仍须核对查询所在应用、商店和范围 |
webhookSubscriptions 只返回店铺级订阅,不包含 TOML 的应用级订阅。2026-10 查询说明。因此不要为了“修复查询为空”再建一个相同的店铺级订阅,否则可能引入重复通知。
2. 用一个可解释的过滤与裁剪配置
下面是假设商品同步应用的配置片段:只接收当前 vendor 值为 Acme 的商品更新,向后端传递商品 ID、标题、供应商和更新时间。应合入现有配置,不能用它替换完整应用文件。app.example.com 是占位地址;本片段也不代表已经完成应用商店要求的全部隐私订阅配置。
[webhooks]
api_version = "2026-10"
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "https://app.example.com/webhooks/products-update"
filter = "vendor:Acme"
include_fields = ["id", "title", "vendor", "updated_at"]
name = "catalog-acme"
topics 与 uri 定义主题和目标;filter 控制投递条件,include_fields 控制载荷字段。name 可辅助在投递头中识别订阅。应用级载荷版本由 [webhooks].api_version 设置。配置字段、订阅字段与 name。
这里有三个必须一起验证的行为:
- 过滤当前值,不过滤“哪个字段刚发生变化”。
vendor:Acme使用等值条件并区分大小写;当商品仍属于 Acme 时,标题修改也可以满足条件。若要求“仅供应商变化时触发”,这份配置没有提供该保证。过滤语义与搜索差异。 - 过滤字段必须保留在裁剪载荷中。 设置了
include_fields后,过滤表达式引用的全部字段都要包含其中。本例不能删除vendor。错误字段或类型可能允许创建订阅却抑制所有投递;错误语法则阻止创建。过滤约束。 - 保留
updated_at,检查裁剪后的去抖风险。 短时间内多个事件产生完全相同的小载荷时,后续投递可能被去抖丢弃。官方建议加入updated_at,让更新对应的载荷发生变化;这不构成“每个事件必达”的承诺。载荷修改与去抖。
字段名取自 Webhook JSON:这里是 updated_at,不是 GraphQL 商品字段的 updatedAt。商品更新样例中也包含 vendor、title 和 id。products/update 载荷。
3. 本地先挡住这份配置的常见错误
Python 3.11+ 的 tomllib 可以解析 TOML;无效语法会抛出 TOMLDecodeError。下面进一步检查本例的版本、主题、固定过滤式、字段依赖和 HTTPS 地址。它不是通用过滤表达式解析器,也不代表 Shopify 已接受配置。Python tomllib 原始文档。
import tomllib
from urllib.parse import urlsplit
VERSION = "2026-10"
def lint_subscription(toml_text):
try:
config = tomllib.loads(toml_text)
except tomllib.TOMLDecodeError:
return ["INVALID_TOML"]
webhooks = config.get("webhooks")
if not isinstance(webhooks, dict):
return ["MISSING_WEBHOOKS"]
issues = []
if webhooks.get("api_version") != VERSION:
issues.append("VERSION_MISMATCH")
subs = webhooks.get("subscriptions")
if not isinstance(subs, list) or len(subs) != 1:
return issues + ["EXPECTED_ONE_EXAMPLE_SUBSCRIPTION"]
sub = subs[0]
if not isinstance(sub, dict):
return issues + ["BAD_SUBSCRIPTION"]
if sub.get("topics") != ["products/update"]:
issues.append("TOPIC_MISMATCH")
if sub.get("filter") != "vendor:Acme":
issues.append("FILTER_OUTSIDE_EXAMPLE")
fields = sub.get("include_fields")
if not isinstance(fields, list) or any(not isinstance(f, str) for f in fields):
issues.append("BAD_INCLUDE_FIELDS")
else:
if "vendor" not in fields:
issues.append("FILTER_FIELD_MISSING")
if "updated_at" not in fields:
issues.append("DEBOUNCE_RISK")
if not {"id", "title"}.issubset(fields):
issues.append("CONSUMER_FIELD_MISSING")
uri = sub.get("uri")
try:
parsed = urlsplit(uri) if isinstance(uri, str) else None
if parsed is None or parsed.scheme != "https" or not parsed.hostname:
issues.append("HTTPS_URI_REQUIRED")
except ValueError:
issues.append("HTTPS_URI_REQUIRED")
return issues
把配置片段保存为 catalog-subscription.toml,在同一目录保存函数后运行:
from pathlib import Path
issues = lint_subscription(
Path("catalog-subscription.toml").read_text(encoding="utf-8")
)
assert issues == [], issues
离线测试确认:原片段通过;删除 vendor、删除 updated_at、把字段写为 updatedAt、改成 HTTP、改错版本或主题,以及 TOML 语法损坏时都会返回对应问题。复杂过滤式要另做依赖分析和平台验收,不能靠本函数批准。
4. 沿着开发版本、候选版本、发布版本核对
- 记录输入。 保存应用配置文件名、应用标识、开发店、配置提交号、主题、URI、过滤式、裁剪字段和预期版本。后端负责人先确认目标 HTTPS 入口已部署。
- 在开发店应用配置。
shopify app dev会把配置变更应用到所选开发店。确认会话关联的是预期应用和开发店,再执行下一节的正反例测试。应用配置生效规则。 - 创建候选应用版本。 在获授权的项目中,
shopify app deploy --no-release可以创建版本而不向商家发布。检查产出的应用版本;app deploy本身不会部署自托管 Web 后端。CLI 部署与 no-release。 - 读回版本配置。 在 Dev Dashboard 打开对应应用,进入 Versions → 目标版本 → Configuration → Subscriptions,检查它属于配置管理的应用级订阅,并对照保存的主题、目标和配置。这个入口用于应用级订阅;API 查询不是其替代证据。官方管理入口。
- 经项目授权发布并复验。 发布候选版本后,记录实际发布版本与投递日志中的 Subscription Method。HTTP 头名按大小写不敏感读取;对照
X-Shopify-Topic、X-Shopify-API-Version和示例的X-Shopify-Name。配置与收到的载荷应形成同一条证据链。投递结构与头字段。
若旧流程已创建同主题、同目标的店铺级订阅,先列清单并安排受控迁移,避免双重通知。官方迁移步骤涉及清理旧订阅,必须按应用实际状态安排,不能见到空查询就盲目新增或删除。从店铺级迁移至应用级。
5. 用正反例证明过滤生效,不能只看接口收到请求
在获授权开发店准备可识别的测试商品,记录改动前后值和触发时间。每个观察都应对应具体应用版本、商品与操作;用同主题正例确认观察链路可用,再判断反例。
| 开发店操作 | 预期观察 | 失败时先检查 |
|---|---|---|
| Acme 商品修改标题 | 满足过滤,观察是否出现该商品投递 | 已生效版本、授权、URI、投递日志 |
| Acme 商品继续修改标题,供应商未变 | 仍满足过滤;它不是供应商变更监听 | 是否把当前值过滤误当字段变化过滤 |
| 非 Acme 商品修改标题 | 不满足过滤,观察窗口内不应出现对应投递 | 当前供应商值、过滤式及并行旧订阅 |
| 快速修改未被保留的商品字段 | 比较实际载荷与 updated_at | 是否因裁剪形成相同载荷及去抖 |
CLI 的 webhook trigger 会发送合成载荷到指定目标,适合检查处理器接收链路;它不能证明商家事件经过这份订阅的过滤条件。因此过滤验收需要上述开发店操作,而不是仅凭一次合成请求成功。官方测试范围。
完成标志是:目标发布版本的订阅配置可读回、正反例有可定位的观察记录、收到的版本与字段符合约定,并且异常能归到配置、授权、目标后端或裁剪策略。若需要回退,应用版本配置与后端部署分别处理;无法由投递记录覆盖的同步缺口交给既有对账流程。原始请求验签、重复处理和死信恢复可继续阅读 Webhook 重放防护与恢复。