PAYATHON 2026

Azure Price Sheet API 返回 404 错误,如何排查?

支付老李

结论

这段代码的认证流程基本正确,Price Sheet URL 也符合订阅级 Get By Billing Period 接口的格式。HTTP 404 通常不是 Python 代码本身造成的,而是 Azure 无法在当前订阅范围内找到指定的价格表资源。

建议先检查以下三项:

  1. billingPeriodName="202407" 是否是该订阅中真实存在且允许查询的账期。
  2. 当前订阅的计费协议是否支持订阅级 Price Sheet API。
  3. 服务主体是否位于正确的租户中,并拥有读取成本和计费数据的权限。

代码还有一个变量名错误。最后打印了未定义的 pricesheet_data,实际变量名是 retail_prices_data。这个错误不会导致 HTTP 404,但请求成功后会触发 NameError。

查看 Azure 返回的错误正文

当前代码直接调用 raise_for_status(),可能会漏掉 Azure 返回的具体错误代码和消息。调用前应先打印响应内容和请求跟踪标识:

response = requests.get(retail_prices_url, headers=headers, timeout=60)

print("Request URL:", response.request.url)
print("Status:", response.status_code)
print("Response:", response.text)
print("x-ms-request-id:", response.headers.get("x-ms-request-id"))
print("x-ms-correlation-request-id:",
      response.headers.get("x-ms-correlation-request-id"))

response.raise_for_status()

Azure 的 JSON 响应通常采用下面的结构:

{
  "error": {
    "code": "...",
    "message": "..."
  }
}

排查时应以 error.code 和 error.message 为准。仅凭 HTTP 404,无法判断是账期不存在、计费模型不支持、订阅范围错误,还是资源被隐藏。

测试不带账期的当前价格表接口

要判断问题是否与 billingPeriodName 有关,可以先调用当前价格表接口:

GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Consumption/pricesheets/default?api-version=2023-05-01

对应代码如下:

current_pricesheet_url = (
    f"https://management.azure.com/subscriptions/{subscription_id}"
    "/providers/Microsoft.Consumption/pricesheets/default"
    "?api-version=2023-05-01"
)

response = requests.get(
    current_pricesheet_url,
    headers=headers,
    timeout=60
)

print(response.status_code)
print(response.text)
response.raise_for_status()

pricesheet_data = response.json()

可以根据结果继续判断:

  • 如果当前价格表请求成功,而指定账期返回 404,202407 很可能不存在、无法查询,或者该账期没有生成价格表。
  • 如果两个接口都返回 404,应重点检查订阅的计费协议、租户、订阅范围,以及该 API 是否适用于当前订阅。
  • 如果返回 401,通常表示令牌无效、已过期或受众不正确。
  • 如果返回 403,说明身份已经通过认证,但缺少读取成本或计费数据的权限。

不要随意把月份拼成 YYYYMM。即使格式正确,该账期也必须真实存在于对应订阅的计费上下文中。尚未关闭的月份、早于订阅创建时间的月份,或者超出服务保留范围的历史月份,都可能没有可用的价格表。

确认订阅的计费协议

Price Sheet 并非以同一种方式向所有 Azure 订阅类型提供。订阅可能属于:

  • Enterprise Agreement(EA)
  • Microsoft Customer Agreement(MCA)
  • Microsoft Partner Agreement(MPA)
  • CSP
  • Web Direct 或其他在线订阅
  • Sponsorship、Visual Studio、试用等特殊优惠类型

不同协议可能使用不同的计费范围,例如:

  • subscription
  • billing account
  • billing profile
  • invoice section
  • enrollment

当前代码使用订阅范围:

/subscriptions/{subscriptionId}/...

如果价格表实际属于 billing account 或 billing profile,在订阅范围内发起请求时可能找不到对应资源。可以先在 Azure 门户的”成本管理 + 计费”中确认:

  1. 订阅所属的计费帐户类型。
  2. 当前账号能否看到 Billing account、Billing profile 或 Enrollment。
  3. 该协议对应的官方 Price Sheet API 使用哪种 scope。
  4. 是否应改用 billing profile、billing account 或 EA enrollment 范围的价格表下载接口。

不要只替换路径中的 provider 名称。不同计费协议不仅使用不同的 URL,所需权限、可用字段和账期表示方式也可能不同。

检查租户和服务主体权限

成功获取令牌,只能证明 client_id 和 client_secret 有效,不能证明该身份有权读取目标订阅。

至少需要确认以下内容:

  • subscriptionId 属于 tenant_id 指定的租户,或者服务主体已经被正确邀请并授权。
  • 角色分配给服务主体对应的 Enterprise application,而不是 App registration 对象本身。
  • 角色分配范围覆盖目标订阅或对应的计费范围。
  • 角色允许读取成本和价格数据,例如适用范围内的 Cost Management Reader,或该计费协议要求的同等计费角色。
  • 如果访问 billing account 或 billing profile,仅有订阅 RBAC 角色可能不够,还需要相应的计费范围权限。

为避免泄露资源是否存在,部分 Azure 接口会返回 404,而不是更直观的 403。因此,不能因为收到 404 就排除租户或权限问题。

修正后的诊断代码

下面的示例分别测试当前价格表和指定账期的价格表,并完整输出 Azure 返回的错误信息:

import json
import requests

tenant_id = "xxx"
client_id = "yyy"
client_secret = "zzz"
subscription_id = "www"
billing_period_name = "202407"

token_url = (
    f"https://login.microsoftonline.com/{tenant_id}"
    "/oauth2/v2.0/token"
)

token_data = {
    "grant_type": "client_credentials",
    "client_id": client_id,
    "client_secret": client_secret,
    "scope": "https://management.azure.com/.default",
}

token_response = requests.post(
    token_url,
    data=token_data,
    timeout=30
)
token_response.raise_for_status()

access_token = token_response.json()["access_token"]

headers = {
    "Authorization": f"Bearer {access_token}",
    "Accept": "application/json",
}

current_url = (
    f"https://management.azure.com/subscriptions/{subscription_id}"
    "/providers/Microsoft.Consumption/pricesheets/default"
    "?api-version=2023-05-01"
)

billing_period_url = (
    f"https://management.azure.com/subscriptions/{subscription_id}"
    "/providers/Microsoft.Billing"
    f"/billingPeriods/{billing_period_name}"
    "/providers/Microsoft.Consumption/pricesheets/default"
    "?api-version=2023-05-01"
)


def request_pricesheet(name, url):
    response = requests.get(url, headers=headers, timeout=60)

    print(f"\n{name}")
    print("URL:", response.request.url)
    print("HTTP status:", response.status_code)
    print("x-ms-request-id:", response.headers.get("x-ms-request-id"))
    print(
        "x-ms-correlation-request-id:",
        response.headers.get("x-ms-correlation-request-id")
    )

    try:
        print(json.dumps(response.json(), indent=2))
    except ValueError:
        print(response.text)

    response.raise_for_status()
    return response.json()


current_pricesheet = request_pricesheet(
    "Current price sheet",
    current_url
)

historical_pricesheet = request_pricesheet(
    f"Price sheet for {billing_period_name}",
    billing_period_url
)

如果当前只需要定位问题,可以先注释掉第二次调用,确认当前价格表接口能否正常使用。

关于折扣价格

Price Sheet 返回的是特定计费协议和计费范围内可用的计价信息。如果接口和计费协议支持,相关价格字段可能包含协商价格或合同价格,但这些数据不能直接视为某项资源的最终账单金额。

最终费用还可能受到以下因素影响:

  • Reserved Instances
  • Savings Plan
  • Azure Hybrid Benefit
  • 免费额度和赠金
  • 阶梯计价
  • 承诺消费
  • 月中价格调整
  • 税费
  • Marketplace 费用
  • 退款和人工调整

如果要核对”实际产生了多少费用”,还需要结合 Cost Details、Usage Details 或账单明细,不能只依赖 Price Sheet。

代码中的其他问题

原代码的最后一行:

print(pricesheet_data)

应改为:

print(retail_prices_data)

下面这次调用是重复的,可以删除:

response.raise_for_status()

也不要打印 token_data,其中包含 client_secret:

print(token_data)

生产环境中应把密钥存放在环境变量或密钥管理服务中,同时避免在日志里输出访问令牌、客户端密钥和完整的错误请求头。

建议按这个顺序定位问题:先读取 404 响应中的 JSON 错误正文,再测试不带账期的当前价格表接口,然后确认订阅的计费协议和正确的 scope,最后检查服务主体在订阅范围和计费范围内的权限。

备注:内容仅供参考。