一次简单的 Http 请求异常处理 (请求的 url 太长, Nginx 直接返回 400, 导致请求服务异常)

1 结论

按照惯例直接说结论。

后台服务 A 有一个 Http 接口, 代码如下:

@RequestMapping(value = "/user", method = RequestMethod.GET)
public List<UserInfoVo> getUserInfoByUserIds(@RequestParam(value = "userIds") List<String> userIds) {// ...
}

没错, 一个 Get 请求, 入参是一个 List

同时有另一个后台服务 B, 里面有段逻辑会通过 RestTemplate 调用服务 A 的这个接口, 代码如下:

public List<UserInfoVo> batchGetUserInfo(Collection<String> userIds) {String url = String.format("%s/user?userIds=%s", remoteHost, StringUtils.join(userIds, ","));String returnResult = restTemplate.getForObject(url, String.class);// ...
}

在服务 B 中, 通过 batchGetUserInfo 方法请求服务 A 时, 传入了一个长度为 122 的 List, List 中每一项是一个 32 位的 UUID。
结果导致调用服务 A 的 url 长度太长, Nginx 认为这时一个异常的请求格式, 直接返回状态码 400, 结构导致服务 B 逻辑异常

2 过程

2.1 反馈

下午, 突然收到用户反馈: 进入某个页面后, 直接白屏。

2.2 定位到直接原因

直接通过 Nginx 请求日志, 发现用户反馈的操作时间段内, 有一个接口一直返回 400 的错误。
根据客户端反馈这个错误的确会导致页面白屏。

知道了直接原因了, 但是没有解决, 还是需要定位到根本原因。

2.3 Arthas 排查

通过错误的 url, 定位到对应的代码, 然后通过 Kibana 查看日志, 发现只有一个简单的异常提示, 没输出任何堆栈信息。

因为是一个查询接口, 所以本地通过拼接参数, 尝试请求这个接口, 发现是逻辑正常的, 应该是数据问题, 这就尴尬了。

通过分析代码逻辑, 看不出什么异常的。
在没有日志, 复现不出反馈情况, 代码逻辑分析不出异常时, 决定通过 Arthas 协助排查了。

在生产环境中, 启动了一个预发版本, 通过 url 模拟用户请求。

同时启动 Arthas, watch 对应的接口

watch com.aaa.bbb.TestController testMethod "{params,returnObj,throwExp}" -x 4

定位到以下异常:

org.springframework.web.client.HttpClientErrorException: 400 at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:287) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]at com.aaa.bbb..service.impl.RemoteServiceImpl.batchGetUserInfo(RemoteServiceImpl.java:206) ~[classes/:?]......

根据堆栈信息定位到代码

public List<StudentVo> batchGetUserInfo(Collection<String> userIds) {String url = String.format("%s/user?userIds=%s", remoteHost, StringUtils.join(userIds, ","));String returnResult = restTemplate.getForObject(url, String.class);// ...
}

看逻辑没多大的异常, 唯一比较惊讶的就是 Get 请求, 入参确实是一个 List, 不过 Http 本身就支持这样操作。

继续通过 Arthas, watch 对应的接口, 不过这次 watch 上面的代码:

watch com.aaa.bbb..service.impl.RemoteServiceImpl batchGetUserInfo "{params,returnObj,throwExp}" -x 4

继续通过 url 模拟用户请求。

发现堆栈信息同样的 HttpClientErrorException: 400 异常, 但是通过打印的参数列表发现, 入参竟然是 123 个的 String。

第一时间感觉到: 参数太多, 拼接的 url 太长, 导致请求失败。
但是转念一下, Get 请求 url 的长度限制是浏览器的行为, Http 协议没有对传输的数据大小进行限制
现在是 2 个后台服务的 Http 请求, 没有经过任什么浏览器, 理论上是这个长度无限制的。

2.4 Nginx 直接返回 400 错误码

请求 url 感觉没什么问题?
既然这样, 会不会是结果响应方处理有什么异常吗?

同样通过 Kibana 查看日志, 发现对应的接口, 没有当前用户的请求日志。也就是说, 对应的请求没有到达服务 A。

不是被调用方的问题, 那么会不会是 RestTemplate 这个框架内部做了限制呢?

restTemplate.getForObject 出发, 进入到源码, 发现内部也是没有对 url 长度做限制的, 同时定位到抛出异常的位置如下

public class DefaultResponseErrorHandler implements ResponseErrorHandler {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {// 从响应里面获取到状态码HttpStatus statusCode = getHttpStatusCode(response);switch (statusCode.series()) {// 状态码 4xxcase CLIENT_ERROR:throw new HttpClientErrorException(statusCode, response.getStatusText(),response.getHeaders(), getResponseBody(response), getCharset(response));// 状态码 5xx						case SERVER_ERROR:throw new HttpServerErrorException(statusCode, response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));default:throw new RestClientException("Unknown status code [" + statusCode + "]");}}
}

抛出异常的结果是根据请求返回的状态码来决定的。 也就是服务 B 有发起请求, 同时收到了一个 400 的错误码, restTemplate 将其封装为一个 HttpClientErrorException。

调用方有发起请求, 被调用方没有请求日志, 2 者之间通过通过 Http 请求, 那么有问题的的地方应该就是 2 者中间的 Nginx 了。

2.5 验证

public List<StudentVo> batchGetUserInfo(Collection<String> userIds) {String url = String.format("%s/user?userIds=%s", remoteHost, StringUtils.join(userIds, ","));String returnResult = restTemplate.getForObject(url, String.class);// ...
}

将上面的 remoteHost 替换为一个具体的 ip 地址, 直接请求对应的容器, 绕过 Nginx。
重新部署, 通过 url 模拟用户请求, 正常响应。

3 总结

服务 A 的请求先经过 Nginx, 再由 Nginx 转发到 B。
而异常的用户的请求到了 Nginx, Nginx 直接返回了 400, 从而导致用户请求异常。

通过查询资料, Nginx 报 400 的场景如下

  1. request_uri 过长超过 nginx 配置大小
  2. cookie 或者 header 过大超过 nginx 配置大小
  3. 空 HOST 头
  4. content_length 和 body 长度不一致

我遇到的情况就是第一种。
Nginx 处理时认为客户端请求格式错误, 于是直接返回 400, 不会向 upstream server (也就是下游服务) 转发请求, 因而 upstream server 对这些错误请求其实完全是无感知的。

至此结束。

碎碎念:
其实对 Http 响应码有一点了解, 结合上面获取到的请求参数太多和堆栈的信息的 400, 基本可以推导出问题了, 不用像我一样, 一步步猜测验证。
而本身通过这次, 对 Http 的响应码和 Nginx 也算是多了一点了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/172717.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Linux]进程等待

文章目录 3.进程等待3.1什么是进程等待3.2为什么要进程等待3.3如何进行进程等待?1.wait2.waitpid2.1函数的讲解2.2status的理解2.3代码理解 3.4学后而思1.直接用全局变量获取子进程退出码可以吗?如下2.进程具有独立性 退出码是子进程的数据 父进程是如何拿到退出码的3.对内存…

云匣子 FastJson反序列化RCE漏洞复现

0x01 产品简介 云匣子是租户连接云资源的安全管理工具&#xff0c;帮助云租户更加安全、精细的管理云上的虚拟机、数据库等资源。 云安宝结合多年的运维和安全实践&#xff0c;将云上的运维和安全有机结合&#xff0c;实现对运维过程的事前规划、事中控制和 事后审计。在此之上…

Handler系列-prepareMainLooper在哪里调用的

ActivityThread的main方法里调用prepareMainLooper public final class ActivityThread {public static void main(String[] args) {Looper.prepareMainLooper(); //创建sMainLooperLooper.loop();} } prepareMainLooper创建了sMainLooper public final class Looper {priva…

Redis常见面试问题

1、Redis分布式锁是怎么实现的 Redis实现分布式锁的7种方案-CSDN博客 2、Redis分布式锁会有什么问题 Redis实现分布式锁的7种方案-CSDN博客 3、Redis有哪些操作时间复杂度不是O(1)&#xff1f; Redis有一些操作的时间复杂度不是O(1)&#xff0c;例如&#xff0c;删除List、…

nodejs微信小程序+python+PHP-婚纱摄影预约系统的设计与实现-安卓-计算机毕业设计

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

Linuxfork,写时拷贝

1.prinf隐藏的缓冲区 1.思考:为什么会有缓冲区的存在? 2.演示及思考? 1).演示缓存区没有存在感 那为什么我们感觉不到缓冲区的存在呢?我们要打印东西直接就打印了呢? 我们用代码演示一下: 比如打开一个main.c,输入内容如下: #include <stdio.h> int main() { …

LLaMA 2:开源的预训练和微调语言模型推理引擎 | 开源日报 No.86

facebookresearch/llama Stars: 36.0k License: NOASSERTION LLaMA 2 是一个开源项目&#xff0c;用于加载 LLaMA 模型并进行推理。 该项目的主要功能是提供预训练和微调后的 LLaMA 语言模型的权重和起始代码。这些模型参数范围从 7B 到 70B 不等。 以下是该项目的关键特性…

kubernetes架构及核心组件简单介绍

目录 整体架构控制面kube-apiserver访问控制通知 kube-scheduler概述默认调度策略 kube-controller-manageretcd架构Raft协议日志复制 数据面kubeletkube-proxy 整体架构 集群架构图 控制面 控制面是kubernetes的核心组件&#xff0c;负责管理和控制集群的整体行为&#xf…

Python文件路径常用操作

1 文件路径 在进行数据处理时&#xff0c;经常要用代码去读文件里的数据&#xff0c;那么首先就得知道这个文件的文件路径。文件路径简单地说就是文件的存放位置。文件路径分为两块&#xff1a;文件夹路径和文件名&#xff0c;文件名又分为文件基本名和扩展名。 举例说明&…

解析MySQL Binlog:从零开始的入门指南【binlog入门指南】

&#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 解析MySQL Binlog&#xff1a;从零开始的入门指南 前言第一&#xff1a;MySQL中的Binlog详解1. 什么是Binlog&#xff1f;2. Binlog的作用a. 数据恢复&#xff1a;b. 主从复制…

jdk17安装全方位手把手安装教程 / 已有jdk8了,安装JDK17后如何配置环境变量 / 多个不同版本的JDK,如何配置环境变量?

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信您对博主首页也很感兴趣o (ˉ▽ˉ&#xff1b;) 学生邮箱白嫖/免费安装JetBrains全家桶(IDEA/pycharm等) —— 保姆级教程 目录 1、下载jdk17 2、安装jdk17 3、配置环境变量 -> 电脑无其他jdk 4、…

【Git】修改提交信息(单次、批量)

文章目录 修改最近一次 commit 的提交信息修改某次 commit 的提交信息方法总结 修改最近一次 commit 的提交信息 git commit --amend -m "new message"修改某次 commit 的提交信息 git log --oneline 运行结果如下2f80f1b commit 4 9ee990a 第三次提交 40f2f03 comm…

Echarts title标题配置项的使用 更改颜色 副标题

title配置项主要是对图表的标题进行配置 title配置项所有属性文档 title&#xff1a; { ...... }设置标题 副标题 text: 简单创建柱形图,//图表标题 subtext: 副标题,如果想对副标题设置 超链接 边框 颜色 宽度…等 比如&#xff1a;设置超链接 sublink:‘…’, 设置标题位置…

【Unity入门】LayerMask小结

LayerMask常用的几种方法 LayerMask.GetMask 根据 层名称 获取其层遮罩值 遮罩值 2^层索引 假设 UserLayerA 和 UserLayerB 是第十层和第十一层。 这两个层会具有 User Layer 值 10 和 11。若要获取其层遮罩值&#xff0c; 可以将其名称传入 GetMask。参数可以是 其名称的列…

10_7iic整体框架流程

在内核中 这边把iic整个流程分成了 4层 iic_dtiver at24_iic_eeprom 也就是我们的自己的驱动 i2c-core.c 核心层 i2c/busses/i2c-s3c2410.c 控制器层 平台总线驱动层,或者也是图中的设备树 硬件描述 我们假设 板子上有三个iic控制器 0 1 2 这里在控制器0 上挂载了gt24c02的eep…

STK Components 二次开发-创建地面站

1.地面站只需要知道地面站的经纬高。 // Define the location of the facility using cartographic coordinates.var location new Cartographic(Trig.DegreesToRadians(-75.596766667), Trig.DegreesToRadians(40.0388333333), 0.0); 2.创建地面站 创建方式和卫星一样生成对…

Centos 7 离线安装(tar) NodeJS 16 和 Vue

目录 一、下载Nodejs二、安装Nodejs2.1、创建安装目录2.2、上传安装包(无网络) or 直接下载(有网络)2.3、解压缩2.4、配置环境变量2.5、创建软连接2.6、更换镜像源2.7、验证是否安装成功 三、安装Vue 一、下载Nodejs Nodejs&#xff1a;https://nodejs.org/en/ 历史版本&#…

如何使用JMeter测试导入接口/导出接口

今天一上班&#xff0c;被开发问了一个问题&#xff1a;JMeter调试接口&#xff0c;文件导入接口怎么老是不通&#xff1f;还有导出文件接口&#xff0c;不知道文件导到哪里去了&#xff1f; 我一听&#xff0c;这不是JMeter做接口测试经常遇到的嘛&#xff0c;但是一时半会又…

解决视口动画插件jquery.aniview.js使用animate.css时无效的问题(最新版本网页视口动画插件的使用及没作用、没反应)

当网站页面元素进入视口时自动应用过渡效果。CSS过渡效果可以为网页添加动画效果&#xff0c;并提供了一种平滑的转换方式&#xff0c;使元素的变化更加流畅和生动。而通过jQuery插件来获取页面滚动位置决定合适调用动画效果。 一、官网 animate.css官网 一款强大的预设css3动…

Linux的基本指令(四)

目录 前言 时间相关的指令 date指令 时间戳 日志 时间戳转化为具体的时间 cal指令 find指令&#xff08;十分重要&#xff09; grep指令&#xff08;行文本过滤工具&#xff09; 学前补充 什么是打包和压缩&#xff1f; 为什么要打包和压缩&#xff1f; 怎么打包和…