重定向后 HttpClient 不处理正文
HttpClient not handling body after redirect
我正在使用 HttpClient(micronaut 2.2.0 的一部分)对从控制器重定向的资源执行 GET,如下所示:
@Controller
public class ExampleController {
@Client("https://www.vrt.be/") @Inject
RxHttpClient client;
@Get
public String getIt() {
return client.toBlocking().retrieve("/");
}
}
我是 运行 这个应用程序,通过 SAM CLI 使用具有 lambda 函数和 API 网关的模板。当发出 curl http://127.0.0.1:3000/
时,它会抛出异常。将模板部署到 AWS 本身时也会发生此异常。
19:38:58.007 [default-nioEventLoopGroup-1-1] DEBUG i.m.h.client.netty.DefaultHttpClient - Sending HTTP Request: GET /
19:38:58.008 [default-nioEventLoopGroup-1-1] DEBUG i.m.h.client.netty.DefaultHttpClient - Chosen Server: www.vrt.be(-1)
19:38:58.023 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - host: www.vrt.be
19:38:58.023 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - connection: close
19:38:58.873 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - HTTP Client Response Received for Request: GET https://www.vrt.be/
19:38:58.873 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Status Code: 301 Moved Permanently
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Content-Type: text/html; charset=iso-8859-1
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Content-Length: 230
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Connection: close
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Date: Tue, 01 Dec 2020 20:19:06 GMT
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Cache-Control: max-age=604800
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Location: https://www.vrt.be/nl/
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Vary: Accept-Encoding
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - X-Cache: Hit from cloudfront
...
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Response Body
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - ----
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.vrt.be/nl/">here</a>.</p>
</body></html>
19:38:58.877 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - ----
19:38:58.989 [main] ERROR i.m.f.a.p.AbstractLambdaContainerHandler - Error while handling request
io.micronaut.http.client.exceptions.HttpClientResponseException: Error decoding HTTP response body: No request present
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2148)
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2021)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
我不确定我对 HttpClient 的使用是否有问题,或者这是否是 HttpClient 实现中的实际问题。
TL;DR
创建文件
src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory
内容如下:
io.micronaut.http.simple.SimpleHttpResponseFactory
它将注册一个额外的 HttpResponseFactory
供任何内部 HTTP 请求使用。
为什么会出现这个问题?
默认情况下,Micronaut 函数应用程序注册了一个名为 MicronautAwsProxyResponseFactory
的 HttpResponseFactory
服务。它旨在处理 AWS API 响应并将所有其他响应委托给存储在 ALTERNATE
静态字段中的备选 HttpResponseFactory
。此字段由 SoftServiceLoader
初始化,检查是否有任何其他实例在任何 META-INF/services/io.micronaut.http.HttpResponseFactory
文件中注册。
您的示例性应用程序的问题始于 DefaultHttpClient
, line 2072。在这里,我们可以找到代表重定向的 redirectExchange
可流动对象。它被发布到事件循环,并定义(使用 first()
方法)它应该 return 重定向请求的结果或者 HttpStatus.notFound()
如果重定向没有发出任何值return.
现在,如果您查看堆栈跟踪的底部,您会发现:
Caused by: io.micronaut.http.server.exceptions.InternalServerException: No request present
at io.micronaut.function.aws.proxy.model.factory.MicronautAwsProxyResponseFactory.status(MicronautAwsProxyResponseFactory.java:79)
at io.micronaut.http.HttpResponseFactory.status(HttpResponseFactory.java:74)
at io.micronaut.http.HttpResponse.notFound(HttpResponse.java:105)
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2059)
... 51 more
它显示了导致异常的确切原因 - 它是 io.micronaut.http.HttpResponse.notFound()
。如果我们查看该方法的实现,我们会发现:
static <T> MutableHttpResponse<T> notFound() {
return HttpResponseFactory.INSTANCE.status(HttpStatus.NOT_FOUND);
}
HttpResponseFactory.INSTANCE
变量return是服务加载器注册的工厂,在我们的例子中是MicronautAwsProxyResponseFactory
.
现在我们来看看MicronautAwsProxyResponseFactory
是如何实现status(status,reason)
方法的:
@Override
public <T> MutableHttpResponse<T> status(HttpStatus status, String reason) {
final HttpRequest<Object> req = ServerRequestContext.currentRequest().orElse(null);
if (req instanceof MicronautAwsProxyRequest) {
final MicronautAwsProxyResponse<T> response = (MicronautAwsProxyResponse<T>) ((MicronautAwsProxyRequest<Object>) req).getResponse();
return response.status(status, reason);
} else {
if (ALTERNATE != null) {
return ALTERNATE.status(status, reason);
} else {
throw new InternalServerException("No request present");
}
}
}
当前请求不是MicronautAwsProxyRequest
的实例,所以流程转到else
分支。这里我们得到 ALTERNATE
未设置,这就是抛出异常的原因。
当你在service loader中注册io.micronaut.http.simple.SimpleHttpResponseFactory
时,上面的流程不会抛出异常,ALTERNATE.status(status,reason)
的结果会是returned.
这种极端情况不容易检测到,尤其是 http-server-netty
等其他模块注册自己的 HttpResponseFactory
,并且一切正常。例如,如果您将 http-server-netty
依赖项添加到您的项目,它会毫无问题地工作,因为它注册了 io.micronaut.http.server.netty.NettyHttpResponseFactory
来处理所有状态而不会抛出异常。
我猜像 micronaut-http-client
这样的模块可能会为开箱即用的服务加载器注册这个 io.micronaut.http.simple.SimpleHttpResponseFactory
,但这是 Micronaut 开发团队的问题。但是,它可能会引入一些副作用,所以我会在应用程序中为服务加载器注册这个工厂。
PS:如果你想调试你的问题,你可以 运行 使用调试器进行单元测试,如下所示。
package demo;
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.services.lambda.runtime.Context;
import io.micronaut.context.ApplicationContext;
import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler;
import io.micronaut.http.HttpMethod;
import org.junit.jupiter.api.Test;
class ExampleControllerTest {
static MicronautLambdaContainerHandler handler;
static Context lambdaContext = new MockLambdaContext();
static {
try {
handler = new MicronautLambdaContainerHandler(
ApplicationContext.build()
);
} catch (ContainerInitializationException e) {
e.printStackTrace();
}
}
@Test
void test() {
AwsProxyRequestBuilder builder = new AwsProxyRequestBuilder("/", HttpMethod.GET.toString());
AwsProxyResponse response = handler.proxy(builder.build(), lambdaContext);
System.out.println(response.getBody());
}
}
我正在使用 HttpClient(micronaut 2.2.0 的一部分)对从控制器重定向的资源执行 GET,如下所示:
@Controller
public class ExampleController {
@Client("https://www.vrt.be/") @Inject
RxHttpClient client;
@Get
public String getIt() {
return client.toBlocking().retrieve("/");
}
}
我是 运行 这个应用程序,通过 SAM CLI 使用具有 lambda 函数和 API 网关的模板。当发出 curl http://127.0.0.1:3000/
时,它会抛出异常。将模板部署到 AWS 本身时也会发生此异常。
19:38:58.007 [default-nioEventLoopGroup-1-1] DEBUG i.m.h.client.netty.DefaultHttpClient - Sending HTTP Request: GET /
19:38:58.008 [default-nioEventLoopGroup-1-1] DEBUG i.m.h.client.netty.DefaultHttpClient - Chosen Server: www.vrt.be(-1)
19:38:58.023 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - host: www.vrt.be
19:38:58.023 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - connection: close
19:38:58.873 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - HTTP Client Response Received for Request: GET https://www.vrt.be/
19:38:58.873 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Status Code: 301 Moved Permanently
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Content-Type: text/html; charset=iso-8859-1
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Content-Length: 230
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Connection: close
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Date: Tue, 01 Dec 2020 20:19:06 GMT
19:38:58.874 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Cache-Control: max-age=604800
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Location: https://www.vrt.be/nl/
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Vary: Accept-Encoding
19:38:58.875 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - X-Cache: Hit from cloudfront
...
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - Response Body
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - ----
19:38:58.876 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.vrt.be/nl/">here</a>.</p>
</body></html>
19:38:58.877 [default-nioEventLoopGroup-1-1] TRACE i.m.h.client.netty.DefaultHttpClient - ----
19:38:58.989 [main] ERROR i.m.f.a.p.AbstractLambdaContainerHandler - Error while handling request
io.micronaut.http.client.exceptions.HttpClientResponseException: Error decoding HTTP response body: No request present
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2148)
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2021)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
我不确定我对 HttpClient 的使用是否有问题,或者这是否是 HttpClient 实现中的实际问题。
TL;DR
创建文件
src/main/resources/META-INF/services/io.micronaut.http.HttpResponseFactory
内容如下:
io.micronaut.http.simple.SimpleHttpResponseFactory
它将注册一个额外的 HttpResponseFactory
供任何内部 HTTP 请求使用。
为什么会出现这个问题?
默认情况下,Micronaut 函数应用程序注册了一个名为 MicronautAwsProxyResponseFactory
的 HttpResponseFactory
服务。它旨在处理 AWS API 响应并将所有其他响应委托给存储在 ALTERNATE
静态字段中的备选 HttpResponseFactory
。此字段由 SoftServiceLoader
初始化,检查是否有任何其他实例在任何 META-INF/services/io.micronaut.http.HttpResponseFactory
文件中注册。
您的示例性应用程序的问题始于 DefaultHttpClient
, line 2072。在这里,我们可以找到代表重定向的 redirectExchange
可流动对象。它被发布到事件循环,并定义(使用 first()
方法)它应该 return 重定向请求的结果或者 HttpStatus.notFound()
如果重定向没有发出任何值return.
现在,如果您查看堆栈跟踪的底部,您会发现:
Caused by: io.micronaut.http.server.exceptions.InternalServerException: No request present
at io.micronaut.function.aws.proxy.model.factory.MicronautAwsProxyResponseFactory.status(MicronautAwsProxyResponseFactory.java:79)
at io.micronaut.http.HttpResponseFactory.status(HttpResponseFactory.java:74)
at io.micronaut.http.HttpResponse.notFound(HttpResponse.java:105)
at io.micronaut.http.client.netty.DefaultHttpClient.channelRead0(DefaultHttpClient.java:2059)
... 51 more
它显示了导致异常的确切原因 - 它是 io.micronaut.http.HttpResponse.notFound()
。如果我们查看该方法的实现,我们会发现:
static <T> MutableHttpResponse<T> notFound() {
return HttpResponseFactory.INSTANCE.status(HttpStatus.NOT_FOUND);
}
HttpResponseFactory.INSTANCE
变量return是服务加载器注册的工厂,在我们的例子中是MicronautAwsProxyResponseFactory
.
现在我们来看看MicronautAwsProxyResponseFactory
是如何实现status(status,reason)
方法的:
@Override
public <T> MutableHttpResponse<T> status(HttpStatus status, String reason) {
final HttpRequest<Object> req = ServerRequestContext.currentRequest().orElse(null);
if (req instanceof MicronautAwsProxyRequest) {
final MicronautAwsProxyResponse<T> response = (MicronautAwsProxyResponse<T>) ((MicronautAwsProxyRequest<Object>) req).getResponse();
return response.status(status, reason);
} else {
if (ALTERNATE != null) {
return ALTERNATE.status(status, reason);
} else {
throw new InternalServerException("No request present");
}
}
}
当前请求不是MicronautAwsProxyRequest
的实例,所以流程转到else
分支。这里我们得到 ALTERNATE
未设置,这就是抛出异常的原因。
当你在service loader中注册io.micronaut.http.simple.SimpleHttpResponseFactory
时,上面的流程不会抛出异常,ALTERNATE.status(status,reason)
的结果会是returned.
这种极端情况不容易检测到,尤其是 http-server-netty
等其他模块注册自己的 HttpResponseFactory
,并且一切正常。例如,如果您将 http-server-netty
依赖项添加到您的项目,它会毫无问题地工作,因为它注册了 io.micronaut.http.server.netty.NettyHttpResponseFactory
来处理所有状态而不会抛出异常。
我猜像 micronaut-http-client
这样的模块可能会为开箱即用的服务加载器注册这个 io.micronaut.http.simple.SimpleHttpResponseFactory
,但这是 Micronaut 开发团队的问题。但是,它可能会引入一些副作用,所以我会在应用程序中为服务加载器注册这个工厂。
PS:如果你想调试你的问题,你可以 运行 使用调试器进行单元测试,如下所示。
package demo;
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.services.lambda.runtime.Context;
import io.micronaut.context.ApplicationContext;
import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler;
import io.micronaut.http.HttpMethod;
import org.junit.jupiter.api.Test;
class ExampleControllerTest {
static MicronautLambdaContainerHandler handler;
static Context lambdaContext = new MockLambdaContext();
static {
try {
handler = new MicronautLambdaContainerHandler(
ApplicationContext.build()
);
} catch (ContainerInitializationException e) {
e.printStackTrace();
}
}
@Test
void test() {
AwsProxyRequestBuilder builder = new AwsProxyRequestBuilder("/", HttpMethod.GET.toString());
AwsProxyResponse response = handler.proxy(builder.build(), lambdaContext);
System.out.println(response.getBody());
}
}