question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`AggregatedHttpResponseHandler` doesn't respect `ServiceRequestContext.additionalResponse{Headers,Trailers}`

See original GitHub issue

Unlike HttpResponseSubscriber, AggregatedHttpResponseHandler doesn’t merge ServiceRequestContext.additionalResponseHeaders and additionalResponseTrailers when encoding response headers and trailers:

https://github.com/line/armeria/blob/871d87297e4d051241589cb1ae95641cbc83f880/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseHandler.java#L103-L134

The following patch reproduces the problem by modifying AbstractUnaryGrpcServiceTest:

diff --git grpc/src/test/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnaryGrpcServiceTest.java grpc/src/test/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnaryGrpcServiceTest.java
index 0e07caee8..9da85a810 100644
--- grpc/src/test/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnaryGrpcServiceTest.java
+++ grpc/src/test/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnaryGrpcServiceTest.java
@@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;

 import java.io.UncheckedIOException;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Stream;

 import org.junit.jupiter.api.Test;
@@ -61,8 +62,10 @@ import com.linecorp.armeria.testing.server.ServiceRequestContextCaptor;

 import io.grpc.ManagedChannel;
 import io.grpc.ManagedChannelBuilder;
+import io.grpc.Metadata;
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
+import io.grpc.stub.MetadataUtils;

 class AbstractUnaryGrpcServiceTest {
     private static final String METHOD_NAME = "/armeria.grpc.testing.TestService/UnaryCall";
@@ -94,6 +97,7 @@ class AbstractUnaryGrpcServiceTest {
             } catch (InvalidProtocolBufferException e) {
                 throw new UncheckedIOException(e);
             }
+            ctx.setAdditionalResponseTrailer("mytest", "value");
             if (request.hasResponseStatus()) {
                 // For statusExceptionUpstream() and statusExceptionDownstream()
                 assertThat(request).isEqualTo(EXCEPTION_REQUEST_MESSAGE);
@@ -125,15 +129,23 @@ class AbstractUnaryGrpcServiceTest {
     @ParameterizedTest
     @ArgumentsSource(UnaryGrpcSerializationFormatArgumentsProvider.class)
     void normalDownstream(SerializationFormat serializationFormat) throws Exception {
+        final AtomicReference<Metadata> headersCapture = new AtomicReference<>();
+        final AtomicReference<Metadata> trailersCapture = new AtomicReference<>();
         final TestServiceBlockingStub stub =
                 GrpcClients.newClient(server.httpUri(serializationFormat),
-                                      TestServiceBlockingStub.class);
+                                      TestServiceBlockingStub.class).withInterceptors(
+                        MetadataUtils.newCaptureMetadataInterceptor(headersCapture, trailersCapture));
         final SimpleResponse response = stub.unaryCall(REQUEST_MESSAGE);
         assertThat(response).isEqualTo(RESPONSE_MESSAGE);
         final ServiceRequestContextCaptor captor = server.requestContextCaptor();
         final HttpHeaders trailers = GrpcWebTrailers.get(captor.take());
         assertThat(trailers).isNotNull();
         assertThat(trailers.getInt(GrpcHeaderNames.GRPC_STATUS)).isZero();
+        if (serializationFormat.toString().equals("gproto")) {
+            assertThat(trailersCapture.get().get(Metadata.Key.of("mytest",
+                                                                 Metadata.ASCII_STRING_MARSHALLER))).isEqualTo(
+                    "value");
+        }
     }

     @Test
@@ -153,9 +165,12 @@ class AbstractUnaryGrpcServiceTest {
     @ParameterizedTest
     @ArgumentsSource(UnaryGrpcSerializationFormatArgumentsProvider.class)
     void statusExceptionDownstream(SerializationFormat serializationFormat) throws Exception {
+        final AtomicReference<Metadata> headersCapture = new AtomicReference<>();
+        final AtomicReference<Metadata> trailersCapture = new AtomicReference<>();
         final TestServiceBlockingStub stub =
                 GrpcClients.newClient(server.httpUri(serializationFormat),
-                                      TestServiceBlockingStub.class);
+                                      TestServiceBlockingStub.class).withInterceptors(
+                        MetadataUtils.newCaptureMetadataInterceptor(headersCapture, trailersCapture));
         assertThatThrownBy(() -> stub.unaryCall(EXCEPTION_REQUEST_MESSAGE))
                 .isInstanceOfSatisfying(StatusRuntimeException.class, cause -> {
                     final Status status = cause.getStatus();
@@ -167,6 +182,10 @@ class AbstractUnaryGrpcServiceTest {
         final HttpHeaders trailers = GrpcWebTrailers.get(captor.take());
         assertThat(trailers).isNotNull();
         assertThat(trailers.getInt(GrpcHeaderNames.GRPC_STATUS)).isEqualTo(StatusCodes.PERMISSION_DENIED);
+        if ("gproto".equals(serializationFormat.toString())) {
+            assertThat(trailersCapture.get().get(
+                    Metadata.Key.of("mytest", Metadata.ASCII_STRING_MARSHALLER))).isEqualTo("value");
+        }
     }

     @Test

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:1
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
trustincommented, May 17, 2022

At second thought, I came to the conclusion that this is not what we really want because:

  • It’s not efficient; and
  • It doesn’t work for gRPC-Web.

Let me close this issue and open a new one with a better idea, given the original issue description is not correct anyway.

1reaction
trustincommented, May 17, 2022

I see. According to gRPC specification:

Response-Headers & Trailers-Only are each delivered in a single HTTP2 HEADERS frame block. Most responses are expected to have both headers and trailers but Trailers-Only is permitted for calls that produce an immediate error. Status must be sent in Trailers even if the status code is OK.

What if we put grpc-status and grpc-message in HTTP/2 trailers, then?

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found