Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
*/
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.HttpContent;
import com.google.api.core.BetaApi;
import com.google.api.pathtemplate.PathTemplate;
import com.google.common.base.Strings;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -56,4 +61,17 @@ public interface HttpRequestFormatter<MessageFormatT> {
default List<PathTemplate> getAdditionalPathTemplates() {
return Collections.emptyList();
}

/**
* Return {@link HttpContent} representing the request body. Defaults to converting {@link
* #getRequestBody(Object)} to JSON, or {@link EmptyContent} if the body is empty.
*/
@BetaApi
default HttpContent getHttpContent(MessageFormatT apiMessage) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we expect this method to be used by classes other than HttpRequestRunnable? If not, I would prefer it to be a private helper method in HttpRequestRunnable instead of a public method in this interface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The only caller is expected to be HttpRequestRunnable, but the point of this being a method on the formatter interface is for formatters that process binary message bodies (e.g. for chunk uploads) to be able to override this to be not-JSON. See e.g. in the unit test for this functionality.

If this JSON implementation were a private helper in HttpRequestRunnable then IIUC the special case logic for the binary case would have to live there as well (e.g. switching behavior based on instanceof the request). IMO it's cleaner for any special casing required for a particular formatter to live in that formatter. I can play around with alternative ways to express that differently if you feel strongly though.

String requestBody = getRequestBody(apiMessage);
if (!Strings.isNullOrEmpty(requestBody)) {
return ByteArrayContent.fromString("application/json; charset=utf-8", requestBody);
}
return new EmptyContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,20 @@
*/
package com.google.api.gax.httpjson;

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;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -154,8 +148,6 @@ public void run() {
}

HttpRequest createHttpRequest() throws IOException {
GenericData tokenRequest = new GenericData();

HttpRequestFormatter<RequestT> requestFormatter = methodDescriptor.getRequestFormatter();

HttpRequestFactory requestFactory;
Expand All @@ -166,24 +158,18 @@ 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")));
} else {
// Force underlying HTTP lib to set Content-Length header to avoid 411s.
// See EmptyContent.java.
jsonHttpContent = new EmptyContent();
}
HttpContent httpContent = requestFormatter.getHttpContent(request);

// 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://")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the use case for this.

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) {
Expand All @@ -196,20 +182,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.
Expand All @@ -235,7 +221,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,24 @@

import static org.mockito.Mockito.mock;

import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.EmptyContent;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.gax.tracing.ApiTracer;
import com.google.api.pathtemplate.PathTemplate;
import com.google.common.truth.Truth;
import com.google.longrunning.ListOperationsRequest;
import com.google.protobuf.ByteString;
import com.google.protobuf.Empty;
import com.google.protobuf.Field;
import com.google.protobuf.util.JsonFormat;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -326,4 +331,110 @@ void testUpdateRunnableTimeout_shouldUpdate() throws IOException {
Truth.assertThat(httpRequest.getReadTimeout()).isEqualTo(30000L);
Truth.assertThat(httpRequest.getConnectTimeout()).isEqualTo(30000L);
}

@Test
void testNonJsonHttpContent() throws IOException {
ByteString rawPayload = ByteString.copyFromUtf8("binary \0 raw \1 payload");
HttpRequestFormatter<Field> binaryRequestFormatter =
new HttpRequestFormatter<Field>() {
@Override
public Map<String, List<String>> getQueryParamNames(Field apiMessage) {
return Collections.emptyMap();
}

@Override
public String getRequestBody(Field apiMessage) {
return "";
}

@Override
public HttpContent getHttpContent(Field apiMessage) {
return new ByteArrayContent("application/octet-stream", rawPayload.toByteArray());
}

@Override
public String getPath(Field apiMessage) {
return "/upload";
}

@Override
public PathTemplate getPathTemplate() {
return PathTemplate.create("{+path}");
}
};

ApiMethodDescriptor<Field, Empty> methodDescriptor =
ApiMethodDescriptor.<Field, Empty>newBuilder()
.setFullMethodName("upload.binary")
.setHttpMethod("POST")
.setRequestFormatter(binaryRequestFormatter)
.setResponseParser(responseParser)
.build();

HttpRequestRunnable<Field, Empty> httpRequestRunnable =
new HttpRequestRunnable<>(
requestMessage,
methodDescriptor,
ENDPOINT,
HttpJsonCallOptions.newBuilder().build(),
new MockHttpTransport(),
HttpJsonMetadata.newBuilder().build(),
result -> {});

HttpRequest httpRequest = httpRequestRunnable.createHttpRequest();
Truth.assertThat(httpRequest.getContent()).isInstanceOf(ByteArrayContent.class);
Truth.assertThat(httpRequest.getContent().getType()).isEqualTo("application/octet-stream");
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
httpRequest.getContent().writeTo(out);
Truth.assertThat(out.toByteArray()).isEqualTo(rawPayload.toByteArray());
}
}

@Test
void testAbsoluteUrlSupport() throws IOException {
String absoluteUrl = "https://custom-upload-host.googleapis.com/upload/session/123?sid=abc";
HttpRequestFormatter<Field> absoluteUrlFormatter =
new HttpRequestFormatter<Field>() {
@Override
public Map<String, List<String>> getQueryParamNames(Field apiMessage) {
return Collections.emptyMap();
}

@Override
public String getRequestBody(Field apiMessage) {
return "";
}

@Override
public String getPath(Field apiMessage) {
return absoluteUrl;
}

@Override
public PathTemplate getPathTemplate() {
return PathTemplate.create("{+path}");
}
};

ApiMethodDescriptor<Field, Empty> methodDescriptor =
ApiMethodDescriptor.<Field, Empty>newBuilder()
.setFullMethodName("upload.absolute")
.setHttpMethod("POST")
.setRequestFormatter(absoluteUrlFormatter)
.setResponseParser(responseParser)
.build();

HttpRequestRunnable<Field, Empty> httpRequestRunnable =
new HttpRequestRunnable<>(
requestMessage,
methodDescriptor,
ENDPOINT,
HttpJsonCallOptions.newBuilder().build(),
new MockHttpTransport(),
HttpJsonMetadata.newBuilder().build(),
result -> {});

HttpRequest httpRequest = httpRequestRunnable.createHttpRequest();
Truth.assertThat(httpRequest.getUrl().build()).isEqualTo(absoluteUrl);
}
}
Loading