重定向后 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 函数应用程序注册了一个名为 MicronautAwsProxyResponseFactoryHttpResponseFactory 服务。它旨在处理 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());
    }
}