Azure Web App 中的 Bot 返回 PaymentRequired 错误
结论
PaymentRequired 不是 Azure Web App 返回的”网站欠费”提示,也不一定与 LUIS 有关。从堆栈看,异常发生在 Bot Framework SDK 执行 ReplyToActivityAsync 时。bot 已经收到请求,但在向传入 Activity 的 serviceUrl 发送回复时,目标服务返回了 HTTP 402 Payment Required。
最常见的原因是把 Bot Framework 的 /api/messages 端点当成普通 REST API 直接调用。该端点接收的不是任意 JSON 消息,而是由 Bot Framework Channel、Direct Line、Web Chat 或 Emulator 生成并认证的 Activity。手工构造请求时,如果 serviceUrl、channelId、conversation.id、认证令牌或 Bot 注册信息不匹配,bot 就无法通过 Connector API 回发消息。
App Service Plan 的问题需要单独处理。Bot Service 的 S1 定价层与承载 Web App 的 App Service Plan 是两个不同的计费资源,Web App 可能仍在使用 Default0 免费计划。
为什么堆栈指向回复阶段
关键调用是:
Microsoft.Bot.Connector.Conversations.ReplyToActivityWithHttpMessagesAsync
Microsoft.Bot.Connector.ConversationsExtensions.ReplyToActivityAsync
流程大致如下:
REST 客户端
→ POST /api/messages
→ bot 处理 Activity
→ ReplyToActivityAsync
→ 请求 Activity.serviceUrl
→ 对方返回 402
因此,只确认 /api/messages 返回异常还不够。还要检查 bot 实际回调的 URL,以及该 URL 返回的响应正文。外层 ErrorResponseException 往往只保留状态名称,真正有用的信息可能在 Connector 返回的响应内容中。
浏览器能打开默认 HTML 首页,只能证明 Web App 正在运行。Emulator 和 Azure Portal 测试成功,说明它们构造的 Activity、认证信息和回复地址有效,但不能证明任意 REST 请求都符合 Bot Framework 协议。
正确调用方式
方案一:普通应用通过 Direct Line 调用
如果需要让自定义网页、移动端或后端系统与 bot 对话,应使用 Direct Line,不要直接向 /api/messages 发送 POST 请求。
典型流程如下:
- 在 Azure Bot 资源中启用 Direct Line channel。
- 在可信后端使用 Direct Line secret 换取 token。
- 创建 conversation。
- 向 Direct Line conversation 发送 Activity。
- 通过轮询或 WebSocket 获取 bot 回复。
发送消息的请求类似:
POST https://directline.botframework.com/v3/directline/conversations/{conversationId}/activities
Authorization: Bearer {directLineToken}
Content-Type: application/json
{
"type": "message",
"from": {
"id": "user-123"
},
"text": "Hello"
}
Direct Line 会管理 serviceUrl、conversation 状态和 Connector 协议,客户端不需要伪造这些字段。
不要把 Direct Line secret 放在浏览器或移动客户端中。客户端应使用短期 token,secret 只能保存在可信服务器端。
方案二:确实需要自定义 REST API
如果只需通过 HTTP 提交文本并同步获取结果,应另外创建普通 API,例如 /api/query,不要复用 Bot Framework 的 /api/messages。
概念示例:
[RoutePrefix("api/query")]
public class QueryController : ApiController
{
[HttpPost]
[Route("")]
public async Task<IHttpActionResult> Post(QueryRequest request)
{
if (request == null || string.IsNullOrWhiteSpace(request.Text))
{
return BadRequest("Text is required.");
}
var result = await MyConversationService.ProcessAsync(request.Text);
return Ok(new { reply = result });
}
}
public sealed class QueryRequest
{
public string Text { get; set; }
}
实际项目还应加入身份验证、会话状态、超时控制和速率限制。如果业务逻辑目前与 IDialogContext 或 Connector Activity 紧密耦合,可以先把核心处理逻辑抽取为独立服务,再由 Bot Framework endpoint 和自定义 REST endpoint 分别调用。
如何确定 402 的具体来源
1. 记录传入 Activity 的路由字段
重点检查:
var activity = await Request.Content.ReadAsAsync<Activity>();
Trace.TraceInformation(
"type={0}, channelId={1}, serviceUrl={2}, conversationId={3}, activityId={4}",
activity.Type,
activity.ChannelId,
activity.ServiceUrl,
activity.Conversation?.Id,
activity.Id);
不要在生产日志中记录 access token、Authorization header、Direct Line secret 或完整的用户隐私数据。
如果 REST 请求中的 serviceUrl 是自行填写的、为空、指向 Web App 本身,或者来自另一套环境,回复失败属于预期结果。serviceUrl 不是”我的 bot 地址”,而是 bot 应将回复发送到的 Channel 或 Connector 地址。
2. 捕获 Connector 的完整错误内容
不同版本的 Bot Builder SDK 可能公开不同的异常属性。以下代码只表示诊断思路,应根据项目实际引用的 SDK 类型调整:
try
{
await context.PostAsync("Reply");
}
catch (Microsoft.Bot.Connector.ErrorResponseException ex)
{
Trace.TraceError(
"Connector reply failed. Status={0}, Response={1}",
ex.Response?.StatusCode,
ex.Response?.Content);
throw;
}
同时检查:
- Application Insights 中的 exceptions、dependencies 和 traces;
- App Service 的 Application Logging;
- 失败依赖项的目标主机和响应正文;
- Azure Bot 的 channel 配置与运行状态。
如果失败依赖项指向 *.botframework.com,问题出在 Connector 或 Channel 的回复链路。如果指向其他域名,则要继续检查对应的外部服务。
3. 对比成功和失败的 Activity
逐项对比 Emulator、Portal Web Chat 和手工 REST 请求中的以下字段:
typeidtimestampserviceUrlchannelIdfrom.idrecipient.idconversation.idreplyToId- HTTP
Authorizationheader
不要复制成功请求中的 conversation.id、activity.id 或 token 后直接重放。这些值通常受会话或环境限制。
4. 检查认证配置
确认 Web App 配置中的 Bot 身份信息与 Azure Bot 注册一致。旧版 SDK 常用的配置包括:
MicrosoftAppId
MicrosoftAppPassword
较新的 Bot Framework 配置还可能涉及租户 ID、应用类型或托管身份,具体取决于所用 SDK 和部署方式。
如果 Emulator、Portal 测试和正式 Channel 都能持续成功,应用凭据整体错误的可能性较低。此时应优先检查手工 REST 请求的认证和 serviceUrl。
修复 App Service Plan 配置
1. 区分两个 S1
Azure 中可能同时存在名称相近但互相独立的定价层:
- Azure Bot 或 Bot Service 的定价层;
- App Service Plan 的计算层。
Bot 资源显示 S1,不代表 Web App 已运行在 App Service Standard S1。Web App 使用的计算计划应在以下位置确认:
Web App → App Service plan
如果这里显示 Default0 (Free),承载该 Web App 的仍是免费计划。
2. 为什么显示 “No App Service Plans Found”
“Change App Service Plan”不会列出订阅中的所有计划。候选计划通常必须满足 Azure 对迁移范围的限制,包括区域、操作系统类型和底层部署环境兼容性。门户界面和具体限制可能随 Azure 更新,应以当前门户提示和 Azure Resource Manager 返回的信息为准。
常见情况包括:
- 现有 S1 计划位于不同区域;
- Windows Web App 与 Linux App Service Plan 不匹配;
- 计划位于不兼容的资源环境;
- 当前订阅、目录或资源筛选不正确;
- 只升级了 Bot Service 定价层,没有创建可用的 App Service Plan。
不能简单地把资源组不同视为唯一原因。应检查目标计划是否符合 Azure 当前允许的迁移条件。
3. 推荐处理顺序
先在 Web App 的 Overview 或 Properties 中确认:
- Subscription
- Resource group
- Location
- Operating system
- 当前 App Service Plan 的资源 ID
然后按以下顺序处理:
- 打开 Web App 的 Scale up (App Service plan)。
- 如果只需升级当前
Default0,直接选择 App Service Standard S1 并应用。 - 如果必须迁移到另一个计划,创建一个与 Web App 区域和操作系统兼容的 App Service Plan。
- 使用 Change App Service Plan 选择该计划。
- 如果仍没有候选项,检查订阅和目录是否正确,并通过 Activity Log 查看门户操作失败的详细原因。
如果现有 Web App 无法迁移到目标计划,可以在目标计划中创建新的 Web App,复制应用设置并重新部署,然后更新 Azure Bot 的 Messaging endpoint,例如:
https://{new-app-name}.azurewebsites.net/api/messages
迁移时还要同步连接字符串、身份配置、证书、应用设置和访问限制。
注意事项
402 Payment Required 只是 HTTP 状态名称。不能仅凭这个名称判断 App Service 欠费或实例规格不足,必须结合失败请求的目标 URL 和响应正文分析。
App Service 免费计划可能存在配额、休眠和性能限制,但这些问题通常无法解释 ReplyToActivityAsync 为什么会针对某个特定 serviceUrl 返回 402。除非日志明确显示 App Service 配额或订阅状态异常,否则应分别排查 Connector 回复问题和 App Service Plan 配置问题。
最直接的修复方式通常是停止把 /api/messages 当作通用 REST API,改用 Direct Line。如果业务需要同步 REST 接口,则新增独立端点。同时,应在 Web App 的 Scale up 页面升级实际使用的 Default0 App Service Plan,而不是只修改 Bot Service 的 S1 定价层。
备注:内容仅供参考。
