Skip to content

feat(gax): implement uploadChunk in HttpJsonResumableUploadClient - #14133

Closed
whowes wants to merge 1 commit into
whowes/resumable-upload-startfrom
whowes/resumable-upload-chunk
Closed

feat(gax): implement uploadChunk in HttpJsonResumableUploadClient#14133
whowes wants to merge 1 commit into
whowes/resumable-upload-startfrom
whowes/resumable-upload-chunk

Conversation

@whowes

@whowes whowes commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Stack created with GitHub Stacks CLIGive Feedback 💬

@whowes whowes changed the title whowes/resumable upload chunk feat(gax): implement uploadChunk in HttpJsonResumableUploadClient Aug 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for chunked resumable uploads in the HTTP/JSON transport layer of gax-java. It adds the ChunkUploadRequest and ChunkUploadResponse value objects, updates the ResumableUploadClient interface and its implementation HttpJsonResumableUploadClient to support transmitting individual chunks, and renames StartUploadRequest.builder() to newBuilder() for consistency. Comprehensive unit tests have also been added. The review feedback suggests several improvements: explicitly failing the upload if a 308 Resume Incomplete status is returned without a valid committed offset header to prevent data corruption, removing redundant null checks on non-nullable payload fields, and cleaning up an unnecessary type cast to HttpJsonCallContext.

Comment on lines +341 to +345
if ((statusCode >= 200 && statusCode < 300) || statusCode == 308) {
long confirmedOffset =
committedOffset >= 0
? committedOffset
: request.getOffset() + request.getPayload().size();

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.

high

If the server returns 308 Resume Incomplete but the X-Goog-Upload-Size-Received header is missing or malformed (causing committedOffset to remain -1), falling back to assuming the entire chunk was committed (request.getOffset() + request.getPayload().size()) can lead to silent data corruption or upload gaps. We should explicitly fail the upload if statusCode == 308 and committedOffset < 0.

      if ((statusCode >= 200 && statusCode < 300) || statusCode == 308) {
        if (statusCode == 308 && committedOffset < 0) {
          future.setException(
              ApiExceptionFactory.createException(
                  "Server returned 308 Resume Incomplete but the "
                      + UPLOAD_SIZE_RECEIVED_HEADER
                      + " header was missing or invalid.",
                  /* cause= */ null,
                  HttpJsonStatusCode.of(statusCode),
                  /* retryable= */ false));
          return;
        }
        long confirmedOffset =
            committedOffset >= 0
                ? committedOffset
                : request.getOffset() + request.getPayload().size();

@Override
public HttpContent getHttpContent(ChunkUploadRequest request) {
if (request.getPayload() != null && !request.getPayload().isEmpty()) {
return new ByteArrayContent(

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.

medium

The null check request.getPayload() != null is redundant because ChunkUploadRequest is annotated with @NullMarked and getPayload() returns a non-nullable ByteString. We can safely simplify this to just check if the payload is empty.

                  if (!request.getPayload().isEmpty()) {
References
  1. Avoid adding defensive null checks for values that are guaranteed to be non-null by design, as this can hide invariant breaks.

String command;
if (request.isFinal()) {
command =
(request.getPayload() != null && !request.getPayload().isEmpty())

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.

medium

The null check request.getPayload() != null is redundant because ChunkUploadRequest is annotated with @NullMarked and getPayload() returns a non-nullable ByteString. We can safely simplify this to just check if the payload is empty.

Suggested change
(request.getPayload() != null && !request.getPayload().isEmpty())
(!request.getPayload().isEmpty())
References
  1. Avoid adding defensive null checks for values that are guaranteed to be non-null by design, as this can hide invariant breaks.

Comment on lines +215 to +220
HttpJsonCallContext context =
(HttpJsonCallContext)
HttpJsonCallContext.createDefault()
.nullToSelf(clientContext.getDefaultCallContext())
.merge(inputContext)
.withExtraHeaders(chunkHeaders);

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.

medium

The cast to (HttpJsonCallContext) is redundant here. Since HttpJsonCallContext.createDefault().nullToSelf(...).merge(...) already returns HttpJsonCallContext (via covariant return types), and withExtraHeaders is called on it, the resulting expression is already of type HttpJsonCallContext.

        HttpJsonCallContext context =
            HttpJsonCallContext.createDefault()
                .nullToSelf(clientContext.getDefaultCallContext())
                .merge(inputContext)
                .withExtraHeaders(chunkHeaders);

@whowes
whowes force-pushed the whowes/resumable-upload-chunk branch from 7003d8a to f8a42a5 Compare August 19, 2026 21:57
@whowes
whowes force-pushed the whowes/resumable-upload-start branch from dfe0617 to 3655e2e Compare August 19, 2026 21:57
@whowes
whowes force-pushed the whowes/resumable-upload-chunk branch from f8a42a5 to c4d36b5 Compare August 19, 2026 22:10
@whowes
whowes force-pushed the whowes/resumable-upload-chunk branch from c4d36b5 to faf99d0 Compare August 19, 2026 22:43
@whowes whowes closed this Aug 19, 2026
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant