Skip to content

Commit 5ecea13

Browse files
committed
feat(gax): detect and defer failure on unsupported resumable upload transport configs
1 parent eca60e7 commit 5ecea13

3 files changed

Lines changed: 227 additions & 0 deletions

File tree

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
package com.google.api.gax.httpjson;
3131

3232
import com.google.api.core.BetaApi;
33+
import com.google.api.core.InternalApi;
3334
import com.google.api.core.ObsoleteApi;
3435
import com.google.api.gax.longrunning.OperationSnapshot;
3536
import com.google.api.gax.resumable.ResumableUploadClient;
@@ -44,6 +45,7 @@
4445
import com.google.api.gax.rpc.ResumableUploadCallable;
4546
import com.google.api.gax.rpc.ServerStreamingCallSettings;
4647
import com.google.api.gax.rpc.ServerStreamingCallable;
48+
import com.google.api.gax.rpc.StubSettings;
4749
import com.google.api.gax.rpc.UnaryCallSettings;
4850
import com.google.api.gax.rpc.UnaryCallable;
4951
import com.google.api.gax.tracing.ApiTracerContext;
@@ -245,6 +247,30 @@ ResumableUploadCallable<RequestT, ResponseT> createResumableUploadCallable(
245247
return Callables.resumableUpload(uploadClient, callSettings, clientContext);
246248
}
247249

250+
/**
251+
* Returns {@code true} if the given {@link StubSettings} has a transport configuration that can
252+
* be used or bridged to HTTP/JSON for resumable uploads, or {@code false} if it contains
253+
* non-HTTP/JSON transport customizations that cannot be bridged. Designed for use by generated
254+
* code.
255+
*/
256+
@BetaApi
257+
@InternalApi
258+
public static boolean isResumableUploadSupported(StubSettings<?> settings) {
259+
return ResumableUploadTransportValidator.isSupported(settings);
260+
}
261+
262+
/**
263+
* Creates a {@link ResumableUploadCallable} that synchronously throws an {@link
264+
* IllegalStateException} when invoked because the client's transport configuration is
265+
* incompatible with HTTP/JSON resumable uploads. Designed for use by generated code.
266+
*/
267+
@BetaApi
268+
@InternalApi
269+
public static <RequestT, ResponseT>
270+
ResumableUploadCallable<RequestT, ResponseT> createFailingResumableUploadCallable() {
271+
return ResumableUploadTransportValidator.createFailingCallable();
272+
}
273+
248274
static ApiTracerContext getApiTracerContext(ApiMethodDescriptor<?, ?> methodDescriptor) {
249275
return ApiTracerContext.newBuilder()
250276
.setFullMethodName(methodDescriptor.getFullMethodName())
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.httpjson;
31+
32+
import com.google.api.gax.rpc.ApiCallContext;
33+
import com.google.api.gax.rpc.FixedTransportChannelProvider;
34+
import com.google.api.gax.rpc.InputStreamSupplier;
35+
import com.google.api.gax.rpc.ResumableUploadCallable;
36+
import com.google.api.gax.rpc.ResumableUploadFuture;
37+
import com.google.api.gax.rpc.ResumableUploadOptions;
38+
import com.google.api.gax.rpc.StubSettings;
39+
import com.google.api.gax.rpc.TransportChannelProvider;
40+
import org.jspecify.annotations.NullMarked;
41+
import org.jspecify.annotations.Nullable;
42+
43+
/**
44+
* Helper to detect and surface deferred failures for transport configurations on {@link
45+
* StubSettings} that cannot be bridged to HTTP/JSON for resumable uploads.
46+
*/
47+
@NullMarked
48+
final class ResumableUploadTransportValidator {
49+
50+
private static final String UNSUPPORTED_TRANSPORT_MESSAGE =
51+
"When using the gRPC client, resumable uploads execute over HTTP/JSON and require all"
52+
+ " endpoint, credentials, and header customizations to be configured on the service"
53+
+ " Settings.Builder.";
54+
55+
private ResumableUploadTransportValidator() {}
56+
57+
static boolean isSupported(StubSettings<?> settings) {
58+
TransportChannelProvider provider = settings.getTransportChannelProvider();
59+
if (HttpJsonTransportChannel.getHttpJsonTransportName().equals(provider.getTransportName())) {
60+
return true;
61+
}
62+
// gRPC stubs bridge resumable uploads to HTTP/JSON using only the headers,
63+
// credentials, and endpoint configured on StubSettings. The provider is only compatible if it
64+
// does not wrap a pre-built channel (FixedTransportChannelProvider) and still expects all
65+
// three settings to be supplied by StubSettings.
66+
return !(provider instanceof FixedTransportChannelProvider)
67+
&& provider.needsHeaders()
68+
&& provider.needsCredentials()
69+
&& provider.needsEndpoint();
70+
}
71+
72+
static <RequestT, ResponseT>
73+
ResumableUploadCallable<RequestT, ResponseT> createFailingCallable() {
74+
return new FailingResumableUploadCallable<>();
75+
}
76+
77+
private static final class FailingResumableUploadCallable<RequestT, ResponseT>
78+
extends ResumableUploadCallable<RequestT, ResponseT> {
79+
80+
@Override
81+
public ResumableUploadFuture<ResponseT> futureCall(
82+
RequestT request,
83+
InputStreamSupplier payload,
84+
@Nullable ApiCallContext context,
85+
@Nullable ResumableUploadOptions options) {
86+
throw new IllegalStateException(UNSUPPORTED_TRANSPORT_MESSAGE);
87+
}
88+
89+
@Override
90+
public ResumableUploadFuture<ResponseT> resumeCall(
91+
String sessionUrl, InputStreamSupplier payload, @Nullable ResumableUploadOptions options) {
92+
throw new IllegalStateException(UNSUPPORTED_TRANSPORT_MESSAGE);
93+
}
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.httpjson;
31+
32+
import static com.google.common.truth.Truth.assertThat;
33+
import static org.junit.jupiter.api.Assertions.assertThrows;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
36+
import static org.mockito.Mockito.withSettings;
37+
38+
import com.google.api.gax.rpc.FixedTransportChannelProvider;
39+
import com.google.api.gax.rpc.InputStreamSupplier;
40+
import com.google.api.gax.rpc.ResumableUploadCallable;
41+
import com.google.api.gax.rpc.StubSettings;
42+
import com.google.api.gax.rpc.TransportChannel;
43+
import com.google.api.gax.rpc.TransportChannelProvider;
44+
import com.google.api.gax.rpc.testing.FakeStubSettings;
45+
import java.io.ByteArrayInputStream;
46+
import java.io.IOException;
47+
import org.junit.jupiter.api.Test;
48+
49+
class ResumableUploadTransportValidatorTest {
50+
51+
@Test
52+
void testIsSupported() throws IOException {
53+
TransportChannelProvider httpJsonProvider =
54+
InstantiatingHttpJsonChannelProvider.newBuilder().build();
55+
assertThat(
56+
HttpJsonCallableFactory.isResumableUploadSupported(
57+
settingsWithProvider(httpJsonProvider)))
58+
.isTrue();
59+
assertThat(HttpJsonCallableFactory.isResumableUploadSupported(grpcSettings(true, true, true)))
60+
.isTrue();
61+
62+
assertThat(HttpJsonCallableFactory.isResumableUploadSupported(grpcSettings(false, true, true)))
63+
.isFalse();
64+
assertThat(HttpJsonCallableFactory.isResumableUploadSupported(grpcSettings(true, false, true)))
65+
.isFalse();
66+
assertThat(HttpJsonCallableFactory.isResumableUploadSupported(grpcSettings(true, true, false)))
67+
.isFalse();
68+
69+
TransportChannel grpcChannel =
70+
mock(TransportChannel.class, withSettings().withoutAnnotations());
71+
when(grpcChannel.getTransportName()).thenReturn("grpc");
72+
assertThat(
73+
HttpJsonCallableFactory.isResumableUploadSupported(
74+
settingsWithProvider(FixedTransportChannelProvider.create(grpcChannel))))
75+
.isFalse();
76+
}
77+
78+
@Test
79+
void testCreateFailingCallable() {
80+
ResumableUploadCallable<String, String> callable =
81+
HttpJsonCallableFactory.createFailingResumableUploadCallable();
82+
InputStreamSupplier payload = () -> new ByteArrayInputStream(new byte[0]);
83+
84+
assertThrows(
85+
IllegalStateException.class, () -> callable.futureCall("request", payload, null, null));
86+
assertThrows(
87+
IllegalStateException.class,
88+
() -> callable.resumeCall("https://example.com/session", payload, null));
89+
}
90+
91+
private static StubSettings<?> settingsWithProvider(TransportChannelProvider provider)
92+
throws IOException {
93+
return FakeStubSettings.newBuilder().setTransportChannelProvider(provider).build();
94+
}
95+
96+
private static StubSettings<?> grpcSettings(
97+
boolean needsHeaders, boolean needsCredentials, boolean needsEndpoint) throws IOException {
98+
TransportChannelProvider provider =
99+
mock(TransportChannelProvider.class, withSettings().withoutAnnotations());
100+
when(provider.getTransportName()).thenReturn("grpc");
101+
when(provider.needsHeaders()).thenReturn(needsHeaders);
102+
when(provider.needsCredentials()).thenReturn(needsCredentials);
103+
when(provider.needsEndpoint()).thenReturn(needsEndpoint);
104+
return settingsWithProvider(provider);
105+
}
106+
}

0 commit comments

Comments
 (0)