Skip to content

Commit 0554402

Browse files
committed
feat(gax): allow non-JSON HttpContent and absolute request URLs in HttpRequestRunnable
1 parent 1eb6aa3 commit 0554402

3 files changed

Lines changed: 140 additions & 27 deletions

File tree

sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestFormatter.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
*/
3030
package com.google.api.gax.httpjson;
3131

32+
import com.google.api.client.http.ByteArrayContent;
33+
import com.google.api.client.http.EmptyContent;
34+
import com.google.api.client.http.HttpContent;
3235
import com.google.api.pathtemplate.PathTemplate;
36+
import com.google.common.base.Strings;
3337
import java.util.Collections;
3438
import java.util.List;
3539
import java.util.Map;
@@ -56,4 +60,16 @@ public interface HttpRequestFormatter<MessageFormatT> {
5660
default List<PathTemplate> getAdditionalPathTemplates() {
5761
return Collections.emptyList();
5862
}
63+
64+
/**
65+
* Return {@link HttpContent} representing the request body. Defaults to converting {@link
66+
* #getRequestBody(Object)} to JSON, or {@link EmptyContent} if the body is empty.
67+
*/
68+
default HttpContent getHttpContent(MessageFormatT apiMessage) {
69+
String requestBody = getRequestBody(apiMessage);
70+
if (!Strings.isNullOrEmpty(requestBody)) {
71+
return ByteArrayContent.fromString("application/json; charset=utf-8", requestBody);
72+
}
73+
return new EmptyContent();
74+
}
5975
}

sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,20 @@
2929
*/
3030
package com.google.api.gax.httpjson;
3131

32-
import com.google.api.client.http.EmptyContent;
3332
import com.google.api.client.http.GenericUrl;
3433
import com.google.api.client.http.HttpContent;
35-
import com.google.api.client.http.HttpMediaType;
3634
import com.google.api.client.http.HttpMethods;
3735
import com.google.api.client.http.HttpRequest;
3836
import com.google.api.client.http.HttpRequestFactory;
3937
import com.google.api.client.http.HttpResponse;
4038
import com.google.api.client.http.HttpResponseException;
4139
import com.google.api.client.http.HttpTransport;
42-
import com.google.api.client.http.json.JsonHttpContent;
43-
import com.google.api.client.json.JsonFactory;
4440
import com.google.api.client.json.JsonObjectParser;
4541
import com.google.api.client.json.gson.GsonFactory;
46-
import com.google.api.client.util.GenericData;
4742
import com.google.api.gax.tracing.ApiTracer;
4843
import com.google.auth.Credentials;
4944
import com.google.auth.http.HttpCredentialsAdapter;
5045
import com.google.auto.value.AutoValue;
51-
import com.google.common.base.Strings;
5246
import java.io.ByteArrayInputStream;
5347
import java.io.IOException;
5448
import java.io.InputStream;
@@ -154,8 +148,6 @@ public void run() {
154148
}
155149

156150
HttpRequest createHttpRequest() throws IOException {
157-
GenericData tokenRequest = new GenericData();
158-
159151
HttpRequestFormatter<RequestT> requestFormatter = methodDescriptor.getRequestFormatter();
160152

161153
HttpRequestFactory requestFactory;
@@ -166,24 +158,18 @@ HttpRequest createHttpRequest() throws IOException {
166158
requestFactory = httpTransport.createRequestFactory();
167159
}
168160

169-
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
170161
// Create HTTP request body.
171-
String requestBody = requestFormatter.getRequestBody(request);
172-
HttpContent jsonHttpContent;
173-
if (!Strings.isNullOrEmpty(requestBody)) {
174-
jsonFactory.createJsonParser(requestBody).parse(tokenRequest);
175-
jsonHttpContent =
176-
new JsonHttpContent(jsonFactory, tokenRequest)
177-
.setMediaType((new HttpMediaType("application/json; charset=utf-8")));
178-
} else {
179-
// Force underlying HTTP lib to set Content-Length header to avoid 411s.
180-
// See EmptyContent.java.
181-
jsonHttpContent = new EmptyContent();
182-
}
162+
HttpContent httpContent = requestFormatter.getHttpContent(request);
183163

184164
// Populate URL path and query parameters.
185-
String normalizedEndpoint = normalizeEndpoint(endpoint);
186-
GenericUrl url = new GenericUrl(normalizedEndpoint + requestFormatter.getPath(request));
165+
String path = requestFormatter.getPath(request);
166+
GenericUrl url;
167+
if (path.startsWith("http://") || path.startsWith("https://")) {
168+
url = new GenericUrl(path);
169+
} else {
170+
String normalizedEndpoint = normalizeEndpoint(endpoint);
171+
url = new GenericUrl(normalizedEndpoint + path);
172+
}
187173
Map<String, List<String>> queryParams = requestFormatter.getQueryParamNames(request);
188174
for (Entry<String, List<String>> queryParam : queryParams.entrySet()) {
189175
if (queryParam.getValue() != null) {
@@ -196,20 +182,20 @@ HttpRequest createHttpRequest() throws IOException {
196182
tracer.requestUrlResolved(url.build());
197183
}
198184

199-
HttpRequest httpRequest = buildRequest(requestFactory, url, jsonHttpContent);
185+
HttpRequest httpRequest = buildRequest(requestFactory, url, httpContent);
200186

201187
for (Map.Entry<String, Object> entry : headers.getHeaders().entrySet()) {
202188
HttpHeadersUtils.setHeader(
203189
httpRequest.getHeaders(), entry.getKey(), (String) entry.getValue());
204190
}
205191

206-
httpRequest.setParser(new JsonObjectParser(jsonFactory));
192+
httpRequest.setParser(new JsonObjectParser(GsonFactory.getDefaultInstance()));
207193

208194
return httpRequest;
209195
}
210196

211197
private HttpRequest buildRequest(
212-
HttpRequestFactory requestFactory, GenericUrl url, HttpContent jsonHttpContent)
198+
HttpRequestFactory requestFactory, GenericUrl url, HttpContent httpContent)
213199
throws IOException {
214200
// A workaround to support PATCH request. This assumes support of "X-HTTP-Method-Override"
215201
// header on the server side, which GCP services usually do.
@@ -235,7 +221,7 @@ private HttpRequest buildRequest(
235221
if (HttpMethods.PATCH.equals(actualHttpMethod)) {
236222
actualHttpMethod = HttpMethods.POST;
237223
}
238-
HttpRequest httpRequest = requestFactory.buildRequest(actualHttpMethod, url, jsonHttpContent);
224+
HttpRequest httpRequest = requestFactory.buildRequest(actualHttpMethod, url, httpContent);
239225
if (originalHttpMethod != null && !originalHttpMethod.equals(actualHttpMethod)) {
240226
HttpHeadersUtils.setHeader(
241227
httpRequest.getHeaders(), "X-HTTP-Method-Override", originalHttpMethod);

sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpRequestRunnableTest.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,24 @@
3131

3232
import static org.mockito.Mockito.mock;
3333

34+
import com.google.api.client.http.ByteArrayContent;
3435
import com.google.api.client.http.EmptyContent;
36+
import com.google.api.client.http.HttpContent;
3537
import com.google.api.client.http.HttpRequest;
3638
import com.google.api.client.testing.http.MockHttpTransport;
3739
import com.google.api.gax.tracing.ApiTracer;
40+
import com.google.api.pathtemplate.PathTemplate;
3841
import com.google.common.truth.Truth;
3942
import com.google.longrunning.ListOperationsRequest;
43+
import com.google.protobuf.ByteString;
4044
import com.google.protobuf.Empty;
4145
import com.google.protobuf.Field;
4246
import com.google.protobuf.util.JsonFormat;
4347
import java.io.ByteArrayOutputStream;
4448
import java.io.IOException;
4549
import java.nio.charset.StandardCharsets;
4650
import java.util.Arrays;
51+
import java.util.Collections;
4752
import java.util.HashMap;
4853
import java.util.LinkedHashMap;
4954
import java.util.List;
@@ -326,4 +331,110 @@ void testUpdateRunnableTimeout_shouldUpdate() throws IOException {
326331
Truth.assertThat(httpRequest.getReadTimeout()).isEqualTo(30000L);
327332
Truth.assertThat(httpRequest.getConnectTimeout()).isEqualTo(30000L);
328333
}
334+
335+
@Test
336+
void testNonJsonHttpContent() throws IOException {
337+
ByteString rawPayload = ByteString.copyFromUtf8("binary \0 raw \1 payload");
338+
HttpRequestFormatter<Field> binaryRequestFormatter =
339+
new HttpRequestFormatter<Field>() {
340+
@Override
341+
public Map<String, List<String>> getQueryParamNames(Field apiMessage) {
342+
return Collections.emptyMap();
343+
}
344+
345+
@Override
346+
public String getRequestBody(Field apiMessage) {
347+
return "";
348+
}
349+
350+
@Override
351+
public HttpContent getHttpContent(Field apiMessage) {
352+
return new ByteArrayContent("application/octet-stream", rawPayload.toByteArray());
353+
}
354+
355+
@Override
356+
public String getPath(Field apiMessage) {
357+
return "/upload";
358+
}
359+
360+
@Override
361+
public PathTemplate getPathTemplate() {
362+
return PathTemplate.create("{+path}");
363+
}
364+
};
365+
366+
ApiMethodDescriptor<Field, Empty> methodDescriptor =
367+
ApiMethodDescriptor.<Field, Empty>newBuilder()
368+
.setFullMethodName("upload.binary")
369+
.setHttpMethod("POST")
370+
.setRequestFormatter(binaryRequestFormatter)
371+
.setResponseParser(responseParser)
372+
.build();
373+
374+
HttpRequestRunnable<Field, Empty> httpRequestRunnable =
375+
new HttpRequestRunnable<>(
376+
requestMessage,
377+
methodDescriptor,
378+
ENDPOINT,
379+
HttpJsonCallOptions.newBuilder().build(),
380+
new MockHttpTransport(),
381+
HttpJsonMetadata.newBuilder().build(),
382+
result -> {});
383+
384+
HttpRequest httpRequest = httpRequestRunnable.createHttpRequest();
385+
Truth.assertThat(httpRequest.getContent()).isInstanceOf(ByteArrayContent.class);
386+
Truth.assertThat(httpRequest.getContent().getType()).isEqualTo("application/octet-stream");
387+
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
388+
httpRequest.getContent().writeTo(out);
389+
Truth.assertThat(out.toByteArray()).isEqualTo(rawPayload.toByteArray());
390+
}
391+
}
392+
393+
@Test
394+
void testAbsoluteUrlSupport() throws IOException {
395+
String absoluteUrl = "https://custom-upload-host.googleapis.com/upload/session/123?sid=abc";
396+
HttpRequestFormatter<Field> absoluteUrlFormatter =
397+
new HttpRequestFormatter<Field>() {
398+
@Override
399+
public Map<String, List<String>> getQueryParamNames(Field apiMessage) {
400+
return Collections.emptyMap();
401+
}
402+
403+
@Override
404+
public String getRequestBody(Field apiMessage) {
405+
return "";
406+
}
407+
408+
@Override
409+
public String getPath(Field apiMessage) {
410+
return absoluteUrl;
411+
}
412+
413+
@Override
414+
public PathTemplate getPathTemplate() {
415+
return PathTemplate.create("{+path}");
416+
}
417+
};
418+
419+
ApiMethodDescriptor<Field, Empty> methodDescriptor =
420+
ApiMethodDescriptor.<Field, Empty>newBuilder()
421+
.setFullMethodName("upload.absolute")
422+
.setHttpMethod("POST")
423+
.setRequestFormatter(absoluteUrlFormatter)
424+
.setResponseParser(responseParser)
425+
.build();
426+
427+
HttpRequestRunnable<Field, Empty> httpRequestRunnable =
428+
new HttpRequestRunnable<>(
429+
requestMessage,
430+
methodDescriptor,
431+
ENDPOINT,
432+
HttpJsonCallOptions.newBuilder().build(),
433+
new MockHttpTransport(),
434+
HttpJsonMetadata.newBuilder().build(),
435+
result -> {});
436+
437+
HttpRequest httpRequest = httpRequestRunnable.createHttpRequest();
438+
Truth.assertThat(httpRequest.getUrl().build()).isEqualTo(absoluteUrl);
439+
}
329440
}

0 commit comments

Comments
 (0)