Skip to content

Commit 992f81d

Browse files
committed
Handle HTTP binding error.
Signed-off-by: Artur Souza <[email protected]>
1 parent d55095c commit 992f81d

File tree

10 files changed

+409
-93
lines changed

10 files changed

+409
-93
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
GOPROXY: https://proxy.golang.org
4545
JDK_VER: ${{ matrix.java }}
4646
DAPR_CLI_VER: 1.13.0-rc.1
47-
DAPR_RUNTIME_VER: 1.13.0-rc.2
47+
DAPR_RUNTIME_VER: 1.13.0-rc.10
4848
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.13.0-rc.1/install/install.sh
4949
DAPR_CLI_REF:
5050
DAPR_REF:

.github/workflows/validate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
GOPROXY: https://proxy.golang.org
3939
JDK_VER: ${{ matrix.java }}
4040
DAPR_CLI_VER: 1.13.0-rc.1
41-
DAPR_RUNTIME_VER: 1.13.0-rc.5
41+
DAPR_RUNTIME_VER: 1.13.0-rc.10
4242
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.13.0-rc.1/install/install.sh
4343
DAPR_CLI_REF:
4444
DAPR_REF:

sdk-actors/src/main/java/io/dapr/actors/client/DaprGrpcClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package io.dapr.actors.client;
1515

1616
import com.google.protobuf.ByteString;
17+
import io.dapr.client.DaprClientGrpc;
1718
import io.dapr.client.resiliency.ResiliencyOptions;
1819
import io.dapr.config.Properties;
1920
import io.dapr.exceptions.DaprException;
@@ -128,7 +129,7 @@ public void start(final Listener<RespT> responseListener, final Metadata metadat
128129
* @return Client after adding interceptors.
129130
*/
130131
private static DaprGrpc.DaprStub intercept(ContextView context, DaprGrpc.DaprStub client) {
131-
return GrpcWrapper.intercept(context, client);
132+
return DaprClientGrpc.intercept(context, client, null);
132133
}
133134

134135
private <T> Mono<T> createMono(Consumer<StreamObserver<T>> consumer) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apiVersion: dapr.io/v1alpha1
2+
kind: Component
3+
metadata:
4+
name: github-http-binding-404
5+
spec:
6+
type: bindings.http
7+
version: v1
8+
metadata:
9+
- name: url
10+
value: https://api.github.com/unknown_path
11+
scopes:
12+
- bindingit-httpoutputbinding-exception
13+
---
14+
apiVersion: dapr.io/v1alpha1
15+
kind: Component
16+
metadata:
17+
name: github-http-binding-404-success
18+
spec:
19+
type: bindings.http
20+
version: v1
21+
metadata:
22+
- name: url
23+
value: https://api.github.com/unknown_path
24+
- name: errorIfNot2XX
25+
value: "false"
26+
scopes:
27+
- bindingit-httpoutputbinding-ignore-error

sdk-tests/src/test/java/io/dapr/it/binding/http/BindingIT.java

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 The Dapr Authors
2+
* Copyright 2024 The Dapr Authors
33
* Licensed under the Apache License, Version 2.0 (the "License");
44
* you may not use this file except in compliance with the License.
55
* You may obtain a copy of the License at
@@ -17,9 +17,9 @@
1717
import io.dapr.client.DaprClient;
1818
import io.dapr.client.DaprClientBuilder;
1919
import io.dapr.client.domain.HttpExtension;
20+
import io.dapr.exceptions.DaprException;
2021
import io.dapr.it.BaseIT;
2122
import io.dapr.it.DaprRun;
22-
import org.junit.jupiter.api.Test;
2323
import org.junit.jupiter.params.ParameterizedTest;
2424
import org.junit.jupiter.params.provider.ValueSource;
2525

@@ -28,28 +28,81 @@
2828

2929
import static io.dapr.it.Retry.callWithRetry;
3030
import static org.junit.jupiter.api.Assertions.assertEquals;
31+
import static org.junit.jupiter.api.Assertions.assertTrue;
3132
import static org.junit.jupiter.api.Assertions.fail;
3233

3334
/**
3435
* Service for input and output binding example.
3536
*/
3637
public class BindingIT extends BaseIT {
3738

38-
private static final String BINDING_NAME = "sample123";
39+
@ParameterizedTest
40+
@ValueSource(booleans = {true, false})
41+
public void httpOutputBindingError(boolean useGrpc) throws Exception {
42+
DaprRun daprRun = startDaprApp(
43+
this.getClass().getSimpleName() + "-httpoutputbinding-exception",
44+
60000);
45+
// At this point, it is guaranteed that the service above is running and all ports being listened to.
46+
if (useGrpc) {
47+
daprRun.switchToGRPC();
48+
} else {
49+
daprRun.switchToHTTP();
50+
}
3951

40-
private static final String BINDING_OPERATION = "create";
52+
try(DaprClient client = new DaprClientBuilder().build()) {
53+
// Validate error message
54+
callWithRetry(() -> {
55+
System.out.println("Checking exception handling for output binding ...");
56+
try {
57+
client.invokeBinding("github-http-binding-404", "get", "").block();
58+
fail("Should throw an exception");
59+
} catch (DaprException e) {
60+
assertEquals(404, e.getHttpStatusCode());
61+
// This HTTP binding did not set `errorIfNot2XX` to false in component metadata, so the error payload is not
62+
// consistent between HTTP and gRPC.
63+
assertTrue(new String(e.getPayload()).contains(
64+
"error invoking output binding github-http-binding-404: received status code 404"));
65+
}
66+
}, 10000);
67+
}
68+
}
4169

42-
public static class MyClass {
43-
public MyClass() {
70+
@ParameterizedTest
71+
@ValueSource(booleans = {true, false})
72+
public void httpOutputBindingErrorIgnoredByComponent(boolean useGrpc) throws Exception {
73+
DaprRun daprRun = startDaprApp(
74+
this.getClass().getSimpleName() + "-httpoutputbinding-ignore-error",
75+
60000);
76+
// At this point, it is guaranteed that the service above is running and all ports being listened to.
77+
if (useGrpc) {
78+
daprRun.switchToGRPC();
79+
} else {
80+
daprRun.switchToHTTP();
4481
}
4582

46-
public String message;
83+
try(DaprClient client = new DaprClientBuilder().build()) {
84+
// Validate error message
85+
callWithRetry(() -> {
86+
System.out.println("Checking exception handling for output binding ...");
87+
try {
88+
client.invokeBinding("github-http-binding-404-success", "get", "").block();
89+
fail("Should throw an exception");
90+
} catch (DaprException e) {
91+
assertEquals(404, e.getHttpStatusCode());
92+
// The HTTP binding must set `errorIfNot2XX` to false in component metadata for the error payload to be
93+
// consistent between HTTP and gRPC.
94+
assertEquals(
95+
"{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest\"}",
96+
new String(e.getPayload()));
97+
}
98+
}, 10000);
99+
}
47100
}
48101

49102
@ParameterizedTest
50103
@ValueSource(booleans = {true, false})
51104
public void inputOutputBinding(boolean useGrpc) throws Exception {
52-
System.out.println("Working Directory = " + System.getProperty("user.dir"));
105+
final String bidingName = "sample123";
53106
String serviceNameVariant = useGrpc ? "-grpc" : "-http";
54107

55108
DaprRun daprRun = startDaprApp(
@@ -69,7 +122,7 @@ public void inputOutputBinding(boolean useGrpc) throws Exception {
69122
callWithRetry(() -> {
70123
System.out.println("Checking if input binding is up before publishing events ...");
71124
client.invokeBinding(
72-
BINDING_NAME, BINDING_OPERATION, "ping").block();
125+
bidingName, "create", "ping").block();
73126

74127
try {
75128
Thread.sleep(1000);
@@ -88,14 +141,14 @@ public void inputOutputBinding(boolean useGrpc) throws Exception {
88141

89142
System.out.println("sending first message");
90143
client.invokeBinding(
91-
BINDING_NAME, BINDING_OPERATION, myClass, Collections.singletonMap("MyMetadata", "MyValue"), Void.class).block();
144+
bidingName, "create", myClass, Collections.singletonMap("MyMetadata", "MyValue"), Void.class).block();
92145

93146
// This is an example of sending a plain string. The input binding will receive
94147
// cat
95148
final String m = "cat";
96149
System.out.println("sending " + m);
97150
client.invokeBinding(
98-
BINDING_NAME, BINDING_OPERATION, m, Collections.singletonMap("MyMetadata", "MyValue"), Void.class).block();
151+
bidingName, "create", m, Collections.singletonMap("MyMetadata", "MyValue"), Void.class).block();
99152

100153
// Metadata is not used by Kafka component, so it is not possible to validate.
101154
callWithRetry(() -> {
@@ -127,4 +180,11 @@ public void inputOutputBinding(boolean useGrpc) throws Exception {
127180
}, 8000);
128181
}
129182
}
183+
184+
public static class MyClass {
185+
public MyClass() {
186+
}
187+
188+
public String message;
189+
}
130190
}

sdk/src/main/java/io/dapr/client/DaprClientGrpc.java

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
import io.dapr.client.resiliency.ResiliencyOptions;
5151
import io.dapr.config.Properties;
5252
import io.dapr.exceptions.DaprException;
53-
import io.dapr.internal.opencensus.GrpcWrapper;
53+
import io.dapr.internal.exceptions.DaprHttpException;
5454
import io.dapr.internal.resiliency.RetryPolicy;
5555
import io.dapr.internal.resiliency.TimeoutPolicy;
5656
import io.dapr.serializer.DaprObjectSerializer;
@@ -65,6 +65,7 @@
6565
import io.grpc.ClientCall;
6666
import io.grpc.ClientInterceptor;
6767
import io.grpc.ForwardingClientCall;
68+
import io.grpc.ForwardingClientCallListener;
6869
import io.grpc.Metadata;
6970
import io.grpc.MethodDescriptor;
7071
import io.grpc.stub.StreamObserver;
@@ -81,10 +82,14 @@
8182
import java.util.Iterator;
8283
import java.util.List;
8384
import java.util.Map;
84-
import java.util.concurrent.ExecutionException;
8585
import java.util.function.Consumer;
8686
import java.util.stream.Collectors;
8787

88+
import static io.dapr.internal.exceptions.DaprHttpException.isSuccessfulHttpStatusCode;
89+
import static io.dapr.internal.exceptions.DaprHttpException.isValidHttpStatusCode;
90+
import static io.dapr.internal.exceptions.DaprHttpException.parseHttpStatusCode;
91+
import static io.dapr.internal.opencensus.GrpcWrapper.appendTracingToMetadata;
92+
8893
/**
8994
* An adapter for the GRPC Client.
9095
*
@@ -351,6 +356,7 @@ public <T> Mono<T> invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef
351356
*/
352357
@Override
353358
public <T> Mono<T> invokeBinding(InvokeBindingRequest request, TypeRef<T> type) {
359+
Metadata responseMetadata = new Metadata();
354360
try {
355361
final String name = request.getName();
356362
final String operation = request.getOperation();
@@ -377,10 +383,19 @@ public <T> Mono<T> invokeBinding(InvokeBindingRequest request, TypeRef<T> type)
377383

378384
return Mono.deferContextual(
379385
context -> this.<DaprProtos.InvokeBindingResponse>createMono(
380-
it -> intercept(context, asyncStub).invokeBinding(envelope, it)
386+
responseMetadata,
387+
it -> intercept(context, asyncStub, m -> responseMetadata.merge(m)).invokeBinding(envelope, it)
381388
)
382389
).flatMap(
383390
it -> {
391+
int httpStatusCode =
392+
parseHttpStatusCode(it.getMetadataMap().getOrDefault("statusCode", ""));
393+
if (isValidHttpStatusCode(httpStatusCode) && !isSuccessfulHttpStatusCode(httpStatusCode)) {
394+
// Exception condition in a successful request.
395+
// This is useful to send an exception due to an error from the HTTP binding component.
396+
throw DaprException.propagate(new DaprHttpException(httpStatusCode, it.getData().toByteArray()));
397+
}
398+
384399
try {
385400
return Mono.justOrEmpty(objectSerializer.deserialize(it.getData().toByteArray(), type));
386401
} catch (IOException e) {
@@ -1155,21 +1170,74 @@ public void start(final Listener<RespT> responseListener, final Metadata metadat
11551170
* @param client GRPC client for Dapr.
11561171
* @return Client after adding interceptors.
11571172
*/
1158-
private static DaprGrpc.DaprStub intercept(ContextView context, DaprGrpc.DaprStub client) {
1159-
return GrpcWrapper.intercept(context, client);
1173+
private static DaprGrpc.DaprStub intercept(
1174+
ContextView context,
1175+
DaprGrpc.DaprStub client) {
1176+
return intercept(context, client, null);
1177+
}
1178+
1179+
/**
1180+
* Populates GRPC client with interceptors for telemetry - internal use only.
1181+
*
1182+
* @param context Reactor's context.
1183+
* @param client GRPC client for Dapr.
1184+
* @param metadataConsumer Handles metadata result.
1185+
* @return Client after adding interceptors.
1186+
*/
1187+
public static DaprGrpc.DaprStub intercept(
1188+
ContextView context,
1189+
DaprGrpc.DaprStub client,
1190+
Consumer<Metadata> metadataConsumer) {
1191+
ClientInterceptor interceptor = new ClientInterceptor() {
1192+
@Override
1193+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
1194+
MethodDescriptor<ReqT, RespT> methodDescriptor,
1195+
CallOptions options,
1196+
Channel channel) {
1197+
ClientCall<ReqT, RespT> clientCall = channel.newCall(methodDescriptor, options);
1198+
return new ForwardingClientCall.SimpleForwardingClientCall<>(clientCall) {
1199+
@Override
1200+
public void start(final Listener<RespT> responseListener, final Metadata metadata) {
1201+
appendTracingToMetadata(context, metadata);
1202+
1203+
final ClientCall.Listener<RespT> headerListener =
1204+
new ForwardingClientCallListener.SimpleForwardingClientCallListener<>(responseListener) {
1205+
@Override
1206+
public void onHeaders(Metadata headers) {
1207+
responseListener.onHeaders(headers);
1208+
if (metadataConsumer != null) {
1209+
metadataConsumer.accept(headers);
1210+
}
1211+
}
1212+
};
1213+
super.start(headerListener, metadata);
1214+
}
1215+
};
1216+
}
1217+
};
1218+
return client.withInterceptors(interceptor);
11601219
}
11611220

11621221
private <T> Mono<T> createMono(Consumer<StreamObserver<T>> consumer) {
1222+
return this.createMono(null, consumer);
1223+
}
1224+
1225+
private <T> Mono<T> createMono(Metadata metadata, Consumer<StreamObserver<T>> consumer) {
11631226
return retryPolicy.apply(
1164-
Mono.create(sink -> DaprException.wrap(() -> consumer.accept(createStreamObserver(sink))).run()));
1227+
Mono.create(sink -> DaprException.wrap(() -> consumer.accept(
1228+
createStreamObserver(sink, metadata))).run()));
11651229
}
11661230

11671231
private <T> Flux<T> createFlux(Consumer<StreamObserver<T>> consumer) {
1232+
return this.createFlux(null, consumer);
1233+
}
1234+
1235+
private <T> Flux<T> createFlux(Metadata metadata, Consumer<StreamObserver<T>> consumer) {
11681236
return retryPolicy.apply(
1169-
Flux.create(sink -> DaprException.wrap(() -> consumer.accept(createStreamObserver(sink))).run()));
1237+
Flux.create(sink -> DaprException.wrap(() -> consumer.accept(createStreamObserver(sink, metadata))).run()));
11701238
}
11711239

1172-
private <T> StreamObserver<T> createStreamObserver(MonoSink<T> sink) {
1240+
private <T> StreamObserver<T> createStreamObserver(MonoSink<T> sink, Metadata grpcMetadata) {
11731241
return new StreamObserver<T>() {
11741242
@Override
11751243
public void onNext(T value) {
@@ -1178,7 +1246,7 @@ public void onNext(T value) {
11781246

11791247
@Override
11801248
public void onError(Throwable t) {
1181-
sink.error(DaprException.propagate(new ExecutionException(t)));
1249+
sink.error(DaprException.propagate(DaprHttpException.fromGrpcExecutionException(grpcMetadata, t)));
11821250
}
11831251

11841252
@Override
@@ -1188,7 +1256,7 @@ public void onCompleted() {
11881256
};
11891257
}
11901258

1191-
private <T> StreamObserver<T> createStreamObserver(FluxSink<T> sink) {
1259+
private <T> StreamObserver<T> createStreamObserver(FluxSink<T> sink, final Metadata grpcMetadata) {
11921260
return new StreamObserver<T>() {
11931261
@Override
11941262
public void onNext(T value) {
@@ -1197,7 +1265,7 @@ public void onNext(T value) {
11971265

11981266
@Override
11991267
public void onError(Throwable t) {
1200-
sink.error(DaprException.propagate(new ExecutionException(t)));
1268+
sink.error(DaprException.propagate(DaprHttpException.fromGrpcExecutionException(grpcMetadata, t)));
12011269
}
12021270

12031271
@Override

0 commit comments

Comments
 (0)