FeignClient默认连接方式HttpURLConnection之坑---get请求变为post,访问405
迪丽瓦拉
2025-05-28 07:56:16
0

目录

 

背景:     

现象:

分析:

总结:


 

背景:     

      在项目中,使用feignClient 进行http 服务调用,feignClient的默认连接方式为HttpURLConnection,因为HttpURLConnection没有连接池,并发高的时候,会有一定的网络开销,在做项目优化的时候,替换改为okHttp以便复用其连接池。基于这个思路,按照feignClient的配置要求,在yaml中进行配置替换后,简单验证没问题,则正常上线了。。。。

现象:

不出意外的还是出了意外,服务调用出现了405 - 用来访问的 HTTP 调用不被允许(方法不被允许),赶紧回滚排查。

排查报错的调研发现,调用写法形如下:

@GetMapping("/inner/xxx/xx/xx")
Result test(@RequestBody TestDto dto);

明明是get请求,且之前是正常的,为什么替换了okHttp后,会出现服务调用不被允许呢,进一步排查,发现最终发起的是post请求,百思不得其解。。。。。

分析:

从feignClient的源码开始排查,feignClient 默认是feign.Client.Default#execute 执行http请求,源码为:

    @Overridepublic Response execute(Request request, Options options) throws IOException {// convertAndSend 获取连接HttpURLConnection connection = convertAndSend(request, options);return convertResponse(connection).toBuilder().request(request).build();}
进入查看convertAndSend的逻辑,重点是

//如果请求的请求体不为空,则设置 connection.setDoOutput(true);,记住这个
     

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {final HttpURLConnectionconnection =(HttpURLConnection) new URL(request.url()).openConnection();if (connection instanceof HttpsURLConnection) {HttpsURLConnection sslCon = (HttpsURLConnection) connection;if (sslContextFactory != null) {sslCon.setSSLSocketFactory(sslContextFactory);}if (hostnameVerifier != null) {sslCon.setHostnameVerifier(hostnameVerifier);}}connection.setConnectTimeout(options.connectTimeoutMillis());connection.setReadTimeout(options.readTimeoutMillis());connection.setAllowUserInteraction(false);connection.setInstanceFollowRedirects(true);connection.setRequestMethod(request.method());Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING);booleangzipEncodedRequest =contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);booleandeflateEncodedRequest =contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);boolean hasAcceptHeader = false;Integer contentLength = null;for (String field : request.headers().keySet()) {if (field.equalsIgnoreCase("Accept")) {hasAcceptHeader = true;}for (String value : request.headers().get(field)) {if (field.equals(CONTENT_LENGTH)) {if (!gzipEncodedRequest && !deflateEncodedRequest) {contentLength = Integer.valueOf(value);connection.addRequestProperty(field, value);}} else {connection.addRequestProperty(field, value);}}}// Some servers choke on the default accept string.if (!hasAcceptHeader) {connection.addRequestProperty("Accept", "*/*");}//如果请求的请求体不为空,则设置 connection.setDoOutput(true);,记住这个if (request.body() != null) {if (contentLength != null) {connection.setFixedLengthStreamingMode(contentLength);} else {connection.setChunkedStreamingMode(8196);}connection.setDoOutput(true);OutputStream out = connection.getOutputStream();if (gzipEncodedRequest) {out = new GZIPOutputStream(out);} else if (deflateEncodedRequest) {out = new DeflaterOutputStream(out);}try {out.write(request.body());} finally {try {out.close();} catch (IOException suppressed) { // NOPMD}}}return connection;}
重点是这段逻辑,请记住:如果请求的请求体不为空,则设置 connection.setDoOutput(true);
if (request.body() != null) {// 忽略其他逻辑// ....connection.setDoOutput(true);OutputStream out = connection.getOutputStream();
}

继续跟进 OutputStream out = connection.getOutputStream();的源码

@Overridepublic synchronized OutputStream getOutputStream() throws IOException {connecting = true;SocketPermission p = URLtoSocketPermission(this.url);if (p != null) {try {return AccessController.doPrivilegedWithCombiner(new PrivilegedExceptionAction() {public OutputStream run() throws IOException {return getOutputStream0();}}, null, p);} catch (PrivilegedActionException e) {throw (IOException) e.getException();}} else {return getOutputStream0();}}

继续查看:sun.net.www.protocol.http.HttpURLConnection#getOutputStream0

private synchronized OutputStream getOutputStream0() throws IOException {try {if (!doOutput) {throw new ProtocolException("cannot write to a URLConnection"+ " if doOutput=false - call setDoOutput(true)");}if (method.equals("GET")) {method = "POST"; // Backward compatibility}//忽略其他逻辑//......
}

由此看到了,在上一步,因为逻辑中发现有请求体,设置了connection.setDoOutput(true);此处,doOutput 为true时,如果请求是GET请求,会转为POST请求,结果真相大白。。。。

总结:

此种问题的出现,本质还是对rest接口定义不规范造成的。比如之前被调用方可能是用@RequestMapping注解,没特殊指定是get请求,还是post请求,则两种请求都可以,后面调用方可能改为了指定是post请求约束。我们作为调用方,表象是用的get请求,实际走的是post请求,所以没有影响,后面改为okHttp后,okHttp不会做这种特殊的转换,所以我们的请求还是get请求,故而就会有问题了

相关内容