【生产问题记录】一次简单的 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 {

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
		    // 从响应里面获取到状态码
		    HttpStatus statusCode = getHttpStatusCode(response);
		    switch (statusCode.series()) {
			      // 状态码 4xx
			      case 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 也算是多了一点了解。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 当出现Nginx请求400问题时,通常表示客户端发出了一个无效的请求。以下是一些可能的解决方案: 1. 检查URL请求方法:确保URL的格式正确,并使用正确的请求方法(如GET、POST等)。确保没有任何拼写错误或多余的字符。 2. 检查请求标头:确保请求的标头是正确的,并且没有任何误导或错误的信息。特别要注意Content-Length、Content-Type等标头是否正确。 3. 检查请求参数:如果请求中包含参数,确保参数的格式正确。检查参数名和值是否正确,并注意URL编码。 4. 清除浏览器缓存:有时候浏览器缓存可能导致一些请求问题。尝试清除浏览器缓存,然后重新发送请求。 5. 重启Nginx服务:如果以上方法都没有解决问题,可以尝试重启Nginx服务。首先停止Nginx服务,然后再重新启动。 6. 查看错误日志:在Nginx的错误日志中,可能会提供更详细的错误信息。查看错误日志,以便定位问题所在,并根据错误信息进行相应的处理。 如果以上方法都没有解决问题,可以尝试查看Nginx的文档或论坛,搜索其他用户的解决方案。另外,如果400问题频繁出现,可以联系Nginx的技术支持或开发者社区,获取更专业的帮助。 ### 回答2: 当使用Nginx时,遇到HTTP 400(Bad Request)错误,可以按以下步骤解决该问题: 1. 检查URL请求头:首先检查URL是否正确且完整,确保请求中的所有参数和请求头正确。确保URL中没有任何特殊字符或空格,正确地使用URL编码。 2. 检查请求方法:确保请求使用正确的HTTP方法(如GET、POST等)。如果请求方法不正确,服务器可能返回400错误。 3. 检查请求内容:检查请求体中的数据是否符合服务器的要求。特别关注JSON或XML格式数据是否按照规定的格式发送。 4. 检查请求大小限制:Nginx默认的`client_max_body_size`为1M,如果请求体大小超过该值,Nginx返回400错误。可以通过修改Nginx配置文件中的`client_max_body_size`值来解决该问题。 5. 检查Nginx配置文件:检查是否存在错误的配置项或缺少必要的配置项。可以使用`nginx -t`命令检查配置文件的语法错误,并使用`service nginx reload`重载配置文件。 6. 检查代理服务器:如果Nginx作为代理服务器,确保正确地将请求转发到后端服务器,并且后端服务器能够正确处理请求。可以通过暂时绕过Nginx直接请求后端服务器来排除代理服务器的问题。 7. 检查日志文件:查看Nginx的访问日志和错误日志,以了解更详细的错误信息。日志文件位于Nginx的`logs`目录下,具体位置可以通过查看Nginx配置文件中的`error_log`和`access_log`指令找到。 如果经过以上步骤后仍然无法解决问题,可以进一步检查应用程序或后端服务器的日志,以确定是由Nginx还是后端服务引起的400错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值