Skip to content

全栈 Serverless 架构避坑实录:阿里云 API 网关 Body 转换问题

问题背景:在实现支付宝回调(Alipay webhook)时,遇到 application/x-www-form-urlencoded 格式的请求体被转换为 JSON 对象的问题。本文深入分析问题链路、平台行为及解决方案。

项目架构:ServerlessInsight 全栈 Serverless 架构

ServerlessInsight Architecture

在深入问题细节之前,先介绍一下项目架构——一个经典的 Serverless API 架构,旨在实现零运维、自动伸缩与低成本运行。

在此架构中,我们使用了以下两个开源框架构建 Serverless 应用:

  1. ServerlessInsight:作为架构的配置与编排层。它负责管理云资源(如函数、触发器、API 网关路由),内置了对阿里云服务的适配逻辑,默认采用 PASSTHROUGH(透传)模式配置 API Gateway,以追求零配置的开发体验。

    • 自动化资源管理:无需在云控制台手动创建函数、配置触发器或绑定域名。ServerlessInsight 通过代码定义一切,自动化地编排和管理云资源(FC 实例、API 网关路由、权限策略等)。
    • 多云适配的通用策略:为了适配阿里云、腾讯云、AWS 等不同云厂商的环境,ServerlessInsight 在设计 API 网关配置时采取了"最大公约数"策略,默认采用 PASSTHROUGH(透传) 模式。
  2. serverless-adapter:作为架构的运行时适配层。它位于函数计算(FC)与业务框架(如 Express/Koa)之间,核心职责是将云厂商异构的 event 事件对象统一转换为标准的 HTTP 请求对象(req),让开发者能像写传统 Web 应用一样写 Serverless 代码。

正是这两个框架与阿里云平台机制的交互,引发了我们今天要讨论的问题。


问题现象:API 配置与预期发生转变

为了更直观地理解问题,先来看一下具体的 API 接口配置场景 以及 请求参数 在链路中的流转变化。

Alipay Webhook Request Flow

在实际业务中,我们通常会通过 ServerlessInsight 部署一个处理支付回调的接口。

  • 客户端(如支付宝)

    • 请求方式POST
    • 请求头Content-Type: application/x-www-form-urlencoded
    • 请求体:标准的键值对字符串
  • 服务端(阿里云环境)

    • API Gateway 配置:默认 RequestMode: PASSTHROUGH
    • 后端服务:Function Compute 运行 Node.js (Express) 代码

按照常理,PASSTHROUGH 模式应该将原始参数透传给后端。但在处理 x-www-form-urlencoded 数据时,链路却发生了意想不到的转变。

预期行为

支付宝回调使用 application/x-www-form-urlencoded 格式发送请求:

http
POST /api/v1/webhooks/alipay HTTP/1.1
Content-Type: application/x-www-form-urlencoded

notify_time=2024-01-01+12:00:00&notify_type=trade_status_sync&sign_type=RSA2&sign=xxx&...

实际行为

在 Function Compute 中收到的 event.body 已经是 JSON 对象:

javascript
// event.body 的值
{
  "notify_time": "2024-01-01 12:00:00",
  "notify_type": "trade_status_sync",
  "sign_type": "RSA2",
  "sign": "xxx",
  // ...
}

这导致签名验证失败,因为签名是基于原始 urlencoded 字符串计算的。


深度链路剖析:为什么 Body 变成了 Object?

为了解决这个问题,我们需要深入代码层面,理清 serverless-adapter 与 API Gateway 之间的交互逻辑。

RequestMode 三种模式

根据阿里云官方文档,API Gateway 支持三种入参请求模式:

模式中文名称行为描述
MAPPING(过滤未知参数)入参映射(过滤未知参数)Query、Path、Body Form 参数需配置前后端映射,网关只透传配置的参数
MAPPING(透传未知参数)入参映射(透传未知参数)除配置的参数外,其他参数透明传递
PASSTHROUGH入参透传不需要配置 Query、Body Form 参数,客户端传给网关的参数都会被透传给后端

阿里云 API Gateway 的"伪透传"

API Gateway 的 PASSTHROUGH 模式,其定义的"透传"是指参数级别的透传,而非原始请求格式的透传

  • 机制:为了适配后端函数计算(FC)的 JSON 事件结构,网关在转发请求时会自动解析 application/x-www-form-urlencoded 格式。
  • 结果:它将 key=value&key=value 拆解,组装成一个 JSON 对象赋值给 event.body。此时,原始的字符串形态已经丢失。

这种设计旨在通过标准化事件格式,提升函数计算后端在多语言环境下的兼容性与开发体验。

serverless-adapter 的处理

serverless-adapter 的核心逻辑位于 requestBody 函数中,它需要处理不同类型的 event.body

typescript
const requestBody = (event) => {
  if (typeof event.body === 'string') {
    // 如果是字符串,直接处理
  } else if (typeof event.body === 'object') {
    // 如果是对象(来自 API Gateway 的解析结果)
    return Buffer.from(JSON.stringify(event.body)); // ⚠️ 问题点
  }
}

逻辑冲突

  • API Gateway 认为:我把参数解析成对象给你(FC),你用起来更方便。
  • serverless-adapter 认为:我收到了一个对象,为了给 Express 框架使用,我需要把它序列化回字符串。
  • 最终后果:原始的 a=1&b=2 变成了 {"a":"1","b":"2"}。对于支付回调这种强依赖原始 Body 的场景,这就导致了验签失败。

核心解决方案:应用层兼容处理

这个方案的核心思想是:既然无法改变网关层的行为,就在应用层编写兼容逻辑,灵活处理网关传递过来的、已经解析过的数据。

方案优势

  • 改动最小:无需修改网关配置或引入复杂的绕行方案,仅在业务代码层面进行调整。
  • 保留网关能力:可以继续使用 API 网关提供的流量控制、鉴权、监控等强大功能。
  • 灵活性强:能够应对不同来源、不同格式的请求,是处理第三方回调(如支付、Webhook)等复杂场景的理想选择。

实施策略与代码示例

关键在于根据 Content-Type 和请求体的实际内容,编写一个智能的解析适配器:

typescript
// src/controllers/webhook.ts
const parseBody = (
  raw: string | undefined,
  parsedBody: Record<string, string>
): Record<string, string> => {
  // 场景 1:body 已是 JSON 对象(API Gateway 已解析)
  if (parsedBody && typeof parsedBody === 'object' && Object.keys(parsedBody).length > 1) {
    const hasSign = 'sign' in parsedBody;
    if (hasSign) {
      return parsedBody;  // 直接使用已解析的对象
    }
  }

  // 场景 2:rawBody 是 JSON 字符串
  if (raw?.startsWith('{') || raw?.startsWith('[')) {
    try {
      return JSON.parse(raw);
    } catch {
      // 继续尝试 urlencoded 解析
    }
  }

  // 场景 3:rawBody 是 urlencoded 字符串
  return parseUrlencoded(raw ?? '');
};

其他方案补充说明

另外两种方案在实际操作中各有侧重,可以作为备选或特定场景下的优化思路。

方案二:增强 serverless-adapter(进阶)

这是一个更深度的框架层改造方案。

  • 核心逻辑:修改 Serverless 框架的适配器,在它将事件(Event)转换为标准的 req 对象时增加特殊逻辑。例如,当 Content-Typeapplication/x-www-form-urlencoded 时,不是简单地将对象 JSON.stringify,而是将其重新拼接成 key=value& 格式的字符串。
  • 挑战:实现复杂,需要处理对象属性顺序、特殊字符编码等问题,以保证重新拼接的字符串与客户端发送的原始字符串完全一致(这对于验签至关重要)。

方案三:使用 HTTP 触发器(绕行)

这是一个架构层面的绕行方案。

  • 优点:能够 100% 获取到原始的 HTTP 请求体和请求头,完全绕过 API 网关的预处理。
  • 缺点:放弃了 API 网关提供的一系列企业级功能,如统一的域名管理、请求限流、IP 黑白名单、日志审计等,可能会增加应用自身的安全和运维负担。

架构决策指南

在 Serverless 开发中,选择合适的方案需要权衡利弊。下表可以帮助您做出决策:

场景推荐方案理由
通用 RESTful API默认配置对于大多数 JSON 接口,网关的默认透传或解析行为完全足够,开发体验最佳
支付回调 / Webhook应用层兼容最推荐。既能利用网关能力,又能通过灵活的代码逻辑处理验签等特殊需求
需要原始 Body 的简单服务HTTP 触发器如果服务非常简单,且对原始请求有强依赖,又不需要网关功能,此方案最直接

总而言之,应用层兼容处理是应对 API 网关 Body 预处理问题的最佳实践。它体现了在分布式系统中,通过增加应用层的智能来适应基础设施层约束的务实思想。