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

feat: Dynamic Routing Headers for HttpJson #1667

Merged
merged 50 commits into from
Jun 6, 2023
Merged

Conversation

lqiu96
Copy link
Contributor

@lqiu96 lqiu96 commented May 5, 2023

Changes in this PR:

Showcase Request Headers

Current Behavior (No manual percent-encoding on client side)

Screenshot 2023-05-11 at 10 24 41 AM Screenshot 2023-05-11 at 10 25 04 AM

Manually percent-encoding the header key and values

Screenshot 2023-05-11 at 10 22 18 AM Screenshot 2023-05-11 at 10 22 52 AM

Showcase Testing

Intercept the header values before the get sent out via ClientInterceptors (grpc and httpjson).


Thank you for opening a Pull Request! For general contributing guidelines, please refer to contributing guide

Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #1666 ☕️

@product-auto-label product-auto-label bot added the size: xl Pull request size is extra large. label May 5, 2023
@lqiu96 lqiu96 changed the title Main dynamic routing headers feat: Dynamic Routing Headers for HttpJson May 5, 2023
@lqiu96 lqiu96 marked this pull request as ready for review May 30, 2023 21:20
@lqiu96 lqiu96 requested a review from a team as a code owner May 30, 2023 21:20
@lqiu96 lqiu96 added the owlbot:run Add this label to trigger the Owlbot post processor. label May 30, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label May 30, 2023
public ApiFuture<ResponseT> futureCall(RequestT request, ApiCallContext context) {
ApiCallContext newCallContext = context;
String encodedHeader = paramsEncoder.encode(request);
if (encodedHeader != null && !encodedHeader.isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be a miss in the past, looks like we were sending the header even it is empty, do we have requirement that we should not send this header if it's empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was following these two guidances from the routing.proto:
https://github.com/googleapis/googleapis/blob/6782d08a0fa56c39070f9d1337d629eaecfe2fc1/google/api/routing.proto#L134-L150
and
https://github.com/googleapis/googleapis/blob/6782d08a0fa56c39070f9d1337d629eaecfe2fc1/google/api/routing.proto#L109-L110

My understanding is that we should not be sending the header if it's empty. Just noticed that Grpc's callables don't check and I can add a check for them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you confirm with Slava? You are probably right and I agree that we should not send header if it's empty, but want to make sure it would not break existing behavior(in case some server are expecting empty header).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Conversation with Slava:

the behaviour should be to NOT send the x-goog-request-params at all
server-side will likely do the right thing even if you send an empty value


Map<String, String> headerMap = callOptions.getRequestHeaderMap();
for (Map.Entry<String, List<String>> extraHeaderEntrySet :
httpJsonContext.getExtraHeaders().entrySet()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, I though we already have logics to handle extra headers, but looks like httpJsonContext.getExtraHeaders() was never used, I think it still makes sense to use it, but the benefit is much smaller than I expected.
That being said, I'm not sure setting it to HttpJsonCallOptions is the best way. Based on these comment, HttpJsonCallOptions is for API specific data not request specific data, and the dynamic routing header is request specific, which is inline with what I'm thinking as well. We can change this behavior if we want but I would see if it there is a way to set the header to HttpJsonMetadata directly.
There is also a TODO below // TODO: add headers interceptor logic which could be related as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can change this behavior if we want but I would see if it there is a way to set the header to HttpJsonMetadata directly.

I was actually looking into this before and I wasn't able to think of a clean way to do this. I believe the headers are for the class are initially set empty:

clientCall.start(new FutureListener<>(future), HttpJsonMetadata.newBuilder().build());

Ideally, I would be able to just populate the headers there, but I couldn't find a clean way to access the context.

ManagedHttpJsonChannel's newCall only takes in callOptions and I stored it there:

public <RequestT, ResponseT> HttpJsonClientCall<RequestT, ResponseT> newCall(
ApiMethodDescriptor<RequestT, ResponseT> methodDescriptor, HttpJsonCallOptions callOptions) {
return new HttpJsonClientCallImpl<>(
methodDescriptor,
endpoint,
callOptions,
httpTransport,
executor,
deadlineScheduledExecutorService);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I think I have a bit of leeway here. I see that HttpJsonClientCalls futureUnaryCall is package-private:

static <RequestT, ResponseT> ApiFuture<ResponseT> futureUnaryCall(
HttpJsonClientCall<RequestT, ResponseT> clientCall, RequestT request) {

I'm thinking about just adding HttpJsonCallContext as a third param so I can have access to the headers without having to add it to the call options. I can populate the HttpJsonMetadata with the extraHeaders in the callcontext.

* @param headerKey the header key for the routing header param
* @param fieldValue the field value from a request
*/
public void add(String headerKey, String fieldValue) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Now that we know we have to encode the headers, I think we should do it in a more generic way, like in RequestUrlParamsEncoder or even encode all extraHeaders.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we need to encode all headers? I thought it was just the routing headers since it explicitly called for it. We have this header that I believe isn't encoded: x-goog-api-client -> gl-java/11.0.19 gapic/ gax/2.27.1-SNAPSHOT rest/.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to encode all headers?

That's a great question. I think we should but I'm not sure. There are some security risks of not encoding urls, so I was under the impression that headers should be encoded and are already encoded as well by the downstream grpc or http libraries. Probably not part of this PR but we can do a little more investigation on the headers encoding in general. As part of this PR, I think we should move this logic to RequestUrlParamsEncoder because that class is specific for the routing headers and the name is perfect for this logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably not part of this PR but we can do a little more investigation on the headers encoding in general.

Sure! I will create an issue in the backlog to investigate this.

As part of this PR, I think we should move this logic to RequestUrlParamsEncoder because that class is specific for the routing headers and the name is perfect for this logic.

Yep, I agree. The only concern I have is that changing RequestUrlParamsEncoder to now encode the header values might be a behavior breaking change:

/**
* Encodes the {@code request} in a form of a URL parameters string, for example {@code
* "param1=value+1&param2=value2%26"}. This method may optionally validate that the name-value
* paris are URL-encoded, but it will not perform the actual encoding of them (it will only
* concatenate the valid individual name-value pairs in a valid URL parameters string). This is
* so, because in most practical cases the name-value paris are already URL-encoded.
*
* @param request request message
* @throws IllegalArgumentException if is not
*/
@Override
public String encode(RequestT request) {
as it expects the params to the percent encoded already.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This class is only used for encoding the request params(which is effective routing headers), and it is marked as @InternalAPI so we should be able to change it at anytime.

Copy link
Contributor Author

@lqiu96 lqiu96 Jun 6, 2023

Choose a reason for hiding this comment

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

I think it would make sense to have all routing headers to be built with RequestParamsBuilder (both implicit and explicit) and have it return a map of the params. It would be consistent instead of having explicit -> RequestParamsBuilder and implicit -> ImmutableMap.Builder. Both would have some light validation done with checking that the header keys and values are non-null and non-empty.

It is adding a new public method to maintain, but I think it is better than having to add more logic for handling null PathTemplates in the original method. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

SGTM.

Copy link
Member

Choose a reason for hiding this comment

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

@blakeli0 @lqiu96 Did this effectively change the behavior of implicit routing headers from not encoding params to now URL encoding them?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it did. There was another discussion regarding should we add it or not for existing routing headers, we discussed offline and confirmed with Slava that all routing header should be encoded.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, there was a discussion with Slava about the need to encode both the header key and value. We saw there was customer issue (b/283866374) regarding having an unencoded value.

Map<String, Object> extraHeaders = new HashMap<>();
for (Map.Entry<String, List<String>> entrySet : headers.entrySet()) {
// HeaderValueList is always non-null. Check that it contains at least one value.
// Should only ever contain one value, but take the first one if there are multiple.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you create an issue and link it in the comment?

Copy link
Contributor Author

@lqiu96 lqiu96 Jun 6, 2023

Choose a reason for hiding this comment

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

Metadata/ extraHeaders Issue: #1752

I'll add this as a TODO in the comments.

@@ -7,4 +7,10 @@
<className>com/google/api/gax/paging/Page</className>
<method>* stream*(*)</method>
</difference>
<!-- Remove url encoding validation from RequestUrlParamsEncoder (@InternalApi) -->
<difference>
<differenceType>7004</differenceType>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it is an internal api, is there a way to ignore all types and methods for this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems CLIRR doesn't have a clear way to exclude based on annotation: mojohaus/clirr-maven-plugin#27

I think the second best way to ignore all types and methods for this class is to exclude this file in the clirr pom config.

* @param headerKey the header key for the routing header param
* @param fieldValue the field value from a request
*/
public void add(String headerKey, String fieldValue) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm good with the change in RequestUrlParamsEncoder. That change also means that we may not need this method anymore, I think we should either keep implicit dynamic routing header as it is, or move this logic to the method above, and pass PathTemplate as null for implicit dynamic routing headers. Passing null around is not really a good practice and usually having an overloaded method is cleaner, but introducing new public method also has a cost.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jun 6, 2023

[gapic-generator-java-root] SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 5 Code Smells

83.9% 83.9% Coverage
9.6% 9.6% Duplication

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jun 6, 2023

[java_showcase_integration_tests] SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 6 Code Smells

35.5% 35.5% Coverage
12.4% 12.4% Duplication

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jun 6, 2023

[java_showcase_unit_tests] SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 6 Code Smells

83.6% 83.6% Coverage
12.4% 12.4% Duplication

@@ -64,6 +64,6 @@ public void call(
HttpJsonClientCall<RequestT, ResponseT> call = HttpJsonClientCalls.newCall(descriptor, context);
HttpJsonDirectStreamController<RequestT, ResponseT> controller =
new HttpJsonDirectStreamController<>(call, responseObserver);
controller.start(request);
controller.start(request, context);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Like this change!

@lqiu96 lqiu96 merged commit 003b993 into main Jun 6, 2023
@lqiu96 lqiu96 deleted the main-dynamic_routing_headers branch June 6, 2023 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size: xl Pull request size is extra large.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Dynamic Routing Headers for HttpJson
3 participants