Azure Cosmos DB HTTP 400错误求助:Retrofit连接失败排查
排查Azure Cosmos DB REST API 400错误的几个关键点
嘿,连续3天卡这个问题确实闹心,我帮你梳理下Cosmos DB REST API返回400(错误请求)的常见坑,结合你用的Retrofit+Gson场景来分析:
一、先盯紧认证头——这是400的重灾区
Cosmos DB的Authorization头生成规则特别严格,差一点都不行,你可以对照这几点检查:
- 时间戳必须精准:必须是RFC1123格式(比如
Fri, 05 Jul 2024 12:34:56 GMT),而且和Cosmos DB服务器时间差不能超过15分钟,否则直接拒。生成的时候一定要用英文Locale,别用中文的,不然月份会变成“七月”这种,签名直接无效。 - 资源路径/类型不能错:
resourceId要对应你操作的资源层级,比如操作容器里的文档,就是dbs/你的数据库ID/colls/你的容器ID;如果是操作数据库本身,就是dbs/你的数据库ID。resourceType要匹配操作类型:比如文档是docs,容器是colls,数据库是dbs。
- 签名的字符串要完整:签名的原始字符串必须包含HTTP方法(小写)、Content-Type、时间戳、资源URI、resourceType,顺序不能乱,格式要和官方要求一致,少个换行或者空格都会失败。
- 密钥别搞混:要用主密钥或者只读密钥,复制的时候别漏了末尾的等号,也别把密钥里的特殊字符转义错了。
二、Gson序列化是否符合Cosmos DB要求
Cosmos DB对请求体格式要求很严,Gson的配置容易踩坑:
- 必填字段不能少:比如创建文档时必须有
id字段,而且id不能包含特殊字符(比如/、\、?、#等)。 - 日期格式要正确:必须是ISO 8601格式(
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'),Gson默认的日期序列化可能不符合,建议手动指定:Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .create(); - 别序列化多余字段:如果你的实体类里有Cosmos DB不认可的字段,可能会被拒,要么用
@Expose注解控制,要么用自定义序列化器。
三、Retrofit的配置有没有遗漏
- 必须加的请求头不能忘:
x-ms-version:指定API版本,比如2023-03-15,这个没设置肯定400。Content-Type:必须是application/json,别写错成别的类型。
- 动态头要用拦截器:
x-ms-date和Authorization是动态生成的,不能用静态的@Headers注解,得用OkHttp拦截器来添加,不然每次请求的时间戳都是固定的,很快就过期了。
四、调试技巧:把请求打出来看看
建议你给Retrofit加个日志拦截器,把完整的请求头、请求体都打印出来,和官方示例对比:
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build();
然后把打印出来的请求拿到Postman里手动发一遍,如果Postman能成功,那问题肯定在代码的请求构造上;如果Postman也失败,那就是资源配置或者密钥的问题。
附一个可参考的认证拦截器示例
我给你写个简单的拦截器模板,你可以根据自己的需求调整:
public class CosmosAuthInterceptor implements Interceptor { private final String masterKey; private final String resourceType; private final String resourceId; public CosmosAuthInterceptor(String masterKey, String resourceType, String resourceId) { this.masterKey = masterKey; this.resourceType = resourceType; this.resourceId = resourceId; } @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); String verb = original.method().toLowerCase(); String date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).format(new Date()); String contentType = original.header("Content-Type") != null ? original.header("Content-Type") : "application/json"; // 构造签名原始字符串,顺序不能乱 String stringToSign = String.format("%s\n%s\n%s\n%s\n%s\n", verb, contentType, date, original.url().encodedPath(), resourceType.toLowerCase()); // 生成签名 String signature = generateSignature(stringToSign, masterKey); String authHeader = String.format("type=master&ver=1.0&sig=%s", signature); // 构建新请求 Request modified = original.newBuilder() .header("x-ms-date", date) .header("x-ms-version", "2023-03-15") .header("Authorization", authHeader) .header("Content-Type", contentType) .build(); return chain.proceed(modified); } private String generateSignature(String stringToSign, String masterKey) { try { byte[] keyBytes = Base64.decode(masterKey, Base64.DEFAULT); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(keyBytes, "HmacSHA256")); byte[] hash = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(Base64.encodeToString(hash, Base64.NO_WRAP), StandardCharsets.UTF_8.name()); } catch (Exception e) { throw new RuntimeException("生成签名失败", e); } } }
使用的时候把拦截器加到OkHttpClient里,注意根据你的操作类型传正确的resourceType和resourceId。
你可以先按这些点排查,应该能找到问题所在!
内容的提问来源于stack exchange,提问作者Gregory Stein
相关产品推荐
相关产品推荐

