-
Notifications
You must be signed in to change notification settings - Fork 57
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
Conversation
gax-java/gax/src/main/java/com/google/api/gax/rpc/RequestParamsBuilder.java
Outdated
Show resolved
Hide resolved
public ApiFuture<ResponseT> futureCall(RequestT request, ApiCallContext context) { | ||
ApiCallContext newCallContext = context; | ||
String encodedHeader = paramsEncoder.encode(request); | ||
if (encodedHeader != null && !encodedHeader.isEmpty()) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep will do.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
Line 83 in ef4a7ff
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:
Lines 73 to 83 in ef4a7ff
public <RequestT, ResponseT> HttpJsonClientCall<RequestT, ResponseT> newCall( | |
ApiMethodDescriptor<RequestT, ResponseT> methodDescriptor, HttpJsonCallOptions callOptions) { | |
return new HttpJsonClientCallImpl<>( | |
methodDescriptor, | |
endpoint, | |
callOptions, | |
httpTransport, | |
executor, | |
deadlineScheduledExecutorService); | |
} |
There was a problem hiding this comment.
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:
Lines 79 to 80 in ef4a7ff
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.
gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java
Outdated
Show resolved
Hide resolved
gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonMetadata.java
Outdated
Show resolved
Hide resolved
...ax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonUnaryRequestParamCallable.java
Outdated
Show resolved
Hide resolved
...ase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITDynamicRoutingHeaders.java
Show resolved
Hide resolved
* @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) { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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/
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/RequestUrlParamsEncoder.java
Lines 68 to 79 in ef4a7ff
/** | |
* Encodes the {@code request} in a form of a URL parameters string, for example {@code | |
* "param1=value+1¶m2=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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
[gapic-generator-java-root] SonarCloud Quality Gate failed. |
[java_showcase_integration_tests] SonarCloud Quality Gate failed. |
[java_showcase_unit_tests] SonarCloud Quality Gate failed. |
@@ -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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this change!
Changes in this PR:
RequestParamsBuilder
to percent-encode both the header key and value (as part of https://github.com/googleapis/googleapis/blob/b9792eec95cc5c75e5fb173bb068aecde45881b1/google/api/routing.proto#L62-L64)Showcase Request Headers
Current Behavior (No manual percent-encoding on client side)
Manually percent-encoding the header key and values
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:
Fixes #1666 ☕️