Skip to content
Draft
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,13 +29,17 @@
*/
package com.google.api.gax.httpjson;

import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_COMMAND_HEADER;
import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_OFFSET_HEADER;

import com.google.api.client.http.HttpMethods;
import com.google.api.core.ApiFuture;
import com.google.api.gax.resumable.ChunkUploadRequest;
import com.google.api.gax.resumable.ChunkUploadResponse;
import com.google.api.gax.resumable.ResumableUploadStatus;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ClientContext;
import com.google.api.gax.rpc.EndpointContext;
import com.google.api.gax.rpc.UnaryCallable;
import com.google.api.pathtemplate.PathTemplate;
import com.google.common.base.Preconditions;
Expand All @@ -55,10 +59,6 @@
class ResumableUploadChunkCallable<ResponseT>
extends UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>> {

private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
private static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset";
private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";

private static final String COMMAND_UPLOAD = "upload";
private static final String COMMAND_FINALIZE = "finalize";
private static final String COMMAND_UPLOAD_FINALIZE = "upload, finalize";
Expand Down Expand Up @@ -129,6 +129,9 @@
HttpJsonCallContext.createDefault()
.nullToSelf(clientContext.getDefaultCallContext())
.merge(inputContext)
// Session URLs are self-authenticating; omit credentials and reset EndpointContext.
.withCredentials(null)

Check warning on line 133 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadChunkCallable.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Annotate the parameter with @javax.annotation.Nullable in method 'withCredentials' declaration, or make sure that null can not be passed as argument.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-cloud-java_showcase&issues=AaGPLjBcVFGSMShBxcyL&open=AaGPLjBcVFGSMShBxcyL&pullRequest=14870
.withEndpointContext(EndpointContext.getDefaultInstance())
.withExtraHeaders(chunkHeaders);

HttpJsonClientCall<ChunkUploadRequest, String> clientCall =
Expand Down Expand Up @@ -175,10 +178,7 @@

@Override
public void onHeaders(HttpJsonMetadata responseHeaders) {
Map<String, Object> headers = responseHeaders.getHeaders();
this.uploadStatus =
ResumableUploadStatus.fromHeader(
HttpHeadersUtils.getSingleHeader(headers, UPLOAD_STATUS_HEADER));
this.uploadStatus = ResumableUploadHeaders.parseUploadStatus(responseHeaders);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.gax.resumable.ResumableUploadSession;
import com.google.api.gax.resumable.ResumableUploadStatus;
import com.google.api.gax.rpc.ApiExceptionFactory;
import com.google.api.gax.rpc.StatusCode;
import com.google.common.base.Strings;
import org.jspecify.annotations.NullMarked;

/** Shared header constants and parsing utilities for HTTP/JSON resumable upload wire calls. */
@NullMarked
final class ResumableUploadHeaders {

static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol";
static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL";
static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity";
static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset";
static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";
static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received";

private ResumableUploadHeaders() {}

static ResumableUploadStatus parseUploadStatus(HttpJsonMetadata responseHeaders) {
return ResumableUploadStatus.fromHeader(
HttpHeadersUtils.getSingleHeader(responseHeaders.getHeaders(), UPLOAD_STATUS_HEADER));
}

static int parseChunkGranularity(HttpJsonMetadata responseHeaders) {
String granularityStr =
HttpHeadersUtils.getSingleHeader(responseHeaders.getHeaders(), UPLOAD_GRANULARITY_HEADER);
if (Strings.isNullOrEmpty(granularityStr)) {
return ResumableUploadSession.DEFAULT_CHUNK_GRANULARITY;
}
try {
int parsed = Integer.parseInt(granularityStr);
if (parsed <= 0) {
throw ApiExceptionFactory.createException(
"Response contained non-positive "
+ UPLOAD_GRANULARITY_HEADER
+ " header: "
+ granularityStr,
/* cause= */ null,
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
/* retryable= */ false);
}
return parsed;
} catch (NumberFormatException e) {
throw ApiExceptionFactory.createException(
"Response contained invalid " + UPLOAD_GRANULARITY_HEADER + " header: " + granularityStr,
e,
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
/* retryable= */ false);
}
}
Comment thread
whowes marked this conversation as resolved.
Resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@
*/
package com.google.api.gax.httpjson;

import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_COMMAND_HEADER;
import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_SIZE_RECEIVED_HEADER;

import com.google.api.client.http.HttpMethods;
import com.google.api.core.ApiFuture;
import com.google.api.gax.resumable.QueryStatusRequest;
import com.google.api.gax.resumable.QueryStatusResponse;
import com.google.api.gax.resumable.ResumableUploadSession;
import com.google.api.gax.resumable.ResumableUploadStatus;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ApiExceptionFactory;
import com.google.api.gax.rpc.ClientContext;
import com.google.api.gax.rpc.EndpointContext;
import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.rpc.UnaryCallable;
import com.google.api.pathtemplate.PathTemplate;
Expand All @@ -61,9 +66,6 @@
class ResumableUploadQueryStatusCallable<ResponseT>
extends UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>> {

private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";
private static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received";
private static final String COMMAND_QUERY = "query";

private static final Map<String, List<String>> QUERY_STATUS_HEADERS =
Expand Down Expand Up @@ -119,6 +121,9 @@
HttpJsonCallContext.createDefault()
.nullToSelf(clientContext.getDefaultCallContext())
.merge(inputContext)
// Session URLs are self-authenticating; omit credentials and reset EndpointContext.
.withCredentials(null)

Check warning on line 125 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadQueryStatusCallable.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Annotate the parameter with @javax.annotation.Nullable in method 'withCredentials' declaration, or make sure that null can not be passed as argument.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-cloud-java_showcase&issues=AaGPLjE_VFGSMShBxcyM&open=AaGPLjE_VFGSMShBxcyM&pullRequest=14870
.withEndpointContext(EndpointContext.getDefaultInstance())
.withExtraHeaders(QUERY_STATUS_HEADERS);

HttpJsonClientCall<QueryStatusRequest, String> clientCall =
Expand Down Expand Up @@ -183,6 +188,7 @@
private final ResumableUploadHttpJsonFuture<QueryStatusResponse<ResponseT>> future;
private final HttpResponseParser<ResponseT> responseParser;
private ResumableUploadStatus uploadStatus = ResumableUploadStatus.UNKNOWN;
private int chunkGranularity = ResumableUploadSession.DEFAULT_CHUNK_GRANULARITY;
@Nullable private Long committedOffset = null;
@Nullable private Throwable headerParsingException;
private String responseBody = "";
Expand All @@ -196,12 +202,10 @@

@Override
public void onHeaders(HttpJsonMetadata responseHeaders) {
Map<String, Object> headers = responseHeaders.getHeaders();
this.uploadStatus =
ResumableUploadStatus.fromHeader(
HttpHeadersUtils.getSingleHeader(headers, UPLOAD_STATUS_HEADER));
this.uploadStatus = ResumableUploadHeaders.parseUploadStatus(responseHeaders);
try {
this.committedOffset = parseSizeReceived(responseHeaders);
this.chunkGranularity = ResumableUploadHeaders.parseChunkGranularity(responseHeaders);
} catch (Throwable t) {
this.headerParsingException = t;
}
Expand Down Expand Up @@ -229,12 +233,14 @@
QueryStatusResponse.<ResponseT>newBuilder()
.setUploadStatus(uploadStatus)
.setResponse(responseParser.parse(stream))
.setChunkGranularity(chunkGranularity)
.build());
} else if (committedOffset != null) {
future.set(
QueryStatusResponse.<ResponseT>newBuilder()
.setCommittedOffset(committedOffset)
.setUploadStatus(uploadStatus)
.setChunkGranularity(chunkGranularity)
.build());
} else {
future.setException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
*/
package com.google.api.gax.httpjson;

import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_COMMAND_HEADER;
import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_PROTOCOL_HEADER;
import static com.google.api.gax.httpjson.ResumableUploadHeaders.UPLOAD_URL_HEADER;

import com.google.api.core.ApiFuture;
import com.google.api.gax.resumable.ResumableUploadSession;
import com.google.api.gax.rpc.ApiCallContext;
Expand All @@ -52,11 +56,6 @@
class ResumableUploadStartCallable<RequestT>
extends UnaryCallable<RequestT, ResumableUploadSession> {

private static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol";
private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
private static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL";
private static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity";

private static final Map<String, List<String>> START_UPLOAD_HEADERS =
ImmutableMap.of(
UPLOAD_PROTOCOL_HEADER, ImmutableList.of("resumable"),
Expand Down Expand Up @@ -108,7 +107,7 @@
/** A listener that parses HTTP response headers to produce a {@link ResumableUploadSession}. */
private static class StartUploadResponseListener extends HttpJsonClientCall.Listener<String> {
private final ResumableUploadHttpJsonFuture<ResumableUploadSession> future;
private int chunkGranularity = 1;
private int chunkGranularity = ResumableUploadSession.DEFAULT_CHUNK_GRANULARITY;
@Nullable private String uploadUrl;
@Nullable private Throwable headerParsingException;

Expand All @@ -119,39 +118,16 @@

@Override
public void onHeaders(HttpJsonMetadata responseHeaders) {
Map<String, Object> headers = responseHeaders.getHeaders();

String url = HttpHeadersUtils.getSingleHeader(headers, UPLOAD_URL_HEADER);
String url =
HttpHeadersUtils.getSingleHeader(responseHeaders.getHeaders(), UPLOAD_URL_HEADER);
if (!Strings.isNullOrEmpty(url)) {
this.uploadUrl = url;
}

String granularityStr = HttpHeadersUtils.getSingleHeader(headers, UPLOAD_GRANULARITY_HEADER);
if (Strings.isNullOrEmpty(granularityStr)) {
return;
}

try {
int parsed = Integer.parseInt(granularityStr);
if (parsed <= 0) {
this.headerParsingException =
ApiExceptionFactory.createException(
"Start upload response contained non-positive chunk granularity header: "
+ granularityStr,
/* cause= */ null,
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
/* retryable= */ false);
} else {
this.chunkGranularity = parsed;
}
} catch (NumberFormatException e) {
this.headerParsingException =
ApiExceptionFactory.createException(
"Start upload response contained invalid chunk granularity header: "
+ granularityStr,
e,
HttpJsonStatusCode.of(StatusCode.Code.INTERNAL),
/* retryable= */ false);
this.chunkGranularity = ResumableUploadHeaders.parseChunkGranularity(responseHeaders);
} catch (Throwable t) {

Check warning on line 129 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Catch Exception instead of Throwable.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-cloud-java_showcase&issues=AaGPLjHGVFGSMShBxcyN&open=AaGPLjHGVFGSMShBxcyN&pullRequest=14870
this.headerParsingException = t;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.rpc.UnaryCallSettings;
import com.google.api.pathtemplate.PathTemplate;
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2Credentials;
import com.google.common.base.Strings;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -161,7 +164,7 @@ void startUpload_nonPositiveChunkGranularityHeader_throwsException() {
assertThat(exception.getCause()).isInstanceOf(InternalException.class);
assertThat(exception.getCause())
.hasMessageThat()
.contains("Start upload response contained non-positive chunk granularity header: -256");
.contains("Response contained non-positive X-Goog-Upload-Chunk-Granularity header: -256");
}

@Test
Expand Down Expand Up @@ -205,6 +208,7 @@ void startUpload_withPayloadAndQueryParams_sendsCorrectRequest() {
assertThat(transport.capturedHeaders.get("x-goog-upload-protocol"))
.containsExactly("resumable");
assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("start");
assertThat(transport.capturedHeaders.get("authorization")).containsExactly("Bearer test-token");
}

@Test
Expand Down Expand Up @@ -336,6 +340,7 @@ void uploadChunk_intermediateChunk_sendsUploadCommandAndReturnsActiveStatus() {
assertThat(transport.capturedUrl).isEqualTo(TEST_UPLOAD_URL);
assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("upload");
assertThat(transport.capturedHeaders.get("x-goog-upload-offset")).containsExactly("0");
assertThat(transport.capturedHeaders).doesNotContainKey("authorization");
}

@Test
Expand Down Expand Up @@ -512,6 +517,25 @@ void queryStatus_activeUpload_returnsCommittedOffset() {
assertThat(response.getUploadStatus()).isEqualTo(ResumableUploadStatus.ACTIVE);

assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("query");
assertThat(transport.capturedHeaders).doesNotContainKey("authorization");
}

@Test
void queryStatus_withChunkGranularity_returnsGranularity() {
MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse();
httpResponse.setStatusCode(200);
httpResponse.addHeader("X-Goog-Upload-Status", "active");
httpResponse.addHeader("X-Goog-Upload-Size-Received", "524288");
httpResponse.addHeader("X-Goog-Upload-Chunk-Granularity", "262144");

HttpJsonResumableUploadClient<TestRequest, String> client = createClient(httpResponse);
QueryStatusRequest request = QueryStatusRequest.create(TEST_UPLOAD_URL);

QueryStatusResponse<String> response = client.queryStatusCallable().call(request);

assertThat(response.getCommittedOffset()).isEqualTo(524288L);
assertThat(response.getChunkGranularity()).isEqualTo(262144);
assertThat(response.getUploadStatus()).isEqualTo(ResumableUploadStatus.ACTIVE);
}

@Test
Expand Down Expand Up @@ -622,10 +646,15 @@ private static HttpJsonResumableUploadClient<TestRequest, String> createClient(
.setHttpTransport(transport)
.build();

Credentials credentials = OAuth2Credentials.create(new AccessToken("test-token", null));
HttpJsonCallContext defaultCallContext =
HttpJsonCallContext.createDefault().withChannel(channel).withCredentials(credentials);

ClientContext clientContext =
ClientContext.newBuilder()
.setTransportChannel(HttpJsonTransportChannel.create(channel))
.setDefaultCallContext(HttpJsonCallContext.createDefault().withChannel(channel))
.setDefaultCallContext(defaultCallContext)
.setCredentials(credentials)
.build();

return HttpJsonResumableUploadClient.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,18 @@ public abstract class QueryStatusResponse<ResponseT> {
/** Returns the status of the upload session returned by the server. */
public abstract ResumableUploadStatus getUploadStatus();

/**
* Returns the byte alignment granularity required by the server for chunk uploads, or {@code 1}
* if no alignment restriction was returned.
*/
public abstract int getChunkGranularity();

public abstract Builder<ResponseT> toBuilder();

public static <ResponseT> Builder<ResponseT> newBuilder() {
return new AutoValue_QueryStatusResponse.Builder<ResponseT>()
.setUploadStatus(ResumableUploadStatus.ACTIVE);
.setUploadStatus(ResumableUploadStatus.ACTIVE)
.setChunkGranularity(ResumableUploadSession.DEFAULT_CHUNK_GRANULARITY);
}

@AutoValue.Builder
Expand All @@ -80,6 +87,8 @@ public abstract static class Builder<ResponseT> {

public abstract Builder<ResponseT> setUploadStatus(ResumableUploadStatus uploadStatus);

public abstract Builder<ResponseT> setChunkGranularity(int chunkGranularity);

public abstract QueryStatusResponse<ResponseT> build();
}
}
Loading
Loading