-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(gax): allow non-JSON HttpContent and absolute request URLs in HttpRequestRunnable #14134
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are | ||
| * met: | ||
| * | ||
| * * Redistributions of source code must retain the above copyright | ||
| * notice, this list of conditions and the following disclaimer. | ||
| * * Redistributions in binary form must reproduce the above | ||
| * copyright notice, this list of conditions and the following disclaimer | ||
| * in the documentation and/or other materials provided with the | ||
| * distribution. | ||
| * * Neither the name of Google LLC nor the names of its | ||
| * contributors may be used to endorse or promote products derived from | ||
| * this software without specific prior written permission. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| package com.google.api.gax.httpjson; | ||
|
|
||
| import com.google.api.client.http.HttpContent; | ||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| /** | ||
| * Formatter for requests that supply arbitrary {@link HttpContent} payloads (such as raw bytes or | ||
| * streams) rather than serialized JSON strings. | ||
| */ | ||
| @NullMarked | ||
| interface HttpContentRequestFormatter<MessageFormatT> extends HttpRequestFormatter<MessageFormatT> { | ||
|
Check warning on line 40 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpContentRequestFormatter.java
|
||
|
|
||
| /** Returns {@link HttpContent} representing the request body. */ | ||
| HttpContent getHttpContent(MessageFormatT apiMessage); | ||
|
|
||
| /** | ||
| * Not supported. Formatters implementing this interface handle raw payloads that may not be | ||
| * serializable as JSON strings, providing them via {@link #getHttpContent(Object)} instead. | ||
| * | ||
| * @throws UnsupportedOperationException always | ||
| */ | ||
| @Override | ||
| default String getRequestBody(MessageFormatT apiMessage) { | ||
| throw new UnsupportedOperationException( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me think that |
||
| "HttpContentRequestFormatter uses getHttpContent() instead of getRequestBody()"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,21 +29,18 @@ | |
| */ | ||
| package com.google.api.gax.httpjson; | ||
|
|
||
| import com.google.api.client.http.ByteArrayContent; | ||
| import com.google.api.client.http.EmptyContent; | ||
| import com.google.api.client.http.GenericUrl; | ||
| import com.google.api.client.http.HttpContent; | ||
| import com.google.api.client.http.HttpMediaType; | ||
| import com.google.api.client.http.HttpMethods; | ||
| import com.google.api.client.http.HttpRequest; | ||
| import com.google.api.client.http.HttpRequestFactory; | ||
| import com.google.api.client.http.HttpResponse; | ||
| import com.google.api.client.http.HttpResponseException; | ||
| import com.google.api.client.http.HttpTransport; | ||
| import com.google.api.client.http.json.JsonHttpContent; | ||
| import com.google.api.client.json.JsonFactory; | ||
| import com.google.api.client.json.JsonObjectParser; | ||
| import com.google.api.client.json.gson.GsonFactory; | ||
| import com.google.api.client.util.GenericData; | ||
| import com.google.api.gax.tracing.ApiTracer; | ||
| import com.google.auth.Credentials; | ||
| import com.google.auth.http.HttpCredentialsAdapter; | ||
|
|
@@ -154,8 +151,6 @@ public void run() { | |
| } | ||
|
|
||
| HttpRequest createHttpRequest() throws IOException { | ||
| GenericData tokenRequest = new GenericData(); | ||
|
|
||
| HttpRequestFormatter<RequestT> requestFormatter = methodDescriptor.getRequestFormatter(); | ||
|
|
||
| HttpRequestFactory requestFactory; | ||
|
|
@@ -166,24 +161,24 @@ HttpRequest createHttpRequest() throws IOException { | |
| requestFactory = httpTransport.createRequestFactory(); | ||
| } | ||
|
|
||
| JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); | ||
| // Create HTTP request body. | ||
| String requestBody = requestFormatter.getRequestBody(request); | ||
| HttpContent jsonHttpContent; | ||
| if (!Strings.isNullOrEmpty(requestBody)) { | ||
| jsonFactory.createJsonParser(requestBody).parse(tokenRequest); | ||
| jsonHttpContent = | ||
| new JsonHttpContent(jsonFactory, tokenRequest) | ||
| .setMediaType((new HttpMediaType("application/json; charset=utf-8"))); | ||
| HttpContent httpContent; | ||
| if (requestFormatter instanceof HttpContentRequestFormatter) { | ||
| httpContent = | ||
| ((HttpContentRequestFormatter<RequestT>) requestFormatter).getHttpContent(request); | ||
| } else { | ||
| // Force underlying HTTP lib to set Content-Length header to avoid 411s. | ||
| // See EmptyContent.java. | ||
| jsonHttpContent = new EmptyContent(); | ||
| httpContent = createJsonHttpContent(requestFormatter); | ||
| } | ||
|
|
||
| // Populate URL path and query parameters. | ||
| String normalizedEndpoint = normalizeEndpoint(endpoint); | ||
| GenericUrl url = new GenericUrl(normalizedEndpoint + requestFormatter.getPath(request)); | ||
| String path = requestFormatter.getPath(request); | ||
| GenericUrl url; | ||
| if (path.startsWith("http://") || path.startsWith("https://")) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this for the upload URL that is returned from the start request?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's the use case for this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Do you mind adding a comment for this special case? |
||
| url = new GenericUrl(path); | ||
| } else { | ||
| String normalizedEndpoint = normalizeEndpoint(endpoint); | ||
| url = new GenericUrl(normalizedEndpoint + path); | ||
| } | ||
| Map<String, List<String>> queryParams = requestFormatter.getQueryParamNames(request); | ||
| for (Entry<String, List<String>> queryParam : queryParams.entrySet()) { | ||
| if (queryParam.getValue() != null) { | ||
|
|
@@ -196,20 +191,20 @@ HttpRequest createHttpRequest() throws IOException { | |
| tracer.requestUrlResolved(url.build()); | ||
| } | ||
|
|
||
| HttpRequest httpRequest = buildRequest(requestFactory, url, jsonHttpContent); | ||
| HttpRequest httpRequest = buildRequest(requestFactory, url, httpContent); | ||
|
|
||
| for (Map.Entry<String, Object> entry : headers.getHeaders().entrySet()) { | ||
| HttpHeadersUtils.setHeader( | ||
| httpRequest.getHeaders(), entry.getKey(), (String) entry.getValue()); | ||
| } | ||
|
|
||
| httpRequest.setParser(new JsonObjectParser(jsonFactory)); | ||
| httpRequest.setParser(new JsonObjectParser(GsonFactory.getDefaultInstance())); | ||
|
|
||
| return httpRequest; | ||
| } | ||
|
|
||
| private HttpRequest buildRequest( | ||
| HttpRequestFactory requestFactory, GenericUrl url, HttpContent jsonHttpContent) | ||
| HttpRequestFactory requestFactory, GenericUrl url, HttpContent httpContent) | ||
| throws IOException { | ||
| // A workaround to support PATCH request. This assumes support of "X-HTTP-Method-Override" | ||
| // header on the server side, which GCP services usually do. | ||
|
|
@@ -235,7 +230,7 @@ private HttpRequest buildRequest( | |
| if (HttpMethods.PATCH.equals(actualHttpMethod)) { | ||
| actualHttpMethod = HttpMethods.POST; | ||
| } | ||
| HttpRequest httpRequest = requestFactory.buildRequest(actualHttpMethod, url, jsonHttpContent); | ||
| HttpRequest httpRequest = requestFactory.buildRequest(actualHttpMethod, url, httpContent); | ||
| if (originalHttpMethod != null && !originalHttpMethod.equals(actualHttpMethod)) { | ||
| HttpHeadersUtils.setHeader( | ||
| httpRequest.getHeaders(), "X-HTTP-Method-Override", originalHttpMethod); | ||
|
|
@@ -284,6 +279,16 @@ private String normalizeEndpoint(String rawEndpoint) { | |
| return normalized; | ||
| } | ||
|
|
||
| private HttpContent createJsonHttpContent(HttpRequestFormatter<RequestT> requestFormatter) { | ||
| String requestBody = requestFormatter.getRequestBody(request); | ||
| if (!Strings.isNullOrEmpty(requestBody)) { | ||
| return ByteArrayContent.fromString("application/json; charset=utf-8", requestBody); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does changing from JsonHttpContent to ByteArrayContent change any behaviors? |
||
| } | ||
| // Force underlying HTTP lib to set Content-Length header to avoid 411s. | ||
| // See EmptyContent.java. | ||
| return new EmptyContent(); | ||
| } | ||
|
|
||
| @FunctionalInterface | ||
| interface ResultListener { | ||
| void setResult(RunnableResult result); | ||
|
|
||
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.
The
HttpContentRequestFormatterinterface is currently package-private. SinceHttpRequestFormatteris a public interface implemented by generated client stubs and other classes outside of thecom.google.api.gax.httpjsonpackage, this interface must be declaredpublicso that it can be implemented by external classes (e.g., for resumable uploads in other packages).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 is intentionally package private, as implementing classes are intended to live only in this package.