资讯动态

OkHttp3 POST请求400 Bad Request原因分析与排查清单

发布时间:2026/10/13 22:53:21 来源:尧图企业网站定制
做了十几年接口自动化说实话最怕的不是 500也不是超时而是那种“看起来一切正常却偏偏给你个 400”的请求。400 Bad Request 这个状态码在自动化测试里就是一颗定时炸弹——500 起码能证明请求打到了服务端是服务端逻辑崩了而 400 往往意味着你的请求从根上就没被人家接受而且问题可能小到一个空格、一个引号、一个看不见的编码字符。我在 Java 技术栈的项目里用 OkHttp3 做 HTTP 客户端说实话它已经算是我用过的所有类库中最省心的一个了API 设计清爽、依赖轻、性能不错还自带连接池和拦截器跟 TestNG、JUnit 这些测试框架整合起来特别顺手。也正因为大家默认它“好用”一旦遇到 POST 请求返回 400很多人反而会懵——明明代码看起来没问题为什么服务端就是不认这篇文章不打算跟你从头讲一遍 HTTP 协议而是从一个专门跑接口自动化、天天跟接口死磕的人的角度把我排查 OkHttp3 POST 请求一直 400 的完整思路、踩过的典型坑、还有一套能直接拿来用的排查清单原原本本分享出来。如果你刚接手接口自动化或者已经被 400 折磨到头疼这篇文章应该能帮你把排查路径缩短一大半。1. 先别急着改代码把 400 的真实含义搞清楚看到 400很多人的第一反应就是“是不是我代码写错了”然后立刻开始改代码。这个方向没错但顺序错了。我自己的习惯是先停一下想清楚 400 在 HTTP 语义里到底在说什么这样才能避免瞎试。1.1 400 到底在表达什么HTTP 400 Bad Request 的意思是服务端无法理解客户端发送的请求。注意“无法理解”这四个字它跟“没有权限”是两码事。401 是“你没带身份凭证”403 是“你带了凭证但没资格”而 400 是“你发过来的东西本身就是乱七八糟的”。在实际接口测试里400 通常有两个来源一是请求参数格式不对比如服务端要求 JSON body你偏偏发了表单格式或者 JSON 字段名大小写不对二是请求头不对比如缺少 Content-Type或者 Content-Type 与实际请求体不匹配服务端一解析就崩了。打个比方服务端是个严格的面试官它让你交一份 Word 格式的简历你递过去一个纯文本记事本写的文档。内容可能完全没错但格式不对面试官直接拒收根本不看内容。400 就是那个“拒收”的动作。1.2 为什么 OkHttp3 发的请求更容易触发 400不是 OkHttp3 本身有问题而是它构造请求的方式太灵活给了我们太多“犯错的空间”。比如同样是 POST用 FormBody 构造和用 RequestBody 写 JSON 字符串最终发出去的网络包差异很大。如果你用错了构造方式服务端那边看到的内容就跟你想象的不一样。再叠加一个因素很多后端框架比如 Spring 系默认启用了严苛的参数校验。字段缺失、格式不符、编码不对统统回你一个 400。也就是说后端代码在“拒收”这件事上非常勤快但不会告诉你具体是哪里不对。所以我的第一条经验是遇到 400不要急着改 Java 代码先做对照实验——用 Postman 或者 curl手动构造一个一模一样的请求发出去看服务端给什么反应。如果 Postman 能通那就是代码实现有差异如果 Postman 也不通说明我们对接口参数的理解有偏差这时候要去找接口文档或直接问后端同学。这个对照实验的做法在我这儿基本上能解决 60% 的 400 问题。我会把所有要调的接口在 Postman 里维护一份原始请求作为基准自动化用例跑 OkHttp3两边能随时对比。信息源统一了排查效率会高很多。2. 对照实验怎么做抓包应该抓什么有了“先对照、后改码”的思路接下来就是具体操作流程。我会按照“Postman 复现 - 抓包对比 - 逐项核对请求要素”这个顺序来。2.1 Postman 或 curl搭建复现基准假设接口是POST http://xxx.com/api/user/login请求体要求是 JSON{ username: tester, password: 123456 }在 Postman 里选中 POST填上 URL在 Body 标签页选 raw 并把格式选为 JSON粘贴这段 JSON直接点 Send。如果服务端正这请求应该返回正常结果。如果手头没有 Postman用 curl 也完全够用curl -X POST http://xxx.com/api/user/login \ -H Content-Type: application/json; charsetutf-8 \ -d {username:tester,password:123456}curl 最大的好处是“没有魔法”——你写什么它就发什么不会帮你自动加东西。这样它就可以作为请求的“基准线”。一个很重要的对比口径如果 Postman 或 curl 能通但 OkHttp3 不通问题大概率在代码这边如果 Postman 或 curl 也不通那就别折腾代码了先去确认接口文档、参数格式和必填字段。2.2 抓包工具亲眼看看 OkHttp3 实际发出的内容很多时候我们以为自己代码里写的是 A但网络包里实际发的是 B。为了搞清楚就得抓包。Windows 上我习惯用 FiddlermacOS 上用 Charles这两个工具都支持代理 HTTPS 流量能清楚看到每个请求的 Headers、Body、原始字节。抓包前要先让 OkHttp3 的流量经过抓包工具。OkHttp3 里设置代理非常方便OkHttpClient client new OkHttpClient.Builder() .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(127.0.0.1, 8888))) .build();Fiddler 和 Charles 默认监听的都是 8888 端口所以上面这段配置可以直接用。抓包之后重点对比三样东西URL 路径和查询参数是否与接口文档完全一致Content-Type 的值是否与请求体格式匹配Body 的原始内容和编码是否与代码里拼接的字符串一致我印象里最深的一次排查就是通过抓包发现代码里写的是application/json; charsetutf-8但服务端严格按照application/json做字符串匹配中间多出的那段; charsetutf-8直接把服务端搞懵了。这种“肉眼根本看不出来但一排就知道”的问题不抓包可能得熬夜排查。2.3 别忽略重定向和中间层还有一类 400 跟参数没关系是重定向引起的。OkHttp3 默认会跟随重定向如果你在自动化代码里关闭了重定向或者服务端做了一个 302 跳转POST 请求跟着跳转时可能被网关丢包最终返回给你一个 400。抓包时如果发现响应是 302就要考虑是不是需要手动处理跳转或者干脆直接调最终的 URL。3. OkHttp3 请求头与参数的四个高频坑如果抓包确认代码发出去的请求有问题那大概率就落在我下面要讲的这四个坑里。一个一个说把代码和现象都还原出来。3.1 Content-Type 与请求体格式不匹配最常见的坑。OkHttp3 构造 POST 请求有几种完全不同的写法用错了就会让服务端“看不懂”。写法一用 FormBody 发表单FormBody formBody new FormBody.Builder() .add(username, tester) .add(password, 123456) .build(); Request request new Request.Builder() .url(http://xxx.com/api/user/login) .post(formBody) .build();这段代码实际发送的请求头是Content-Type: application/x-www-form-urlencoded usernametesterpassword123456写法二用 RequestBody 发 JSONString json {\username\:\tester\,\password\:\123456\}; RequestBody requestBody RequestBody.create( MediaType.parse(application/json; charsetutf-8), json ); Request request new Request.Builder() .url(http://xxx.com/api/user/login) .post(requestBody) .build();这段代码实际发送的请求头是Content-Type: application/json; charsetutf-8 {username:tester,password:123456}如果你用写法一拼参数但接口要求的是 JSON服务端拿到表单数据后按 JSON 解析直接失败顺手给你一个 400。这是最典型的“格式不匹配”。3.2 字符串拼接 JSON 导致非法格式很多同学图省事喜欢用字符串拼接来构造 JSON bodyString json {\username\:\ username \};如果username里恰好包含引号、反斜杠、换行符整个 JSON 结构就崩了。比如username的值是ab拼接出来就是{username: ab}这显然不是合法 JSON服务端解析必然失败返回 400 一点不冤。更稳妥的做法是使用 Gson 或 Jackson 这类序列化库MapString, Object map new HashMap(); map.put(username, username); map.put(password, password); Gson gson new Gson(); String json gson.toJson(map);序列化库会自动处理转义生成的 JSON 一定是合法的。尤其当你做数据驱动、从 Excel 或配置文件里读取参数时更要走序列化这条路别自己拼字符串。3.3 编码问题中文和特殊字符乱掉接口如果涉及中文、emoji、特殊符号编码就极其关键。OkHttp3 里最稳妥的做法是在 MediaType 里显式带charsetutf-8并且读取响应时也用 UTF-8 解码。有一种容易踩的情况是服务端框架默认用 ISO-8859-1 解析请求体而你的请求头里没带 charset中文按照默认字符集解码后直接乱码。服务端如果做了严格校验就会把乱码请求当成非法请求返回 400。还有一种是 URL 里放查询参数值里带了空格、、、#这类特殊字符。自己拼 URL 时必须用URLEncoder.encode()编码否则服务端解析 query string 时会把参数截断。OkHttp3 的HttpUrl会帮你处理编码建议直接用HttpUrl url HttpUrl.parse(http://xxx.com/api/query) .newBuilder() .addQueryParameter(keyword, keyword) .build();这样写出来的 URL 一定是经过正确编码的。3.4 手动设置 Content-Length 引起冲突有些人会在 Header 里手动加上Content-Length试图“帮” OkHttp3 一把。但 OkHttp3 会根据 RequestBody 的实际字节数自动计算 Content-Length如果你手动设置的值和实际长度不一致服务端读 body 时就会错位最后的表现形式也是 400。除非你要实现特别底层的自定义协议否则这几个系统级 Header——Content-Length、Host、Transfer-Encoding——都别手动碰交给 OkHttp3 去处理就好。4. 从代码层面做一次完整修复实战理论说完了这一节我带你把一次真实的 400 排查走完。假设现在有个登录接口一直返回 400我们从零开始调。4.1 给 OkHttp3 挂上日志拦截器排查 400 的第一步是让 OkHttp3 把实际发出的请求原样打出来。官方生态里有现成的 logging-interceptor依赖如下dependency groupIdcom.squareup.okhttp3/groupId artifactIdlogging-interceptor/artifactId version4.12.0/version /dependency然后在代码里配置HttpLoggingInterceptor logging new HttpLoggingInterceptor(message - System.out.println(message)); logging.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client new OkHttpClient.Builder() .addInterceptor(logging) .build();这样跑一次请求控制台会打印出完整内容-- POST http://xxx.com/api/user/login Content-Type: application/x-www-form-urlencoded Content-Length: 37 usernametesterpassword123456 -- END POST看到这一眼很多问题就清楚了。4.2 对照日志与服务端要求假设接口文档约定的是 JSONPOST /api/user/login Content-Type: application/json Body: {username:tester,password:123456}但日志显示你的请求实际是Content-Type: application/x-www-form-urlencoded usernametesterpassword123456问题一眼就看穿用了 FormBody而服务端要 JSON。修复方式是改用字符串 JSON 加RequestBody.create()。为了避免以后再犯同样的错误我在实际代码里会把 JSON POST 封装成统一的工具方法public class HttpUtil { private static final MediaType JSON MediaType.parse(application/json; charsetutf-8); public static Response postJson(String url, String json) throws IOException { RequestBody body RequestBody.create(JSON, json); Request request new Request.Builder() .url(url) .post(body) .build(); return client.newCall(request).execute(); } }整个项目里所有 POST 请求都走同一个入口格式层面就不会再乱。4.3 响应日志同样不能漏400 只是状态码服务端往往会在响应体里给出更具体的错误信息比如{ code: 400, message: username is required }如果你在代码里直接写assert response.code() 200这样的断言那就永远看不到这些提示。排查阶段一定要把响应体打出来。这里有个细节值得注意response.body().string()只能调用一次调用之后 body 流就被关闭了。如果既想打印又要解析就先读成字符串再基于字符串解析String responseBodyStr response.body().string(); System.out.println(响应体: responseBodyStr); JSONObject jsonObject JSON.parseObject(responseBodyStr);别偷懒多次调用string()否则会报 IllegalStateException。5. 服务端校验与环境因素别把锅全甩给代码有时候代码这边已经检查到头了请求格式也无懈可击但 400 还是会出现。这时候要跳出客户端视角从服务端和链路角度找原因。5.1 网关和 WAF 的隐形拦截很多公司的网关或安全设备会对入站请求做二次校验。常见的有要求请求头必须带某个自定义令牌对 User-Agent 做白名单校验非浏览器 UA 直接拒绝对请求体大小设限制超大直接 400对异常 URL 编码做拦截。这类问题本地很难复现。我遇到过一次测试环境怎么发都通到了预发布环境就一直 400。去查网关日志才发现预发布环境的 WAF 规则拦截了带有某个敏感单词的参数。我们的参数名里刚好有个字段叫test_flag被当成测试标记给拦了。处理方式就是改字段名或者找平台方加白。遇到这种情况单看代码看不出任何问题最快的办法是拉着网络或运维同学一起看网关访问日志。5.2 Token 过期或认证信息缺失有类接口在未传 Token 时并不会返回 401而是统一用 400 表示“无法识别请求”。如果自动化脚本之前一直正常某天突然大量出现 400优先检查两件事Token 是否过期Token 的请求头字段名是否被服务端改动。在 OkHttp3 里推荐用拦截器统一给所有请求加认证头client new OkHttpClient.Builder() .addInterceptor(chain - { Request original chain.request(); Request request original.newBuilder() .header(Authorization, Bearer tokenProvider.getToken()) .build(); return chain.proceed(request); }) .build();这种方式的好处是即使个别用例没显式设置认证头它也会自动补全避免因为“漏写头”而触发 400。5.3 环境差异导致参数要求不一致测试环境和联调环境接口版本不一致也是 400 的常见来源。旧环境里某个字段是非必填到了新环境它变成必填了而你还在用旧参数集去请求服务端一校验就 400。处理这种问题的思路是把环境相关配置baseUrl、账号、参数外置到配置文件或环境变量里不要硬编码在用例代码中。这样切换环境时只需要改配置排查问题也更好定位。6. 排查速查表遇到 400 直接按顺序走下面这部分是我每次遇到 OkHttp3 POST 返回 400 时都会过一遍的清单内容不多但覆盖了绝大多数场景。6.1 从现象到原因的快速对照现象优先排查方向常见解法Postman 通、OkHttp3 不通代码实现差异用日志拦截器对比实际请求Content-Type 是表单但接口要 JSON请求体构造方式改用 RequestBody JSON响应体提示缺少必填字段参数不完整对照接口文档补齐必填项中文乱码或特殊字符报 400编码问题设置 charsetutf-8用 URLEncoder 编码所有请求都 400且响应体为空网关或 WAF 拦截查网关日志检查 UA、Token之前正常某天突然 400Token 过期或服务端改动检查认证信息核对接口版本6.2 十分钟排查顺序在 Postman 或 curl 里手动构造同一请求确认接口本身可用。打开 OkHttp3 日志拦截器打印完整请求头、请求体、响应体。对比请求的 URL、Headers、Body 三要素是否与服务端要求一致。检查 Content-Type 与请求体格式是否匹配。检查参数序列化方式确认不是字符串拼接出的非法 JSON。检查编码设置charsetutf-8有没有漏掉。检查 Token、Cookie、UA 等隐性 Header 是否齐全。若本地全部正常直接找服务端或网络团队拉日志。每次都是这么一套流程走下来百分之八九十的 400 都能在第四步和第七步之间解决。6.3 日常写代码的三个好习惯最后再说三个我在实际项目里养成的好习惯它们能从根源上减少 400 的出现概率。第一全局只用一个 OkHttpClient 实例。OkHttp3 自己管理连接池和线程池频繁new OkHttpClient()不仅浪费资源出问题时日志还难追溯。第二把 HTTP 调用统一封装到底层工具类不在每个用例里裸写 Request。这样需要加公共 Header、公共日志、统一拦截器时改一处即可全局生效。第三参数尽量数据驱动从外部配置读取。别把某个环境的账号、密码、参数硬编码在代码里否则环境一换400 就会换个姿势找上你。写在最后400 这个状态码表面上看着简单背后藏的问题却五花八门。我在自动化测试这条路上接过的 400有因为 Content-Type 写错的有因为 JSON 转义失败的有因为编码不对的甚至有一次是服务端自己的校验逻辑写了个 bug参数完全正确也照样给你 400。所以遇到 400 心态一定要稳。先做对照实验再抓包看真实请求然后回代码找差异最后再考虑服务端的原因。别一上来就闷头改代码那样大概率是白费功夫。把上面这套方法背下来实际用几次之后你会慢慢发现——400 不是玄学。它就像一道题干明确的数学题只要每一步都验证到位答案总是会浮出水面的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑