feat(gax): implement uploadChunk in HttpJsonResumableUploadClient - #14133
feat(gax): implement uploadChunk in HttpJsonResumableUploadClient#14133whowes wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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.
| if ((statusCode >= 200 && statusCode < 300) || statusCode == 308) { | ||
| long confirmedOffset = | ||
| committedOffset >= 0 | ||
| ? committedOffset | ||
| : request.getOffset() + request.getPayload().size(); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
- 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()) |
There was a problem hiding this comment.
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.
| (request.getPayload() != null && !request.getPayload().isEmpty()) | |
| (!request.getPayload().isEmpty()) |
References
- Avoid adding defensive null checks for values that are guaranteed to be non-null by design, as this can hide invariant breaks.
| HttpJsonCallContext context = | ||
| (HttpJsonCallContext) | ||
| HttpJsonCallContext.createDefault() | ||
| .nullToSelf(clientContext.getDefaultCallContext()) | ||
| .merge(inputContext) | ||
| .withExtraHeaders(chunkHeaders); |
There was a problem hiding this comment.
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);7003d8a to
f8a42a5
Compare
dfe0617 to
3655e2e
Compare
f8a42a5 to
c4d36b5
Compare
c4d36b5 to
faf99d0
Compare
|
|





Stack created with GitHub Stacks CLI • Give Feedback 💬