构建支持请求 API

CloudflareBeginner
立即练习

简介

支持表单需要一个 API,用于区分有效请求、错误的 JSON、缺失的路由和不可用的工单服务。你将使用 JavaScript 构建这个 HTTP 边界,在本地测试,然后将它与一个临时上游 Worker 一起部署。

使用你自己的 Cloudflare 学习账号,以及在连接实验中学到的设备授权知识。本实验从一个全新的 VM 开始,其中已在 /home/labex/project/support-api 安装 Node.js 22.22.0 和项目本地的 Wrangler 4.131.1。前置要求是了解基本的 JavaScript 函数、对象和模块;本实验会讲解 HTTP 行为和异步请求。两个公开 Worker 仅使用合成数据。提供的上游服务会确认请求,但不会存储任何内容,因此它不是持久化工单系统。完成这个小练习只需要 Workers Free 和 workers.dev 子域名,不需要数据库、购买的域名或付费升级。请求会计入你账号的 Workers 使用量。

结束实验前,你将删除两个 Worker 并退出登录。保持使用同一个终端,以保留用于资源名称和 URL 的 Shell 变量。

按路径和方法路由请求

在本步骤中,你将为每个受支持的 URL 指定明确的方法和响应。路径标识要执行的操作,方法描述执行的动作。GET /health 用于检查可用性,POST /requests 将接收支持请求。

进入准备好的项目并确认工具版本:

cd /home/labex/project/support-api
node --version
npx wrangler --version

预期 Node 输出 v22.22.0,Wrangler 输出 4.131.1。工具已经安装完成;如果在自己的计算机上操作,请安装 Node,并在项目中运行 npm install --save-dev wrangler@4.131.1。使用项目锁定文件复现项目时,请运行 npm ci

生成一个唯一的临时名称。openssl rand -hex 6 会生成 12 个随机十六进制字符;$(...) 会插入该输出,Shell 赋值则会保存这个名称,供后续命令使用。

WORKER_NAME="labex-support-$(openssl rand -hex 6)"

写入标准的 Wrangler 配置。cat > file <<MARKER 会写入后续各行,直到遇到结束标记;未加引号的标记允许 Shell 替换 $WORKER_NAME

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "vars": {"UPSTREAM_URL": "http://127.0.0.1:8081"}
}
CONFIG

main 指定处理程序,compatibility_date 选择运行时行为,vars 则通过 env 提供非机密的上游地址。目前该地址指向稍后启动的本地固定服务。这里禁用公开预览 URL,以简化资源清单。

写入处理程序。带引号的 JS 标记会按原样保留 JavaScript。new URL(...).pathname 提取路由。三元表达式选择允许的方法;HTTP 405 响应还会通过 Allow 标头公布该方法。Response.json 会将对象序列化为 JSON,并设置内容类型。async 处理程序可以在后续步骤中等待异步操作。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    return Response.json({error: 'not_implemented'}, {status: 501});
  }
};
JS

在后台启动本地 Wrangler:> 会重定向输出,2>&1 会把错误也包含进来,& 会在服务器运行时立即返回终端提示符。

npx wrangler dev --port 8080 > api.log 2>&1 &
cat api.log

等待日志报告 8080 端口已准备就绪。如果服务器仍在启动,请重新运行 cat api.logcurl -i 会同时显示 HTTP 状态和标头:

curl -i http://127.0.0.1:8080/health
curl -i http://127.0.0.1:8080/missing
curl -i http://127.0.0.1:8080/requests

预期结果依次为:状态码 200 和 {"status":"ok"};状态码 404 和 {"error":"not_found"};状态码 405、{"error":"method_not_allowed"} 以及 Allow: POST。这些错误响应是有意设计的。服务器仍在运行时,点击验证按钮。

解析并验证 JSON 输入

在本步骤中,你将先拒绝格式错误的输入,再调用任何上游服务。HTTP 415 表示不支持该媒体类型,400 表示无法解析 JSON,422 表示解析后的数据不符合约定。subject 必须是字符串,去除空白后长度为 1–80 个字符。

使用下面的完整版本替换处理程序。headers.get 读取声明的媒体类型;在 ; 处分割可以允许字符集参数。await request.json() 会等待解析,并且只读取一次请求正文。try/catch 会将解析异常转换为可预测的响应。JSON 还可以表示 null、数组或数字,因此验证会先检查数据形状,再使用字符串方法。trim() 会规范化通过验证的 subject

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    return Response.json({subject}, {status: 201});
  }
};
JS

源代码发生变化后,Wrangler 会重新加载。运行 cat api.log 检查是否存在编译错误。发送有效请求:-H 提供标头,--data 提供请求正文并选择 POST 方法。单引号可以让 Shell 保留 JSON 中的双引号。

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"  Printer offline  "}'

预期状态码为 201,响应为 {"subject":"Printer offline"}。这是内存中的确认,不是已保存的工单。测试三种不同的拒绝路径:

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"   "}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: text/plain" --data 'hello'

预期结果分别为 400 invalid_json、422 invalid_subject 和 415 unsupported_media_type。另外尝试 JSON null[]{"subject":5};每种情况都必须返回 422,而不能抛出异常。点击验证按钮;验证器会检查这些边界条件,同时保留健康检查和路由行为。

调用上游并隔离其错误

在本步骤中,你将把 API 连接到提供的工单服务模拟器。上游服务是你的服务所调用的依赖项。对于普通主题,模拟器会返回一个合成工单;对于特殊主题 simulate-outage,会返回 HTTP 503;它从不存储请求。

查看提供的源代码以了解固定服务,然后为它配置一个唯一的 Worker 名称:

cat upstream/index.js
cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false
}
CONFIG

--config 用于选择第二个配置文件。使用 8081 端口和单独的调试器端口,这样两个本地 Worker 就可以同时运行:

npx wrangler dev --config upstream/wrangler.jsonc --port 8081 --inspector-port 9230 > upstream.log 2>&1 &
cat upstream.log
curl -i http://127.0.0.1:8081/health

等待服务准备就绪,预期状态码为 200,响应为 {"service":"support-upstream","status":"ok"}。现在使用下面的完整集成版本替换主处理程序。全局 fetch 函数用于发出出站请求;JSON.stringify 会对经过验证的主题进行编码。await 会等待响应。HTTP 错误不会抛出异常,因此需要通过 upstream.ok 显式检查状态;catch 则单独处理连接失败或无法读取 JSON 响应的情况。HTTP 502 会告知客户端依赖项失败,同时不会暴露上游响应正文。

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    try {
      const upstream = await fetch(`${env.UPSTREAM_URL}/tickets`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({subject})
      });
      if (!upstream.ok) {
        return Response.json({error: 'upstream_unavailable'}, {status: 502});
      }
      const ticket = await upstream.json();
      return Response.json({ticket: ticket.ticket, subject}, {status: 201});
    } catch {
      return Response.json({error: 'upstream_unavailable'}, {status: 502});
    }
  }
};
JS
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'

预期第一个请求返回 201 和 {"ticket":"demo-1001","subject":"Printer offline"},第二个请求返回 502 和 {"error":"upstream_unavailable"}。模拟器的内部诊断信息不得出现在响应中。主题是合成数据,此端点不会产生持久化副作用。两个本地服务器都运行时,点击验证按钮。

本实验使用普通 HTTP 来练习外部服务边界。后续实验会讲解用于 Worker 之间内部调用的服务绑定。Diagnose Worker Failures 实验会讲解有限超时和更丰富的诊断信息。这里的固定服务只返回大小受限的小型响应;生产 API 还必须限制不可信请求和响应的大小。

部署并测试公开 API

在本步骤中,你将把两个 Worker 部署到同一个学习账号,并将本地上游地址替换为其公开 URL。先停止两个本地任务。查看 jobs 的结果,并使用实际的任务编号;以下示例假设 API 是 1,上游是 2。

jobs
kill %1 %2

为这个全新的 VM 授权。该授权会识别你的账号,并允许部署和删除 Worker。末尾的权限与部署课程中的授权范围一致,但本实验不需要日志流。

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

打开显示的浏览器链接,输入当前设备代码,查看 Wrangler 请求的权限(包括所需的 Background Access),仅选择你的学习账号,然后完成授权。返回终端并等待授权完成。

npx wrangler whoami --json

确认输出中的 loggedIn: true、账号名称,以及 accounts 中显示的实际账号 ID。将下面的 YOUR_ACCOUNT_ID 替换为该 ID。保留步骤 1 中生成的名称;如果变量丢失,请读取已保存的配置并恢复它,不要重新生成其他资源名称。

cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy --config upstream/wrangler.jsonc

从部署输出中复制完整、准确的 workers.dev URL。复用账号已有的子域名。如果 Wrangler 提示首次注册子域名,请选择一个可用名称并按照确认流程操作;不要更改账号已有的子域名。

现在重写主配置,将两个占位符替换为你的账号 ID 和上游 URL(末尾不要带斜杠)。global_fetch_strictly_public 会让出站 fetch() 使用公共互联网路由,包括访问该账号 workers.dev 子域名上的另一个 Worker。如果没有此配置,即使两个 Worker 可以分别正常运行,这个同区域 HTTP 调用也可能失败。该标志应放在已部署的 API 配置中;之前的本地回环固定服务不需要它。请参阅 Fetch API 指南

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "compatibility_flags": ["global_fetch_strictly_public"],
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID",
  "vars": {"UPSTREAM_URL": "YOUR_UPSTREAM_URL"}
}
CONFIG
cat wrangler.jsonc
npx wrangler deploy

将主 API 的部署输出中的 URL 复制到下面的变量中:

API_URL="https://YOUR_API.YOUR_SUBDOMAIN.workers.dev"
curl -i "$API_URL/health"
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{'

预期结果与本地测试相同:健康检查返回 200,合成工单返回 201,上游错误返回 502,格式错误的 JSON 返回 400。遇到连接错误时,请等待主机名传播后再重试。在 Dashboard 中选择同一个学习账号,然后打开 Compute → Workers & Pages。找到两个准确的名称,并将其地址与部署输出进行比较。这是一个只读检查点;不要在其中创建重复应用。

下面的示例展示了主 API 及其对应的 -upstream 服务。在左侧边栏展开 Compute,选择 Workers & Pages。如果账号中包含其他项目,请使用 Search applications。将完整生成的名称及其下方的地址与两次部署输出进行比较;你的随机后缀和账号子域名会与此示例不同。

Workers and Pages showing the support API and its matching upstream Worker

两个资源都应出现在当前选中的账号中。它们的存在可以确认部署位置;上面的 HTTP 响应则用于确认 API 是否正常工作。如果缺少任意一个名称,请检查账号选择器和部署输出,然后再重试。不要使用 Create application 来复制 CLI 部署的应用。

点击验证按钮。验证器会独立检查两个 Worker 的归属、已部署的上游绑定,以及公开 API 的正向和负向响应。它只会向本实验的模拟器发送合成的无状态请求。

删除两个临时 Worker

在本步骤中,你将在授权仍然有效时删除 API 及其上游,以便验证删除结果。这些是本实验创建的唯一云资源。删除前先检查两个配置:

cat wrangler.jsonc
cat upstream/wrangler.jsonc

确认主 Worker 使用 labex-support-... 名称,并且对应的上游名称带有 -upstream 后缀,同时使用同一个学习账号 ID。先删除主 API,再删除上游。在每个确认提示处检查准确的名称,然后按一次 y 键。

npx wrangler delete
npx wrangler delete --config upstream/wrangler.jsonc

Wrangler 4.131.1 可能会删除 Worker,然后在检查旧版 Workers Sites KV 数据时打印身份验证错误,因为当前授权没有 KV 访问权限。这个特定诊断信息不能证明删除成功或失败。不要仅为了消除该信息而授予更多权限。刷新 Workers & Pages 并点击验证按钮:成功的授权资源清单必须确认两个名称都不存在。网络错误或授权错误无法得出结论;请先解决这些问题再继续。保留其他应用、学习账号及其子域名。

断开 VM 授权

在本步骤中,确认两个资源都已清理后,你将移除这个 VM 的 Wrangler 授权。退出登录不会删除 Worker,因此必须先完成清理。

npx wrangler logout
npx wrangler whoami --json

预期结果明确显示 "loggedIn": false。未认证的状态命令可能以非零状态退出;只要其结构化结果清楚报告已退出登录,这就是预期行为。网络错误不等同于退出登录。点击验证按钮,然后结束 LabEx 环境。你的浏览器登录状态和学习账号仍可用于后续实验;每个新 VM 都需要单独授权。

总结

你构建了一个按 HTTP 方法处理请求的 API,解析并验证了 JSON,规范化了通过验证的数据,并将上游故障转换为可预测的公开错误。你在本地和 Cloudflare 上测试了正常请求及被拒绝的请求,检查了两个部署的归属,删除了临时资源,并断开了 VM 的授权。

如需参考,请查看官方的 Request APIResponse APIFetch API