Skip to content

Commit 2115e2d

Browse files
therepaniconobc
authored andcommitted
Propagate StatusException trailers in GrpcExceptionHandlerInterceptor
Ensure that trailers from StatusException returned by GrpcExceptionHandler are used when closing the ServerCall. Closes: gh-332 Signed-off-by: Andrey Litvitski <[email protected]>
1 parent eb2adf7 commit 2115e2d

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* returns a null.
4545
*
4646
* @author Dave Syer
47+
* @author Andrey Litvitski
4748
* @see ServerInterceptor
4849
* @see GrpcExceptionHandler
4950
*/
@@ -81,7 +82,7 @@ public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
8182
this.logger.trace("Failed to start exception handler call", t);
8283
StatusException statusEx = fallbackHandler.handleException(t);
8384
exceptionHandledServerCall.close(statusEx != null ? statusEx.getStatus() : Status.fromThrowable(t),
84-
headers(t));
85+
headers(statusEx != null ? statusEx : t));
8586
return new Listener<>() {
8687
};
8788
}

spring-grpc-core/src/test/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptorTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,35 @@
1717
package org.springframework.grpc.server.exception;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.times;
22+
import static org.mockito.Mockito.verify;
23+
import static org.mockito.Mockito.when;
2024

2125
import org.junit.jupiter.api.Test;
26+
import org.mockito.ArgumentCaptor;
2227

2328
import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor.FallbackHandler;
2429

30+
import com.google.protobuf.Any;
31+
import com.google.protobuf.Empty;
32+
import com.google.rpc.Code;
33+
import com.google.rpc.Status;
34+
import io.grpc.Metadata;
35+
import io.grpc.MethodDescriptor;
36+
import io.grpc.ServerCall;
37+
import io.grpc.ServerCallHandler;
38+
import io.grpc.ServerInterceptor;
39+
import io.grpc.StatusException;
40+
import io.grpc.protobuf.ProtoUtils;
41+
import io.grpc.protobuf.StatusProto;
42+
43+
/**
44+
* Tests for {@link GrpcExceptionHandlerInterceptor}.
45+
*
46+
* @author Dave Syer
47+
* @author Andrey Litvitski
48+
*/
2549
public class GrpcExceptionHandlerInterceptorTests {
2650

2751
@Test
@@ -30,4 +54,38 @@ void testNullStatusHandled() {
3054
.isNotNull();
3155
}
3256

57+
@Test
58+
void propagatesTrailersFromStatusExceptionWhenStartCallThrows() {
59+
Status statusWithDetails = Status.newBuilder()
60+
.setCode(Code.PERMISSION_DENIED_VALUE)
61+
.setMessage("access denied")
62+
.addDetails(Any.pack(Empty.getDefaultInstance()))
63+
.build();
64+
StatusException statusEx = StatusProto.toStatusException(statusWithDetails);
65+
GrpcExceptionHandler handler = ex -> statusEx;
66+
ServerInterceptor interceptor = new GrpcExceptionHandlerInterceptor(handler);
67+
@SuppressWarnings("unchecked")
68+
ServerCall<Empty, Empty> call = mock(ServerCall.class);
69+
MethodDescriptor<Empty, Empty> method = MethodDescriptor.<Empty, Empty>newBuilder()
70+
.setType(MethodDescriptor.MethodType.UNARY)
71+
.setFullMethodName("test/Test")
72+
.setRequestMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
73+
.setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
74+
.build();
75+
when(call.getMethodDescriptor()).thenReturn(method);
76+
ServerCallHandler<Empty, Empty> next = (c, headers) -> {
77+
throw new RuntimeException("boom");
78+
};
79+
interceptor.interceptCall(call, new Metadata(), next);
80+
ArgumentCaptor<io.grpc.Status> statusCaptor = ArgumentCaptor.forClass(io.grpc.Status.class);
81+
ArgumentCaptor<Metadata> trailersCaptor = ArgumentCaptor.forClass(Metadata.class);
82+
verify(call, times(1)).close(statusCaptor.capture(), trailersCaptor.capture());
83+
io.grpc.Status closedStatus = statusCaptor.getValue();
84+
Metadata closedTrailers = trailersCaptor.getValue();
85+
assertThat(closedStatus.getCode()).isEqualTo(io.grpc.Status.Code.PERMISSION_DENIED);
86+
Status extracted = StatusProto.fromThrowable(new StatusException(closedStatus, closedTrailers));
87+
assertThat(extracted).isNotNull();
88+
assertThat(extracted).isEqualTo(statusWithDetails);
89+
}
90+
3391
}

0 commit comments

Comments
 (0)