ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android POST 405错误排查全攻略:方法、原理与案例分析

Android POST 405错误排查全攻略:方法、原理与案例分析 1. 405不是网络不通是服务器在说“方法不行”做Android开发的人十有八九都被405这个状态码折磨过。刚入行那会儿我遇到POST请求返回405第一反应是检查网络权限、检查URL对不对、检查是不是没加联网权限结果折腾半天发现跟这些都无关。405的全称是405 Method Not Allowed翻译成人话就是服务器收到了你的请求也识别出了你要访问的资源但你这个HTTP方法GET、POST、PUT、DELETE这些不在允许名单里。打个比方你走到一家餐馆店是开着的服务器在线你也走到了正确地址URL路径没错但服务员告诉你“我们这儿只做外卖不接待堂食。”你非要坐下来点菜对方就给你一个405。这种拒绝是非常明确的跟404那种“资源不存在”完全两码事。404是“你找的地方不对”405是“地方对了但你用错了方式”。很多Android开发者在排查这个问题时习惯性地往网络层、权限层去深挖方向一开始就偏了。我见过不少项目里前端Android用HttpURLConnection或者OkHttp发POST日志里明明打印了请求已发出却收到405于是一遍遍检查URL是不是写错了甚至怀疑是机房网络策略拦截。实际上绝大部分405都出在“客户端发出的请求方式和服务端路由定义的方式不一致”这个点上。要理解这一点你得先弄清HTTP协议里状态码的设计逻辑以及服务端框架处理请求时到底比对的是什么。这篇文章会把我在实际开发中踩过的405相关的坑、排查思路和最终解决方案完整梳理一遍适合正在被405折磨的Android开发也适合后端同学了解客户端的排查视角。内容不会只停留在“哦你改成POST就好了”而是把触发405的各种隐蔽场景都挖出来一步步告诉你为什么会出现以及怎么定位。2. Android里发POST的几套常用姿势先别急着说“我发的就是POST”很多人排查405的第一步就卡住了代码里明明写了request.method POST为什么服务端还说方法不允许这里有个很容易被忽略的事实你脑子里以为的POST和你实际发出去的请求可能根本不是同一个东西。2.1 HttpURLConnection的坑setRequestMethod要放在getOutputStream之前如果你还在用原生的HttpURLConnection最常见的问题是代码顺序写错。刚入行的同事经常写出这种代码URL url new URL(https://api.example.com/login); HttpURLConnection connection (HttpURLConnection) url.openConnection(); connection.setRequestMethod(POST); connection.setDoOutput(true); connection.setRequestProperty(Content-Type, application/json); // 这里开始写body try (OutputStream os connection.getOutputStream()) { byte[] input jsonBody.getBytes(StandardCharsets.UTF_8); os.write(input, 0, input.length); }这段写法本身没问题setRequestMethod(POST)确实在发送请求之前就设置了。但有个隐蔽的细节如果你在调用getOutputStream()之后再去设置请求方法或者请求头很多Android版本的实现会直接忽略甚至抛异常。还有一个特别容易被忽略的点——setRequestMethod只能调用一次而且必须在connect()之前。有些同事把setRequestMethod写在一个公共的网络工具类里实际调用却在连接建立之后那这个设置就完全不生效请求会以默认的GET方式发出去服务器自然给你一个405。2.2 OkHttp的Builder模式方法名写错是最低级的坑现在大部分项目都用OkHttp写起来比HttpURLConnection舒服不少val client OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() val request Request.Builder() .url(https://api.example.com/login) .post(body) // 这里指定POST .header(Content-Type, application/json) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 网络层失败会走到这里 } override fun onResponse(call: Call, response: Response) { if (response.code 405) { // 业务层失败会走到这里 } } })OkHttp的API设计得比较直观.post(body)一写请求行里的方法就是POST。但问题往往出在“你以为调用了其实没调用”。比如某些二次封装的网络层内部用了Request.Builder().method(GET, null)作为兜底外部传入的POST逻辑被某段异常流程吃掉请求最终以GET发出这个在日志里很难看出来因为打印日志的位置可能只打了URL没打请求方法。2.3 Retrofit注解没写对接口方法默认就是GET再往上封装一层很多人用Retrofit。Retrofit的注解规则非常严格接口方法上没写POST默认就是GETinterface ApiService { // 错误示范忘了写POSTRetrofit默认按GET处理 POST(user/login) suspend fun login(Body request: LoginRequest): BaseResponseLoginResult }上面这个例子是写对了的但如果你把POST写成了GET或者干脆漏掉注解请求发出去就是GET。服务端路由只注册了POST入口GET进来就直接405。这种错误在代码Review时特别难发现IDEA也不会给你任何编译期提示只有联调时被打个405回来才暴露。2.4 用curl或者Postman先独立验证判断是客户端还是服务端的问题排查405的第一步永远不是翻客户端代码而是先用一个与客户端完全无关的工具单独发一次同样的请求。我自己的习惯是curl -X POST https://api.example.com/user/login \ -H Content-Type: application/json \ -d {username:test,password:123456}如果curl能正常返回说明服务端没有问题问题一定出在客户端。如果curl同样返回405那就去服务端日志里看路由配置。这个动作能把排查范围缩小一半以上可惜很多人第一步就跳过了直接在自己代码里翻来翻去。3. 真正让服务器返回405的七类穷因与对应解法curl验证完之后问题基本锁定在客户端请求和服务端路由之间的“契约不一致”上。但这层不一致具体是怎么产生的我把这几年碰到过的405场景归了类每一类都有对应的解法。3.1 服务端路由只注册了POST但客户端实际发出的不是POST先回到基础HTTP请求的请求行长这样POST /user/login HTTP/1.1 Host: api.example.com Content-Type: application/json Content-Length: 46如果这里的POST变成了GET或者PUT、PATCH而服务端这段路由只写了PostMapping(/user/login)Spring Boot或者router.post(/user/login, handler)Express/Ktor那必然得到405。这种情况最常见的发生点用了HTTP代理或者拦截器不小心把请求方法改写掉了。比如某些统一封装的网络层为了做缓存或者日志上报会在拦截器里把请求复制一份用request.newBuilder().method(GET, null).build()来生成一个“用于打印日志的副本”结果不小心把改写后的请求发出去了。这种错误非常隐蔽日志打出来的还是POST但线上的请求就是GET。排查这类问题唯一可靠的手段是抓包看请求行。你可以用Charles、Fiddler这类工具或者直接在OkHttp的拦截器里把request.method()和request.url()一起打到日志里class LoggingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() Log.d(Network, method${request.method} url${request.url}) return chain.proceed(request) } }3.2 路径写错了请求落到了另一个Controller上有的服务端框架特别是Spring MVC路由匹配是按精确路径来的/user/login和/user/login/都不是同一个路径更别说/User/Login这种大小写不一致的写法。如果你的URL拼写有误差请求可能落到了一个只接受GET的接口上服务端返回405而不是404——因为路径前缀匹配到了某个Controller只是方法不匹配。真实案例某个项目后端同时提供了GET /api/v1/user/info和POST /api/v1/user/updateAndroid端一个同事做接口联调时把update接口的路径写成了GET /api/v1/user/update服务端那套基于路径前缀的鉴权过滤器先过滤了一遍然后路由分发时发现这个路径只注册了POST入口于是返回405。解法很简单把URL和服务端接口文档逐字符比对一次特别是末尾有没有斜杠、大小写是否一致、路径参数位置对不对。注意URL里拼了中文或者空格但没有做URL编码也可能导致路径匹配错乱这类情况在日志里看URL还挺正常实际服务端解析时已经变了。3.3 服务端允许POST但Content-Type被网关或框架拦了这个场景非常反直觉我当年定位了很久。某个接口在Postman里用application/json发POST一切正常但Android端用OkHttp发POST就405。两边URL一模一样请求方法一模一样唯一的区别是Content-Type。后来查了一圈才发现服务端用的是Spring Security CSRF防护或者某些云WAF策略对POST请求要求必须带特定的Content-Type头比如application/x-www-form-urlencoded、multipart/form-data。Android端发的是application/json; charsetutf-8网关层一看Content-Type不在白名单里直接拦截返回405。这种问题的关键特征是用浏览器访问或Postman能通换到Android就405。因为Postman往往会在界面上帮你把Header按你选的Body类型自动填好而Android代码里如果漏了header(Content-Type, application/json)OkHttp默认不放这个头某些服务端框架就当作“非法请求”处理。处理方案确认服务端到底要求什么Content-Type然后代码里明确设置val request Request.Builder() .url(url) .post(body) .header(Content-Type, application/json) .build()3.4 重定向之后的POST变成了GET这个坑在登录场景里特别常见。服务端某些接口做了HTTP到HTTPS的301重定向或者从旧域名跳到新域名。按照HTTP规范301/302重定向时如果原始请求是POST浏览器和多数HTTP客户端会自动将重定向后的请求改成GET而且会丢掉请求body。OkHttp默认是跟随重定向的followRedirects默认是true。这意味着你的代码发的是POST服务端返回301OkHttp自动跟随第二次请求变成了GET落到重定向后的URL上。如果那个URL只接受POST就会返回405。这个问题排查起来更隐蔽因为你在OkHttp的日志里看到的第一条请求确实是POST但看不到后续跟随重定向时自动发出的GET请求。我当时是抓包才发现异常。解法有两个方向在构建OkHttpClient时关闭自动重定向自己处理val client OkHttpClient.Builder() .followRedirects(false) .followSslRedirects(false) .build()如果业务必须跟随重定向那就确保重定向的目标URL支持同样的请求方法或者让后端直接改掉重定向配置。3.5 WebView里的表单POST少了Method声明有些项目会在WebView里内嵌H5页面H5页面需要通过表单方式POST数据到服务端。这时候要注意H5的form标签如果不写methodpost默认就是GET。这不是Android代码的问题但Android开发者排查时经常会忽视WebView相关页面的影响。还有一种是WebView加载本地HTMLHTML里的JS用XMLHttpRequest发POST但设置了跨域请求服务端没配CORS请求在预检OPTIONS阶段就被拒了。有的服务端框架对OPTIONS请求会返回405Android端看到的最终错误也是405但根因是跨域配置缺失。遇到这种情况不能只盯着Android原生代码要把逻辑链路扩到WebView里运行的H5代码确认form表单和ajax请求的method究竟写的是什么。3.6 服务端API设计本身有问题把POST接口限定成了其他方法还有一种情况服务端框架的用法本身有问题。比如Spring Boot的RequestMapping不指定method时默认匹配所有方法但如果你写了RequestMapping(value /user/login, method RequestMethod.GET)那这个接口就只能GET不写POST。Android端不管怎么努力发POST都是405。这类问题需要后端配合修改路由定义但在等待后端修复时客户端可以临时用一个策略绕过去换一个同功能但方法匹配的接口或者和服务端确认是否有兼容的GET接口。遇到这种情况要留好截图和请求日志方便沟通时作为证据。3.7 Android 9之后默认禁止明文HTTP引发的间接405Android 9API 28开始默认禁止明文HTTP流量。如果你的接口是http://开头OkHttp会直接抛异常或者在某些配置下被系统网络栈拦截返回的错误表现五花八门其中一个常见表现就是非标准的405。严格来说这不算真正的服务端405但确实会出现在“为什么我的POST返回405”的搜索里。排查方法看一眼请求URL是不是http开头是的话要么让后端升级HTTPS要么在AndroidManifest里配置android:usesCleartextTraffictrue或者配置网络安全策略application android:usesCleartextTraffictrue ... 不过这个解法有安全风险生产环境不建议全局放开。4. 从报错到定位405问题的完整排查链路很多开发者遇到405就慌了这里给一套我压箱底的排查顺序按这个顺序走基本15分钟内能定位80%的405问题。4.1 第一步确认异常的确切类型和堆栈看到405先别急先分清楚是网络层抛出来的异常还是服务器返回的HTTP响应。这俩定位方向完全不同如果是IOException: unexpected end of stream或者ConnectException那跟405八竿子打不着。如果是Response.code()等于405也就是服务端正常返回了那已经是业务层的问题了。如果用的是OkHttp RetrofitRetrofit的HttpException里就有code()方法拿到405之后把response.errorBody()?.string()打印出来服务端通常会在body里写一段错误描述比如{message:Method Not Allowed}或者更具体的POST not supported for /user/login。这段body信息量极大很多人排查405时只看状态码不看body这是巨大的浪费。代码里可以这样加日志if (response.code() 405) { val errorBody response.errorBody()?.string() Log.e(Network, http code405, body$errorBody) }4.2 第二步抓包看请求行和响应头如果body信息不够就要抓包看原始请求。Android抓包相比PC端有一点麻烦Android 7.0API 24之后App默认不信任用户安装的CA证书所以用Charles抓HTTPS包的经典方案会失效。这个坑很多人踩过有几种应对办法在App的network_security_config.xml里设置信任用户证书仅限Debug包。用root设备把证书装到系统证书目录。用Frida或objection做SSL Pinning绕过。最简单的方式在OkHttp拦截器里打印完整的请求和响应信息。拦截器输出这一步其实比Charles更直接因为它能看到App进程里最真实的请求内容不会受代理抓包时的影响class HttpLoggingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() val requestBody request.body println(REQUEST: ${request.method} ${request.url}) println(REQUEST HEADERS: ${request.headers}) if (requestBody ! null) { val buffer Buffer() requestBody.writeTo(buffer) println(REQUEST BODY: ${buffer.readUtf8()}) } val response chain.proceed(request) println(RESPONSE: ${response.code} ${response.message}) return response } }看到请求行之后重点确认三件事方法是不是POST、路径是不是完全匹配接口文档、Content-Type是不是服务端要求的。4.3 第三步用curl复现隔离客户端变量抓包拿到请求信息后把相同内容复制到curl里在PC上独立跑一次curl --location --request POST https://api.example.com/user/login \ --header Content-Type: application/json \ --data-raw {username:test,password:123456}如果curl正常说明服务端没问题差异在客户端可能是代理、拦截器、证书等。如果curl同样405说明服务端本身对该路径的POST就是不支持的别再折腾Android代码了直接找后端对路由。4.4 第四步检查服务端日志、确认路由表和服务端同学沟通时不要只说“Android发POST返回405”要把完整信息带上请求URL和请求方法抓包截图Content-Type完整的响应body大致时间点方便后端拉日志后端在Spring Boot里可以通过RequestMappingHandlerMapping查看所有已注册的路由和方法。如果是Ktor或者Node.js Express可以直接看路由定义代码。服务端日志里如果有No mapping for POST /user/loginSpring常见的警告日志那说明这个路径压根没注册POST方法。4.5 第五步二分定位法快速缩小责任方如果项目链路长——Android端 → 网关 → 微服务 → 数据库——405可能出现在任何一层网关策略上。我常用一个二分法从客户端直接请求网关入口确认网关是否放行再让后端同事绕过网关直连服务确认服务本身是否正常。哪一环开始返回405问题就定位在哪一环。这个过程中要特别留意网关层比如Nginx、Kong、APISIX的location配置有些网关默认只允许GET和HEAD会直接对POST返回405。5. 405和404/403/500的分界线别再张冠李戴排查过程中我发现很多人对HTTP状态码的语义边界模糊导致排查方向从一开始就错了。这里把几个容易混淆的状态码放一起对比一下。状态码含义排查方向400 Bad Request请求格式错误服务端压根看不懂你的body检查参数格式、JSON语法、编码401 Unauthorized未认证或登录失效检查token、cookie、签名403 Forbidden已认证但没权限检查角色权限、IP白名单404 Not Found路径或资源不存在检查URL路径拼写405 Method Not Allowed路径存在但HTTP方法不允许检查请求方法、路由方法注册、Content-Type500 Internal Server Error服务端代码执行出错服务端查日志、看堆栈有一个很典型的场景URL路径写错了比如少了一个api结果请求落到了一个不存在的地方有些网关会返回404但有些网关配置了通配符路径会把请求转发到一个兜底Controller上这个Controller只接受GET于是返回405。这就是为什么有时候405和404之间的界线那么模糊。遇到这种情况先把URL路径和服务端路由表逐项比对路径对了再去纠结方法的事。6. 预防405的工程化手段联调前就把它摁死在摇篮里排查是痛苦的但405这类问题完全可以靠工程手段提前发现。分享几个我在团队里推行的做法。6.1 网络层统一封装强制打印请求方法和URL把OkHttp的拦截器做成强制性的所有请求都必须经过统一的错误处理拦截器。这个拦截器除了打印日志还可以在收到405时直接把服务端返回的错误体抛出来让调用方能第一时间看到class ErrorMappingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val response chain.proceed(chain.request()) if (response.code 405) { val body response.peekBody(1024).string() throw HttpException(HTTP 405: ${chain.request().method} ${chain.request().url} body$body) } return response } }这样做的价值在于不让405错误静默地返回给上层业务而是以一种“一眼能看到完整上下文”的异常形式抛出开发者在联调阶段就能快速定位。6.2 接口文档里强制标注Method和Content-Type很多405问题是前后端对接口契约理解不一致导致的。团队里可以约定接口文档里每个接口必须标注Method、Path、Content-Type三个字段缺一不可。Android端生成网络层代码时直接从文档自动生成Retrofit接口定义减少手写注解的出错概率。如果能用OpenAPISwagger规范管理接口前端可以直接从文档自动生成客户端代码比如用OpenAPI Generator生成Retrofit2代码POST、GET这些注解全部由工具生成人肉写错的可能性就基本掐死了。6.3 集成测试阶段加入“方法校验”用例在CI/CD流程里加一个简单的接口契约测试每次后端发版时自动遍历所有路由发起一次GET和一次POST请求校验是否返回预期状态码。如果某个接口只允许POST那GET进来就必须返回405如果不返回405反而说明路由配置有兼容性问题。这类用例写起来不复杂但能把接口契约变更第一时间暴露出来。6.4 客户端要有“全局405兜底提示”不要裸奔生产环境如果还出现405大概率是后端发生了不兼容的发布。客户端最好有全局的HTTP错误码兜底处理遇到405时不要展示一个空白的失败而是给用户一个明确的提示“服务暂不可用请稍后重试”并同步上报到监控平台。这个不需要复杂逻辑在网络层拦截器里统一处理即可。写在最后的几个实在建议做了这么多年Android开发跟405纠缠的时间不算短最后说点实实在在的体会。第一405大多数时候不是客户端代码问题而是前后端契约没对齐。遇到405先别急着改代码先看请求行、看服务端路由表把责任方搞清楚再动手。我这里说的“责任方”不是让你甩锅而是让排查行动有的放矢。第二抓包能力是Android开发的必修课。OkHttp拦截器打日志虽然方便但经过拦截器处理的请求已经是“封装后”的不是网络上真实传输的报文。真要定位问题Charles、Wireshark这些工具该会还得会尤其要搞定Android 7.0之后的证书信任问题否则一到HTTPS的坑就抓瞎。第三沟通时把信息给足。跟后端对405问题只说“我这边405了”基本等于没沟通。把请求方法、URL、Header、Body、响应信息全部贴出来后端一眼就能看出问题在哪一行。很多时候我帮同事排查发现其实是后端自己路由写错了但客户端这边连个完整的请求日志都没打出来绕了一大圈。第四所有网络请求场景都要考虑重定向。服务器返回302、301不一定是死路但重定向之后如果你的POST被变成了GET那405就是大概率事件。OkHttp默认跟随重定向这个行为在生产环境要想清楚该不该关。第五如果你用的是Retrofit OkHttp这套组合405的排查优先级应该是先查接口注解 → 再查路径拼写 → 再查Content-Type → 再查重定向 → 再查网关。按这个顺序走绝大多数问题都能在半小时内定位。如果这些方向都查不出问题才需要怀疑是不是OkHttp版本本身有bug——这种情况极少但也不是零。希望这篇东西能帮到还在被Android POST 405折磨的人。排查网络问题的关键从来不是背答案而是建立一套可靠的定位路径顺着线索一步步逼近真相。405虽然烦人但搞清楚之后你会发现它反而是最好处理的一种HTTP错误。
返回列表