From 86a18ddff5cad343a68b5037afe7f7b8018cd865 Mon Sep 17 00:00:00 2001 From: whowes Date: Sat, 12 Sep 2026 05:28:33 +0000 Subject: [PATCH] feat(gax): add upload-status header plumbing Add nullable getUploadStatus() accessors to ChunkUploadResponse, QueryStatusResponse, and ResumableUploadSession, and plumb the X-Goog-Upload-Status response header through the HTTP/JSON callables. On HTTP 200 chunk responses where X-Goog-Upload-Status is absent, return a ChunkUploadResponse with a null uploadStatus rather than throwing a wire-level exception, allowing higher-level upload coordinators to classify the missing header and trigger protocol recovery. Existing test uploadChunk_missingUploadStatusHeader_throwsInternalException was updated to uploadChunk_missingUploadStatusHeader_returnsNullUploadStatusOnHttp200 to reflect that the missing status header on HTTP 200 is now surfaced via a null status property on ChunkUploadResponse instead of throwing an InternalException at the transport layer. Note: No end-to-end integration test is included because the test server always returns the X-Goog-Upload-Status header on success, making header absence uninjectable end-to-end. --- .../api/gax/httpjson/HttpJsonStatusCode.java | 4 + .../ResumableUploadChunkCallable.java | 42 ++++++--- .../ResumableUploadQueryStatusCallable.java | 32 ++++++- .../ResumableUploadStartCallable.java | 5 ++ .../HttpJsonResumableUploadClientTest.java | 88 ++++++++++++++++--- .../gax/resumable/ChunkUploadResponse.java | 14 +++ .../gax/resumable/QueryStatusResponse.java | 8 ++ .../gax/resumable/ResumableUploadSession.java | 9 ++ .../resumable/ResumableUploadSessionTest.java | 75 ++++++++++++++++ 9 files changed, 249 insertions(+), 28 deletions(-) diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonStatusCode.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonStatusCode.java index 45628fd90f3f..b3e7c6a1235e 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonStatusCode.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonStatusCode.java @@ -51,6 +51,10 @@ public static HttpJsonStatusCode of(Code statusCode) { return new HttpJsonStatusCode(statusCode.getHttpStatusCode(), statusCode); } + static HttpJsonStatusCode of(int httpStatus, Code statusCode) { + return new HttpJsonStatusCode(httpStatus, statusCode); + } + public static HttpJsonStatusCode of(com.google.rpc.Code rpcCode) { return HttpJsonStatusCode.of(rpcCodeToStatusCode(rpcCode)); } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadChunkCallable.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadChunkCallable.java index af0f0a4d11f6..70a9e27f20e7 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadChunkCallable.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadChunkCallable.java @@ -60,6 +60,7 @@ class ResumableUploadChunkCallable 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 STATUS_FINAL = "final"; + private static final String STATUS_CANCELLED = "cancelled"; private static final String COMMAND_UPLOAD = "upload"; private static final String COMMAND_FINALIZE = "finalize"; @@ -191,27 +192,42 @@ public void onMessage(@Nullable String message) { @Override public void onClose(int statusCode, HttpJsonMetadata trailers) { try { - if (statusCode >= 200 && statusCode < 300) { - if (uploadStatus == null) { - future.setException( - ApiExceptionFactory.createException( - "Upload chunk response did not contain valid " - + UPLOAD_STATUS_HEADER - + " header", - /* cause= */ null, - HttpJsonStatusCode.of(StatusCode.Code.INTERNAL), - /* retryable= */ false)); - return; - } + if (STATUS_CANCELLED.equalsIgnoreCase(uploadStatus)) { + Throwable cause = trailers.getException(); + String message = + cause != null && cause.getMessage() != null + ? cause.getMessage() + : "Upload cancelled by server with status code: " + statusCode; + future.setException( + ApiExceptionFactory.createException( + message, + cause, + HttpJsonStatusCode.of(statusCode, StatusCode.Code.FAILED_PRECONDITION), + false)); + } else if (statusCode >= 200 && statusCode < 300) { boolean isComplete = STATUS_FINAL.equalsIgnoreCase(uploadStatus); ChunkUploadResponse.Builder chunkResponseBuilder = - ChunkUploadResponse.newBuilder().setComplete(isComplete); + ChunkUploadResponse.newBuilder() + .setComplete(isComplete) + .setUploadStatus(uploadStatus); if (isComplete) { InputStream stream = new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); chunkResponseBuilder.setResponse(responseParser.parse(stream)); } future.set(chunkResponseBuilder.build()); + } else if (STATUS_FINAL.equalsIgnoreCase(uploadStatus)) { + Throwable cause = trailers.getException(); + String message = + cause != null && cause.getMessage() != null + ? cause.getMessage() + : "Upload rejected by server with status code: " + statusCode; + future.setException( + ApiExceptionFactory.createException( + message, + cause, + HttpJsonStatusCode.of(statusCode, StatusCode.Code.FAILED_PRECONDITION), + false)); } else { Throwable cause = trailers.getException(); future.setException( diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadQueryStatusCallable.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadQueryStatusCallable.java index 8176f62bf711..e4918a07b9a9 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadQueryStatusCallable.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadQueryStatusCallable.java @@ -64,6 +64,7 @@ class ResumableUploadQueryStatusCallable 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 STATUS_FINAL = "final"; + private static final String STATUS_CANCELLED = "cancelled"; private static final String COMMAND_QUERY = "query"; private static final Map> QUERY_STATUS_HEADERS = @@ -215,7 +216,19 @@ public void onMessage(@Nullable String message) { @Override public void onClose(int statusCode, HttpJsonMetadata trailers) { try { - if (statusCode >= 200 && statusCode < 300) { + if (STATUS_CANCELLED.equalsIgnoreCase(uploadStatus)) { + Throwable cause = trailers.getException(); + String message = + cause != null && cause.getMessage() != null + ? cause.getMessage() + : "Upload cancelled by server with status code: " + statusCode; + future.setException( + ApiExceptionFactory.createException( + message, + cause, + HttpJsonStatusCode.of(statusCode, StatusCode.Code.FAILED_PRECONDITION), + false)); + } else if (statusCode >= 200 && statusCode < 300) { if (headerParsingException != null) { future.setException(headerParsingException); return; @@ -223,7 +236,9 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) { boolean isComplete = STATUS_FINAL.equalsIgnoreCase(uploadStatus); if (isComplete) { QueryStatusResponse.Builder queryResponseBuilder = - QueryStatusResponse.newBuilder().setComplete(true); + QueryStatusResponse.newBuilder() + .setComplete(true) + .setUploadStatus(uploadStatus); InputStream stream = new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); queryResponseBuilder.setResponse(responseParser.parse(stream)); @@ -233,6 +248,7 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) { QueryStatusResponse.newBuilder() .setComplete(false) .setCommittedOffset(committedOffset) + .setUploadStatus(uploadStatus) .build()); } else { future.setException( @@ -244,6 +260,18 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) { HttpJsonStatusCode.of(StatusCode.Code.INTERNAL), /* retryable= */ false)); } + } else if (STATUS_FINAL.equalsIgnoreCase(uploadStatus)) { + Throwable cause = trailers.getException(); + String message = + cause != null && cause.getMessage() != null + ? cause.getMessage() + : "Upload rejected by server with status code: " + statusCode; + future.setException( + ApiExceptionFactory.createException( + message, + cause, + HttpJsonStatusCode.of(statusCode, StatusCode.Code.FAILED_PRECONDITION), + false)); } else { Throwable cause = trailers.getException(); future.setException( diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java index 3e546c3e14e6..b32d3873d735 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java @@ -55,6 +55,7 @@ class ResumableUploadStartCallable 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 String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status"; private static final Map> START_UPLOAD_HEADERS = ImmutableMap.of( @@ -109,6 +110,7 @@ private static class StartUploadResponseListener extends HttpJsonClientCall.List private final ResumableUploadHttpJsonFuture future; private long chunkGranularity = 1L; @Nullable private String uploadUrl; + @Nullable private String uploadStatus; @Nullable private Throwable headerParsingException; private StartUploadResponseListener( @@ -120,6 +122,8 @@ private StartUploadResponseListener( public void onHeaders(HttpJsonMetadata responseHeaders) { Map headers = responseHeaders.getHeaders(); + this.uploadStatus = HttpHeadersUtils.getSingleHeader(headers, UPLOAD_STATUS_HEADER); + String url = HttpHeadersUtils.getSingleHeader(headers, UPLOAD_URL_HEADER); if (!Strings.isNullOrEmpty(url)) { this.uploadUrl = url; @@ -172,6 +176,7 @@ public void onClose(int statusCode, HttpJsonMetadata trailers) { ResumableUploadSession.newBuilder() .setUploadUrl(uploadUrl) .setChunkGranularity(chunkGranularity) + .setUploadStatus(uploadStatus) .build()); } else { future.setException( diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClientTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClientTest.java index 2736b3d1c3a8..0b4ace81055c 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClientTest.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClientTest.java @@ -47,8 +47,8 @@ import com.google.api.gax.resumable.ResumableUploadSession; import com.google.api.gax.rpc.AbortedException; import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.FailedPreconditionException; import com.google.api.gax.rpc.InternalException; import com.google.api.gax.rpc.NotFoundException; import com.google.api.gax.rpc.StatusCode; @@ -100,6 +100,23 @@ void startUpload_validHeaders_returnsSession() { assertThat(session.getUploadUrl()).isEqualTo(TEST_UPLOAD_URL); assertThat(session.getChunkGranularity()).isEqualTo(262144L); + assertThat(session.getUploadStatus()).isNull(); + } + + @Test + void startUpload_withUploadStatusHeader_returnsSessionWithUploadStatus() { + MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse(); + httpResponse.setStatusCode(200); + httpResponse.addHeader("X-Goog-Upload-URL", TEST_UPLOAD_URL); + httpResponse.addHeader("X-Goog-Upload-Status", "active"); + + HttpJsonResumableUploadClient client = createClient(httpResponse); + TestRequest request = new TestRequest("upload/v1/resources"); + + ResumableUploadSession session = client.startUploadCallable().call(request); + + assertThat(session.getUploadUrl()).isEqualTo(TEST_UPLOAD_URL); + assertThat(session.getUploadStatus()).isEqualTo("active"); } @Test @@ -262,6 +279,7 @@ void uploadChunk_intermediateChunk_sendsUploadCommandAndReturnsActiveStatus() { assertThat(response.isComplete()).isFalse(); assertThat(response.getResponse()).isNull(); + assertThat(response.getUploadStatus()).isEqualTo("active"); assertThat(transport.capturedUrl).isEqualTo(TEST_UPLOAD_URL); assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("upload"); @@ -292,6 +310,7 @@ void uploadChunk_finalChunk_sendsUploadFinalizeAndReturnsResponseBody() { assertThat(response.isComplete()).isTrue(); assertThat(response.getResponse()) .isEqualTo("{\"name\":\"uploaded-file.txt\",\"size\":524288}"); + assertThat(response.getUploadStatus()).isEqualTo("final"); assertThat(transport.capturedHeaders.get("x-goog-upload-command")) .containsExactly("upload, finalize"); @@ -321,6 +340,7 @@ void uploadChunk_emptyPayloadFinal_sendsFinalizeCommandAndReturnsResponseBody() assertThat(response.isComplete()).isTrue(); assertThat(response.getResponse()) .isEqualTo("{\"name\":\"uploaded-file.txt\",\"size\":1048576}"); + assertThat(response.getUploadStatus()).isEqualTo("final"); assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("finalize"); assertThat(transport.capturedHeaders).doesNotContainKey("x-goog-upload-offset"); @@ -379,10 +399,32 @@ void uploadChunk_serverReturnsConflictOrError_throwsException() { } @Test - void uploadChunk_missingUploadStatusHeader_throwsInternalException() { + void uploadChunk_missingUploadStatusHeader_returnsNullUploadStatusOnHttp200() { MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse(); httpResponse.setStatusCode(200); + HttpJsonResumableUploadClient client = createClient(httpResponse); + ChunkUploadRequest request = + ChunkUploadRequest.newBuilder() + .setUploadUrl(TEST_UPLOAD_URL) + .setPayload("data".getBytes(StandardCharsets.UTF_8)) + .setOffset(0L) + .build(); + + ChunkUploadResponse response = client.uploadChunkCallable().call(request); + + assertThat(response.isComplete()).isFalse(); + assertThat(response.getResponse()).isNull(); + assertThat(response.getUploadStatus()).isNull(); + } + + @Test + void uploadChunk_serverReturnsFinalStatusOnNon200_throwsFailedPreconditionException() { + MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse(); + httpResponse.setStatusCode(503); + httpResponse.addHeader("X-Goog-Upload-Status", "final"); + httpResponse.setContent("{\"error\":{\"message\":\"Upload rejected by backend\"}}"); + HttpJsonResumableUploadClient client = createClient(httpResponse); ChunkUploadRequest request = ChunkUploadRequest.newBuilder() @@ -395,18 +437,19 @@ void uploadChunk_missingUploadStatusHeader_throwsInternalException() { assertThrows( ExecutionException.class, () -> client.uploadChunkCallable().futureCall(request).get()); - assertThat(exception.getCause()).isInstanceOf(InternalException.class); - assertThat(exception.getCause()) - .hasMessageThat() - .contains("Upload chunk response did not contain valid X-Goog-Upload-Status header"); + assertThat(exception.getCause()).isInstanceOf(FailedPreconditionException.class); + FailedPreconditionException failedPrecondition = + (FailedPreconditionException) exception.getCause(); + assertThat(failedPrecondition.getStatusCode().getCode()) + .isEqualTo(StatusCode.Code.FAILED_PRECONDITION); } @Test - void uploadChunk_serverReturnsFinalStatusOnNon200_marksExceptionNonRetryable() { + void uploadChunk_serverReturnsCancelledStatus_throwsFailedPreconditionException() { MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse(); httpResponse.setStatusCode(503); - httpResponse.addHeader("X-Goog-Upload-Status", "final"); - httpResponse.setContent("{\"error\":{\"message\":\"Upload rejected by backend\"}}"); + httpResponse.addHeader("X-Goog-Upload-Status", "cancelled"); + httpResponse.setContent("{\"error\":{\"message\":\"Upload cancelled by backend\"}}"); HttpJsonResumableUploadClient client = createClient(httpResponse); ChunkUploadRequest request = @@ -420,10 +463,11 @@ void uploadChunk_serverReturnsFinalStatusOnNon200_marksExceptionNonRetryable() { assertThrows( ExecutionException.class, () -> client.uploadChunkCallable().futureCall(request).get()); - assertThat(exception.getCause()).isInstanceOf(ApiException.class); - ApiException apiException = (ApiException) exception.getCause(); - assertThat(apiException.isRetryable()).isFalse(); - assertThat(apiException.getStatusCode().getCode()).isEqualTo(StatusCode.Code.UNAVAILABLE); + assertThat(exception.getCause()).isInstanceOf(FailedPreconditionException.class); + FailedPreconditionException failedPrecondition = + (FailedPreconditionException) exception.getCause(); + assertThat(failedPrecondition.getStatusCode().getCode()) + .isEqualTo(StatusCode.Code.FAILED_PRECONDITION); } @Test @@ -442,6 +486,7 @@ void queryStatus_activeUpload_returnsCommittedOffset() { assertThat(response.isComplete()).isFalse(); assertThat(response.getCommittedOffset()).isEqualTo(524288L); assertThat(response.getResponse()).isNull(); + assertThat(response.getUploadStatus()).isEqualTo("active"); assertThat(transport.capturedHeaders.get("x-goog-upload-command")).containsExactly("query"); } @@ -462,6 +507,23 @@ void queryStatus_finalUpload_returnsCompleteAndResponseBody() { assertThat(response.getCommittedOffset()).isNull(); assertThat(response.getResponse()) .isEqualTo("{\"name\":\"uploaded-file.txt\",\"size\":1048576}"); + assertThat(response.getUploadStatus()).isEqualTo("final"); + } + + @Test + void queryStatus_missingUploadStatusHeader_returnsNullUploadStatusOnHttp200() { + MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse(); + httpResponse.setStatusCode(200); + httpResponse.addHeader("X-Goog-Upload-Size-Received", "524288"); + + HttpJsonResumableUploadClient client = createClient(httpResponse); + QueryStatusRequest request = QueryStatusRequest.create(TEST_UPLOAD_URL); + + QueryStatusResponse response = client.queryStatusCallable().call(request); + + assertThat(response.isComplete()).isFalse(); + assertThat(response.getCommittedOffset()).isEqualTo(524288L); + assertThat(response.getUploadStatus()).isNull(); } @Test diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ChunkUploadResponse.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ChunkUploadResponse.java index 26066a6b19de..76ba857d5d35 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ChunkUploadResponse.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ChunkUploadResponse.java @@ -55,6 +55,12 @@ public abstract class ChunkUploadResponse { */ public abstract @Nullable ResponseT getResponse(); + /** + * Returns the status of the upload session returned by the server (e.g. {@code "active"} or + * {@code "final"}), or {@code null} if the server omitted the upload status header. + */ + public abstract @Nullable String getUploadStatus(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -63,9 +69,15 @@ public static Builder newBuilder() { public static ChunkUploadResponse create( boolean isComplete, @Nullable ResponseT response) { + return create(isComplete, response, null); + } + + public static ChunkUploadResponse create( + boolean isComplete, @Nullable ResponseT response, @Nullable String uploadStatus) { return new AutoValue_ChunkUploadResponse.Builder() .setComplete(isComplete) .setResponse(response) + .setUploadStatus(uploadStatus) .build(); } @@ -75,6 +87,8 @@ public abstract static class Builder { public abstract Builder setResponse(@Nullable ResponseT response); + public abstract Builder setUploadStatus(@Nullable String uploadStatus); + public abstract ChunkUploadResponse build(); } } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/QueryStatusResponse.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/QueryStatusResponse.java index 1b44f353af3a..42536b7f86b1 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/QueryStatusResponse.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/QueryStatusResponse.java @@ -65,6 +65,12 @@ public abstract class QueryStatusResponse { */ public abstract @Nullable ResponseT getResponse(); + /** + * Returns the status of the upload session returned by the server (e.g. {@code "active"} or + * {@code "final"}), or {@code null} if the server omitted the upload status header. + */ + public abstract @Nullable String getUploadStatus(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -79,6 +85,8 @@ public abstract static class Builder { public abstract Builder setResponse(@Nullable ResponseT response); + public abstract Builder setUploadStatus(@Nullable String uploadStatus); + public abstract QueryStatusResponse build(); } } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ResumableUploadSession.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ResumableUploadSession.java index 345432957aa8..392ba020dc93 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ResumableUploadSession.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ResumableUploadSession.java @@ -33,6 +33,7 @@ import com.google.api.core.InternalApi; import com.google.auto.value.AutoValue; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** Represents the session metadata returned after starting a resumable upload. */ @NullMarked @@ -58,6 +59,12 @@ public abstract class ResumableUploadSession { */ public abstract long getChunkGranularity(); + /** + * Returns the upload status returned by the server (e.g. {@code "active"}), or {@code null} if + * the server omitted the upload status header. + */ + public abstract @Nullable String getUploadStatus(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -71,6 +78,8 @@ public abstract static class Builder { public abstract Builder setChunkGranularity(long chunkGranularity); + public abstract Builder setUploadStatus(@Nullable String uploadStatus); + public abstract ResumableUploadSession build(); } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/resumable/ResumableUploadSessionTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/resumable/ResumableUploadSessionTest.java index 6021503f92b2..196130cde10d 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/resumable/ResumableUploadSessionTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/resumable/ResumableUploadSessionTest.java @@ -59,4 +59,79 @@ void build_withExplicitChunkGranularity_preservesGranularity() { assertThat(session.getUploadUrl()).isEqualTo(UPLOAD_URL); assertThat(session.getChunkGranularity()).isEqualTo(customChunkGranularity); } + + @Test + void build_withUploadStatus_preservesUploadStatus() { + ResumableUploadSession session = + ResumableUploadSession.newBuilder() + .setUploadUrl(UPLOAD_URL) + .setUploadStatus("active") + .build(); + + assertThat(session.getUploadUrl()).isEqualTo(UPLOAD_URL); + assertThat(session.getUploadStatus()).isEqualTo("active"); + } + + @Test + void build_withoutUploadStatus_defaultsToNull() { + ResumableUploadSession session = + ResumableUploadSession.newBuilder().setUploadUrl(UPLOAD_URL).build(); + + assertThat(session.getUploadStatus()).isNull(); + } + + @Test + void chunkUploadResponse_withUploadStatus_preservesStatus() { + ChunkUploadResponse response = + ChunkUploadResponse.newBuilder() + .setComplete(false) + .setUploadStatus("active") + .build(); + + assertThat(response.isComplete()).isFalse(); + assertThat(response.getUploadStatus()).isEqualTo("active"); + } + + @Test + void chunkUploadResponse_withoutUploadStatus_defaultsToNull() { + ChunkUploadResponse response = + ChunkUploadResponse.newBuilder().setComplete(true).build(); + + assertThat(response.isComplete()).isTrue(); + assertThat(response.getUploadStatus()).isNull(); + } + + @Test + void chunkUploadResponse_createWithUploadStatus_preservesStatus() { + ChunkUploadResponse response = ChunkUploadResponse.create(true, "result", "final"); + + assertThat(response.isComplete()).isTrue(); + assertThat(response.getResponse()).isEqualTo("result"); + assertThat(response.getUploadStatus()).isEqualTo("final"); + } + + @Test + void queryStatusResponse_withUploadStatus_preservesStatus() { + QueryStatusResponse response = + QueryStatusResponse.newBuilder() + .setComplete(false) + .setCommittedOffset(1024L) + .setUploadStatus("active") + .build(); + + assertThat(response.isComplete()).isFalse(); + assertThat(response.getCommittedOffset()).isEqualTo(1024L); + assertThat(response.getUploadStatus()).isEqualTo("active"); + } + + @Test + void queryStatusResponse_withoutUploadStatus_defaultsToNull() { + QueryStatusResponse response = + QueryStatusResponse.newBuilder() + .setComplete(false) + .setCommittedOffset(1024L) + .build(); + + assertThat(response.getUploadStatus()).isNull(); + } }