Skip to content

Commit b7097e4

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

3 files changed

Lines changed: 277 additions & 0 deletions

File tree

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

Lines changed: 39 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,43 @@ 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+
274+
/**
275+
* Creates a {@link ResumableUploadCallable} that synchronously throws an {@link
276+
* IllegalStateException} with the given cause when invoked because the HTTP/JSON client context
277+
* required for resumable uploads could not be created. Designed for use by generated code.
278+
*/
279+
@BetaApi
280+
@InternalApi
281+
public static <RequestT, ResponseT>
282+
ResumableUploadCallable<RequestT, ResponseT> createFailingResumableUploadCallable(
283+
Throwable cause) {
284+
return ResumableUploadTransportValidator.createFailingCallable(cause);
285+
}
286+
248287
static ApiTracerContext getApiTracerContext(ApiMethodDescriptor<?, ?> methodDescriptor) {
249288
return ApiTracerContext.newBuilder()
250289
.setFullMethodName(methodDescriptor.getFullMethodName())
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 static final String CLIENT_CONTEXT_FAILURE_MESSAGE =
56+
"Failed to initialize the HTTP/JSON client context required for resumable uploads."
57+
+ " Resumable uploads use only the endpoint, credentials, and headers configured on the"
58+
+ " service Settings; create the client or stub from Settings rather than a"
59+
+ " ClientContext.";
60+
61+
private ResumableUploadTransportValidator() {}
62+
63+
static boolean isSupported(StubSettings<?> settings) {
64+
TransportChannelProvider provider = settings.getTransportChannelProvider();
65+
if (HttpJsonTransportChannel.getHttpJsonTransportName().equals(provider.getTransportName())) {
66+
return true;
67+
}
68+
// gRPC stubs bridge resumable uploads to HTTP/JSON using only the headers,
69+
// credentials, and endpoint configured on StubSettings. The provider is only compatible if it
70+
// does not wrap a pre-built channel (FixedTransportChannelProvider) and still expects all
71+
// three settings to be supplied by StubSettings.
72+
return !(provider instanceof FixedTransportChannelProvider)
73+
&& provider.needsHeaders()
74+
&& provider.needsCredentials()
75+
&& provider.needsEndpoint();
76+
}
77+
78+
static <RequestT, ResponseT>
79+
ResumableUploadCallable<RequestT, ResponseT> createFailingCallable() {
80+
return new FailingResumableUploadCallable<>(UNSUPPORTED_TRANSPORT_MESSAGE, null);
81+
}
82+
83+
static <RequestT, ResponseT> ResumableUploadCallable<RequestT, ResponseT> createFailingCallable(
84+
Throwable cause) {
85+
return new FailingResumableUploadCallable<>(CLIENT_CONTEXT_FAILURE_MESSAGE, cause);
86+
}
87+
88+
private static final class FailingResumableUploadCallable<RequestT, ResponseT>
89+
extends ResumableUploadCallable<RequestT, ResponseT> {
90+
91+
private final String message;
92+
private final @Nullable Throwable cause;
93+
94+
private FailingResumableUploadCallable(String message, @Nullable Throwable cause) {
95+
this.message = message;
96+
this.cause = cause;
97+
}
98+
99+
@Override
100+
public ResumableUploadFuture<ResponseT> futureCall(
101+
RequestT request,
102+
InputStreamSupplier payload,
103+
@Nullable ApiCallContext context,
104+
@Nullable ResumableUploadOptions options) {
105+
throw new IllegalStateException(message, cause);
106+
}
107+
108+
@Override
109+
public ResumableUploadFuture<ResponseT> resumeCall(
110+
String sessionUrl, InputStreamSupplier payload, @Nullable ResumableUploadOptions options) {
111+
throw new IllegalStateException(message, cause);
112+
}
113+
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
@Test
92+
void testCreateFailingCallableWithCause() {
93+
IOException cause = new IOException("no credentials");
94+
ResumableUploadCallable<String, String> callable =
95+
HttpJsonCallableFactory.createFailingResumableUploadCallable(cause);
96+
InputStreamSupplier payload = () -> new ByteArrayInputStream(new byte[0]);
97+
98+
IllegalStateException futureCallException =
99+
assertThrows(
100+
IllegalStateException.class, () -> callable.futureCall("request", payload, null, null));
101+
assertThat(futureCallException).hasCauseThat().isSameInstanceAs(cause);
102+
IllegalStateException resumeCallException =
103+
assertThrows(
104+
IllegalStateException.class,
105+
() -> callable.resumeCall("https://example.com/session", payload, null));
106+
assertThat(resumeCallException).hasCauseThat().isSameInstanceAs(cause);
107+
}
108+
109+
private static StubSettings<?> settingsWithProvider(TransportChannelProvider provider)
110+
throws IOException {
111+
return FakeStubSettings.newBuilder().setTransportChannelProvider(provider).build();
112+
}
113+
114+
private static StubSettings<?> grpcSettings(
115+
boolean needsHeaders, boolean needsCredentials, boolean needsEndpoint) throws IOException {
116+
TransportChannelProvider provider =
117+
mock(TransportChannelProvider.class, withSettings().withoutAnnotations());
118+
when(provider.getTransportName()).thenReturn("grpc");
119+
when(provider.needsHeaders()).thenReturn(needsHeaders);
120+
when(provider.needsCredentials()).thenReturn(needsCredentials);
121+
when(provider.needsEndpoint()).thenReturn(needsEndpoint);
122+
return settingsWithProvider(provider);
123+
}
124+
}

0 commit comments

Comments
 (0)