Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP client refactor #11182

Merged
merged 23 commits into from
Sep 30, 2024
Merged

HTTP client refactor #11182

merged 23 commits into from
Sep 30, 2024

Conversation

yawkat
Copy link
Member

@yawkat yawkat commented Sep 13, 2024

This is the continuation of #11158 and #11177.

Now that the previous PRs have migrated the reading/writing to ByteBody, it is time to unify the code paths. This PR is a complex refactor of DefaultHttpClient that merges most of the code for normal exchange requests, streaming requests, and proxy requests into a single sendRequestWithRedirects method. In particular, logging, filtering, connection management, redirect handling and error handling are now merged using the ByteBody API.

This PR is a result of various small refactors that all preserve behavior, but the combined change is large enough that it basically looks like a rewrite. Behavior should be almost the same, and all tests still pass. There are some minor changes I can think of, but they seem positive:

  • For exchange requests, the configured request-timeout is only applied to the "top" request instead of all nested redirect requests as well. The inner timeouts are pointless anyway, since the outer timeout will trigger before the inner timeouts do.
  • There was some confusion in the code between the "parent request" (i.e. the request from the ServerRequestContext) and the "original request" (the request that produced a redirect) when it comes to redirect handling, in particular for streamed requests. I think this was simply a bug that was never hit by the test cases because they mostly hit the non-streaming client.
  • Client filters should be a bit more powerful now. Before this PR, URI resolution (i.e. when you do GET("/foo") it would resolve to GET("https://example.com/foo") depending on service ID) would happen before filters, and then would be carried next to the request through the filters. This means that filters can (a) not replace the request because provideResponse would ignore the new request, and (b) they could not mutate the URI of the original request because the new URI would be ignored in favor of the previously resolved URI. In the new implementation, the URI is still resolved before the filters, but it's set on the request, sent through the filters, and then the filtered request is used in the downstream code. So it should be possible to change the URI in a filter, but I've not added a test.

There are still some more changes I want to make to the client, e.g. getting rid of StreamingHttpRequest for the proxy client. I'm also thinking of exposing sendRawRequest as a new API (another HttpClient interface). But these will happen in future PRs.

This is an intermediate PR of my refactor, in the interest of keeping the changes reviewable.
First version of replacing the previous mixed response handling with a single ByteBody-based handler (similar to PipeliningServerHandler on the server). The purpose of this is to (a) unify some code between the streaming and aggregating code paths in DefaultHttpClient, (b) allow ByteBody-based streaming for ProxyHttpClient in the future so that ProxyHttpClient can be implemented for the jdk client and servlet server, (c) eventually create a ByteBody-based HTTP client API (maybe just in ConnectionManager) that allows lower level access to the request and thus can be used to implement the oci sdk http client.
# Conflicts:
#	http-netty/src/main/java/io/micronaut/http/netty/body/StreamingNettyByteBody.java
# Conflicts:
#	http-netty/src/main/java/io/micronaut/http/netty/body/StreamingNettyByteBody.java
# Conflicts:
#	http-netty/src/main/java/io/micronaut/http/netty/body/StreamingNettyByteBody.java
@yawkat yawkat added this to the 4.7.0 milestone Sep 13, 2024
@yawkat
Copy link
Member Author

yawkat commented Sep 13, 2024

FYI @dstepanov, this PR also migrates the client to use mostly Mono, so this is 90% of the way to ExecutionFlow already. I may do that in a future PR, it just needs a solution to the cancellation feature.

# Conflicts:
#	http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java
}
}

pipeline.addLast(ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE, new Http1ResponseHandler(new Http1ResponseHandler.ResponseListener() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be pulled into an inner class to make it easier to read

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id prefer not to, it's only like 50 lines but it would require 7 constructor parameters from the method

@@ -627,8 +627,8 @@ public Optional<io.netty.handler.codec.http.HttpRequest> toHttpRequestDirect() {
}

@Override
public @NonNull Optional<ByteBody> byteBodyDirect() {
return Optional.of(byteBody());
public ByteBody byteBodyDirect() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this now Nullable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's marked as such on the super method

Base automatically changed from client-response-body to 4.7.x September 20, 2024 09:43
# Conflicts:
#	http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java
@yawkat yawkat marked this pull request as ready for review September 20, 2024 10:03
@yawkat yawkat added the type: improvement A minor improvement to an existing feature label Sep 20, 2024
if (parentRequest != null) {
// todo: migrate to new filter
filters.add(
GenericHttpFilter.createLegacyFilter(new ClientServerContextFilter(parentRequest), new FilterOrder.Fixed(Ordered.HIGHEST_PRECEDENCE))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't see why not

parentRequest,
null,
request.uri(requestURI),
(req, resp) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract a new method

}
// first: connect
return connectionManager.connect(requestKey, blockHint)
.flatMap(poolHandle -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract a new methoda

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont agree, this code is basically imperative

// send the raw request
return sendRawRequest(poolHandle, request, byteBody, c -> handleResponseError(request, c));
})
.flatMap(byteBodyResponse -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract a new method

).asNativeBuffer();
}
// send the raw request
return sendRawRequest(poolHandle, request, byteBody, c -> handleResponseError(request, c));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More correct would be to flatMap response of this method instead of the outer one to avoid an extra call for the error path

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's functionally identical

sink.error(e);
});
pipeline.addLast(streamWriter);
byteBuf = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intellij noticed that byteBuf can be null but the Netty API doesn't allow it to be null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all uses of byteBuf are guarded with checks of either byteBuf or streamWriter

).map(r -> (HttpResponse<O>) r);
});

Duration requestTimeout = configuration.getRequestTimeout();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract applyConfiguration

@dstepanov
Copy link
Contributor

Some of the code looks way too long, and the code is going beyond my screen. I prefer to extract some of the lamdas to have an appropriate name to make the code more understandable.
Sometimes, there are two different requests, which makes the code confusing.

@yawkat
Copy link
Member Author

yawkat commented Sep 30, 2024

ive extracted the response handling for exchange. For the other four cases, here is my reasoning why I don't want to:

  • The buildStreamExchange and proxy methods are thin wrappers around sendRequestWithRedirects, essentially in the form proxy() { return sendRequestWithRedirects((req, resp) -> { ... actual code ... }) }. Because they do so little outside the lambda, I think it's more readable to keep the code there, so there's less "indirection" when reading the code. This outweighs the disadvantage of having a large lambda, imo.
  • sendRequestWithRedirectsNoFilter has two big lambdas, but imo it's still more readable this way. The mono code is essentially acting like an async-await, the flatMap calls wait for the result of the previous lambda and run the next lambda when that result is available. It is essentially imperative, so you can read the method from top to bottom.
  • sendRawRequest has a giant Http1ResponseHandler.ResponseListener implementation that is not great. However, that anonymous class only has fairly light implementations of the six methods, only a few lines each. However it uses seven variables from the enclosing method, so if this was a separate class, id need 7 fields, 7 constructor args, 7 sets, which makes everything even more unreadable. So while I think the current version isn't great, the alternative seems worse.

Copy link

sonarcloud bot commented Sep 30, 2024

@yawkat yawkat merged commit f74dae1 into 4.7.x Sep 30, 2024
21 checks passed
@yawkat yawkat deleted the client-refactor2 branch September 30, 2024 13:10
@yawkat yawkat mentioned this pull request Oct 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: improvement A minor improvement to an existing feature
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants