core: Do not rely on HTTP 200

We only want to use the HTTP code for errors, when the response is not
grpc. grpc status codes may be mapped to HTTP codes in the future, and
we don't want to break when that happens. We also don't want to ever
accidentally use Status.OK without receiving it from the server, even
for HTTP 200.
This commit is contained in:
Eric Anderson 2016-09-26 13:55:47 -07:00
parent c38611a230
commit 78107a69c0
6 changed files with 463 additions and 112 deletions

View File

@ -185,7 +185,7 @@ public final class GrpcUtil {
}
if (httpStatusCode < 300) {
// 2xx
return Status.OK;
return Status.UNKNOWN;
}
return Status.UNKNOWN;
}

View File

@ -79,7 +79,7 @@ public abstract class Http2ClientStream extends AbstractClientStream {
private Status transportError;
private Metadata transportErrorMetadata;
private Charset errorCharset = Charsets.UTF_8;
private boolean contentTypeChecked;
private boolean headersReceived;
protected Http2ClientStream(WritableBufferAllocator bufferAllocator, int maxMessageSize,
StatsTraceContext statsTraceCtx) {
@ -94,28 +94,37 @@ public abstract class Http2ClientStream extends AbstractClientStream {
protected void transportHeadersReceived(Metadata headers) {
Preconditions.checkNotNull(headers, "headers");
if (transportError != null) {
// Already received a transport error so just augment it.
transportError = transportError.augmentDescription(headers.toString());
// Already received a transport error so just augment it. Something is really, really strange.
transportError = transportError.augmentDescription("headers: " + headers);
return;
}
Status httpStatus = statusFromHttpStatus(headers);
if (httpStatus == null) {
transportError = Status.INTERNAL.withDescription(
"received non-terminal headers with no :status");
} else if (!httpStatus.isOk()) {
transportError = httpStatus;
} else {
transportError = checkContentType(headers);
}
if (transportError != null) {
// Note we don't immediately report the transport error, instead we wait for more data on the
// stream so we can accumulate more detail into the error before reporting it.
transportError = transportError.augmentDescription("\n" + headers);
transportErrorMetadata = headers;
errorCharset = extractCharset(headers);
} else {
try {
if (headersReceived) {
transportError = Status.INTERNAL.withDescription("Received headers twice");
return;
}
Integer httpStatus = headers.get(HTTP2_STATUS);
if (httpStatus != null && httpStatus >= 100 && httpStatus < 200) {
// Ignore the headers. See RFC 7540 §8.1
return;
}
headersReceived = true;
transportError = validateInitialMetadata(headers);
if (transportError != null) {
return;
}
stripTransportDetails(headers);
inboundHeadersReceived(headers);
} finally {
if (transportError != null) {
// Note we don't immediately report the transport error, instead we wait for more data on
// the stream so we can accumulate more detail into the error before reporting it.
transportError = transportError.augmentDescription("headers: " + headers);
transportErrorMetadata = headers;
errorCharset = extractCharset(headers);
}
}
}
@ -162,14 +171,14 @@ public abstract class Http2ClientStream extends AbstractClientStream {
*/
protected void transportTrailersReceived(Metadata trailers) {
Preconditions.checkNotNull(trailers, "trailers");
if (transportError != null) {
// Already received a transport error so just augment it.
transportError = transportError.augmentDescription(trailers.toString());
} else {
transportError = checkContentType(trailers);
transportErrorMetadata = trailers;
if (transportError == null && !headersReceived) {
transportError = validateInitialMetadata(trailers);
if (transportError != null) {
transportErrorMetadata = trailers;
}
}
if (transportError != null) {
transportError = transportError.augmentDescription("trailers: " + trailers);
inboundTransportError(transportError, transportErrorMetadata);
sendCancel(Status.CANCELLED);
} else {
@ -179,50 +188,44 @@ public abstract class Http2ClientStream extends AbstractClientStream {
}
}
private static Status statusFromHttpStatus(Metadata metadata) {
Integer httpStatus = metadata.get(HTTP2_STATUS);
if (httpStatus != null) {
Status status = GrpcUtil.httpStatusToGrpcStatus(httpStatus);
return status.isOk() ? status
: status.augmentDescription("extracted status from HTTP :status " + httpStatus);
}
return null;
}
/**
* Extract the response status from trailers.
*/
private Status statusFromTrailers(Metadata trailers) {
Status status = trailers.get(Status.CODE_KEY);
if (status == null) {
status = statusFromHttpStatus(trailers);
if (status == null || status.isOk()) {
status = Status.UNKNOWN.withDescription("missing GRPC status in response");
} else {
status = status.withDescription(
"missing GRPC status, inferred error from HTTP status code");
}
if (status != null) {
return status.withDescription(trailers.get(Status.MESSAGE_KEY));
}
String message = trailers.get(Status.MESSAGE_KEY);
if (message != null) {
status = status.augmentDescription(message);
// No status; something is broken. Try to provide a resonanable error.
if (headersReceived) {
return Status.UNKNOWN.withDescription("missing GRPC status in response");
}
return status;
Integer httpStatus = trailers.get(HTTP2_STATUS);
if (httpStatus != null) {
status = GrpcUtil.httpStatusToGrpcStatus(httpStatus);
} else {
status = Status.INTERNAL.withDescription("missing HTTP status code");
}
return status.augmentDescription(
"missing GRPC status, inferred error from HTTP status code");
}
/**
* Inspect the content type field from received headers or trailers and return an error Status if
* content type is invalid or not present. Returns null if no error was found.
* Inspect initial headers to make sure they conform to HTTP and gRPC, returning a {@code Status}
* on failure.
*
* @return status with description of failure, or {@code null} when valid
*/
@Nullable
private Status checkContentType(Metadata headers) {
if (contentTypeChecked) {
return null;
private Status validateInitialMetadata(Metadata headers) {
Integer httpStatus = headers.get(HTTP2_STATUS);
if (httpStatus == null) {
return Status.INTERNAL.withDescription("Missing HTTP status code");
}
contentTypeChecked = true;
String contentType = headers.get(GrpcUtil.CONTENT_TYPE_KEY);
if (!GrpcUtil.isGrpcContentType(contentType)) {
return Status.INTERNAL.withDescription("Invalid content-type: " + contentType);
return GrpcUtil.httpStatusToGrpcStatus(httpStatus)
.augmentDescription("invalid content-type: " + contentType);
}
return null;
}

View File

@ -79,7 +79,7 @@ public abstract class Http2ClientStreamTransportState extends AbstractClientStre
private Status transportError;
private Metadata transportErrorMetadata;
private Charset errorCharset = Charsets.UTF_8;
private boolean contentTypeChecked;
private boolean headersReceived;
protected Http2ClientStreamTransportState(int maxMessageSize, StatsTraceContext statsTraceCtx) {
super(maxMessageSize, statsTraceCtx);
@ -99,28 +99,37 @@ public abstract class Http2ClientStreamTransportState extends AbstractClientStre
protected void transportHeadersReceived(Metadata headers) {
Preconditions.checkNotNull(headers, "headers");
if (transportError != null) {
// Already received a transport error so just augment it.
transportError = transportError.augmentDescription(headers.toString());
// Already received a transport error so just augment it. Something is really, really strange.
transportError = transportError.augmentDescription("headers: " + headers);
return;
}
Status httpStatus = statusFromHttpStatus(headers);
if (httpStatus == null) {
transportError = Status.INTERNAL.withDescription(
"received non-terminal headers with no :status");
} else if (!httpStatus.isOk()) {
transportError = httpStatus;
} else {
transportError = checkContentType(headers);
}
if (transportError != null) {
// Note we don't immediately report the transport error, instead we wait for more data on the
// stream so we can accumulate more detail into the error before reporting it.
transportError = transportError.augmentDescription("\n" + headers);
transportErrorMetadata = headers;
errorCharset = extractCharset(headers);
} else {
try {
if (headersReceived) {
transportError = Status.INTERNAL.withDescription("Received headers twice");
return;
}
Integer httpStatus = headers.get(HTTP2_STATUS);
if (httpStatus != null && httpStatus >= 100 && httpStatus < 200) {
// Ignore the headers. See RFC 7540 §8.1
return;
}
headersReceived = true;
transportError = validateInitialMetadata(headers);
if (transportError != null) {
return;
}
stripTransportDetails(headers);
inboundHeadersReceived(headers);
} finally {
if (transportError != null) {
// Note we don't immediately report the transport error, instead we wait for more data on
// the stream so we can accumulate more detail into the error before reporting it.
transportError = transportError.augmentDescription("headers: " + headers);
transportErrorMetadata = headers;
errorCharset = extractCharset(headers);
}
}
}
@ -159,14 +168,14 @@ public abstract class Http2ClientStreamTransportState extends AbstractClientStre
*/
protected void transportTrailersReceived(Metadata trailers) {
Preconditions.checkNotNull(trailers, "trailers");
if (transportError != null) {
// Already received a transport error so just augment it.
transportError = transportError.augmentDescription(trailers.toString());
} else {
transportError = checkContentType(trailers);
transportErrorMetadata = trailers;
if (transportError == null && !headersReceived) {
transportError = validateInitialMetadata(trailers);
if (transportError != null) {
transportErrorMetadata = trailers;
}
}
if (transportError != null) {
transportError = transportError.augmentDescription("trailers: " + trailers);
http2ProcessingFailed(transportError, transportErrorMetadata);
} else {
Status status = statusFromTrailers(trailers);
@ -175,50 +184,44 @@ public abstract class Http2ClientStreamTransportState extends AbstractClientStre
}
}
private static Status statusFromHttpStatus(Metadata metadata) {
Integer httpStatus = metadata.get(HTTP2_STATUS);
if (httpStatus != null) {
Status status = GrpcUtil.httpStatusToGrpcStatus(httpStatus);
return status.isOk() ? status
: status.augmentDescription("extracted status from HTTP :status " + httpStatus);
}
return null;
}
/**
* Extract the response status from trailers.
*/
private static Status statusFromTrailers(Metadata trailers) {
private Status statusFromTrailers(Metadata trailers) {
Status status = trailers.get(Status.CODE_KEY);
if (status == null) {
status = statusFromHttpStatus(trailers);
if (status == null || status.isOk()) {
status = Status.UNKNOWN.withDescription("missing GRPC status in response");
} else {
status = status.withDescription(
"missing GRPC status, inferred error from HTTP status code");
}
if (status != null) {
return status.withDescription(trailers.get(Status.MESSAGE_KEY));
}
String message = trailers.get(Status.MESSAGE_KEY);
if (message != null) {
status = status.augmentDescription(message);
// No status; something is broken. Try to provide a resonanable error.
if (headersReceived) {
return Status.UNKNOWN.withDescription("missing GRPC status in response");
}
return status;
Integer httpStatus = trailers.get(HTTP2_STATUS);
if (httpStatus != null) {
status = GrpcUtil.httpStatusToGrpcStatus(httpStatus);
} else {
status = Status.INTERNAL.withDescription("missing HTTP status code");
}
return status.augmentDescription(
"missing GRPC status, inferred error from HTTP status code");
}
/**
* Inspect the content type field from received headers or trailers and return an error Status if
* content type is invalid or not present. Returns null if no error was found.
* Inspect initial headers to make sure they conform to HTTP and gRPC, returning a {@code Status}
* on failure.
*
* @return status with description of failure, or {@code null} when valid
*/
@Nullable
private Status checkContentType(Metadata headers) {
if (contentTypeChecked) {
return null;
private Status validateInitialMetadata(Metadata headers) {
Integer httpStatus = headers.get(HTTP2_STATUS);
if (httpStatus == null) {
return Status.INTERNAL.withDescription("Missing HTTP status code");
}
contentTypeChecked = true;
String contentType = headers.get(GrpcUtil.CONTENT_TYPE_KEY);
if (!GrpcUtil.isGrpcContentType(contentType)) {
return Status.INTERNAL.withDescription("Invalid content-type: " + contentType);
return GrpcUtil.httpStatusToGrpcStatus(httpStatus)
.augmentDescription("invalid content-type: " + contentType);
}
return null;
}

View File

@ -0,0 +1,344 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.internal;
import static com.google.common.base.Charsets.US_ASCII;
import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.Status.Code;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link Http2ClientStreamTransportState}. */
@RunWith(JUnit4.class)
public class Http2ClientStreamTransportStateTest {
@Mock private ClientStreamListener mockListener;
@Captor private ArgumentCaptor<Status> statusCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void transportHeadersReceived_notifiesListener() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
verify(mockListener, never()).closed(any(Status.class), any(Metadata.class));
verify(mockListener).headersRead(headers);
}
@Test
public void transportHeadersReceived_doesntRequire200() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "500");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
verify(mockListener, never()).closed(any(Status.class), any(Metadata.class));
verify(mockListener).headersRead(headers);
}
@Test
public void transportHeadersReceived_noHttpStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
state.transportDataReceived(ReadableBuffers.empty(), true);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(headers));
assertEquals(Code.INTERNAL, statusCaptor.getValue().getCode());
}
@Test
public void transportHeadersReceived_wrongContentType_200() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "text/html");
state.transportHeadersReceived(headers);
state.transportDataReceived(ReadableBuffers.empty(), true);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(headers));
assertEquals(Code.UNKNOWN, statusCaptor.getValue().getCode());
assertTrue(statusCaptor.getValue().getDescription().contains("200"));
}
@Test
public void transportHeadersReceived_wrongContentType_401() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "401");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "text/html");
state.transportHeadersReceived(headers);
state.transportDataReceived(ReadableBuffers.empty(), true);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(headers));
assertEquals(Code.UNAUTHENTICATED, statusCaptor.getValue().getCode());
assertTrue(statusCaptor.getValue().getDescription().contains("401"));
assertTrue(statusCaptor.getValue().getDescription().contains("text/html"));
}
@Test
public void transportHeadersReceived_handles_1xx() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata infoHeaders = new Metadata();
infoHeaders.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "100");
state.transportHeadersReceived(infoHeaders);
Metadata infoHeaders2 = new Metadata();
infoHeaders2.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "101");
state.transportHeadersReceived(infoHeaders2);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
verify(mockListener, never()).closed(any(Status.class), any(Metadata.class));
verify(mockListener).headersRead(headers);
}
@Test
public void transportHeadersReceived_twice() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
Metadata headersAgain = new Metadata();
state.transportHeadersReceived(headersAgain);
state.transportDataReceived(ReadableBuffers.empty(), true);
verify(mockListener).headersRead(headers);
verify(mockListener).closed(statusCaptor.capture(), same(headersAgain));
assertEquals(Code.INTERNAL, statusCaptor.getValue().getCode());
assertTrue(statusCaptor.getValue().getDescription().contains("twice"));
}
@Test
public void transportHeadersReceived_unknownAndTwiceLogsSecondHeaders() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "text/html");
state.transportHeadersReceived(headers);
Metadata headersAgain = new Metadata();
String testString = "This is a test";
headersAgain.put(Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER), testString);
state.transportHeadersReceived(headersAgain);
state.transportDataReceived(ReadableBuffers.empty(), true);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(headers));
assertEquals(Code.UNKNOWN, statusCaptor.getValue().getCode());
assertTrue(statusCaptor.getValue().getDescription().contains(testString));
}
@Test
public void transportDataReceived_debugData() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "text/html");
state.transportHeadersReceived(headers);
String testString = "This is a test";
state.transportDataReceived(ReadableBuffers.wrap(testString.getBytes(US_ASCII)), true);
verify(mockListener).closed(statusCaptor.capture(), same(headers));
assertTrue(statusCaptor.getValue().getDescription().contains(testString));
}
@Test
public void transportTrailersReceived_notifiesListener() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
trailers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
trailers.put(Metadata.Key.of("grpc-status", Metadata.ASCII_STRING_MARSHALLER), "0");
state.transportTrailersReceived(trailers);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(Status.OK, trailers);
}
@Test
public void transportTrailersReceived_afterHeaders() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of("grpc-status", Metadata.ASCII_STRING_MARSHALLER), "0");
state.transportTrailersReceived(trailers);
verify(mockListener).headersRead(headers);
verify(mockListener).closed(Status.OK, trailers);
}
@Test
public void transportTrailersReceived_observesStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
trailers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
trailers.put(Metadata.Key.of("grpc-status", Metadata.ASCII_STRING_MARSHALLER), "1");
state.transportTrailersReceived(trailers);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(Status.CANCELLED, trailers);
}
@Test
public void transportTrailersReceived_missingStatusUsesHttpStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "401");
trailers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportTrailersReceived(trailers);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(trailers));
assertEquals(Code.UNAUTHENTICATED, statusCaptor.getValue().getCode());
}
@Test
public void transportTrailersReceived_missingHttpStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
trailers.put(Metadata.Key.of("grpc-status", Metadata.ASCII_STRING_MARSHALLER), "0");
state.transportTrailersReceived(trailers);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(trailers));
assertEquals(Code.INTERNAL, statusCaptor.getValue().getCode());
}
@Test
public void transportTrailersReceived_missingStatusAndMissingHttpStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportTrailersReceived(trailers);
verify(mockListener, never()).headersRead(any(Metadata.class));
verify(mockListener).closed(statusCaptor.capture(), same(trailers));
assertEquals(Code.INTERNAL, statusCaptor.getValue().getCode());
}
@Test
public void transportTrailersReceived_missingStatusAfterHeadersIgnoresHttpStatus() {
BaseTransportState state = new BaseTransportState();
state.setListener(mockListener);
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "200");
headers.put(Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER),
"application/grpc");
state.transportHeadersReceived(headers);
Metadata trailers = new Metadata();
trailers.put(Metadata.Key.of(":status", Metadata.ASCII_STRING_MARSHALLER), "401");
state.transportTrailersReceived(trailers);
verify(mockListener).headersRead(headers);
verify(mockListener).closed(statusCaptor.capture(), same(trailers));
assertEquals(Code.UNKNOWN, statusCaptor.getValue().getCode());
}
private static class BaseTransportState extends Http2ClientStreamTransportState {
public BaseTransportState() {
super(DEFAULT_MAX_MESSAGE_SIZE, StatsTraceContext.NOOP);
}
@Override
protected void http2ProcessingFailed(Status status, Metadata trailers) {
transportReportStatus(status, false, trailers);
}
@Override
protected void deframeFailed(Throwable cause) {}
@Override
public void bytesRead(int processedBytes) {}
}
}

View File

@ -288,7 +288,7 @@ public class NettyClientStreamTest extends NettyStreamTestBase<NettyClientStream
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
verify(listener).closed(captor.capture(), metadataCaptor.capture());
Status status = captor.getValue();
assertEquals(Status.Code.INTERNAL, status.getCode());
assertEquals(Status.Code.UNKNOWN, status.getCode());
assertTrue(status.getDescription().contains("content-type"));
assertEquals("application/bad", metadataCaptor.getValue()
.get(Metadata.Key.of("Content-Type", Metadata.ASCII_STRING_MARSHALLER)));

View File

@ -1413,7 +1413,8 @@ public class OkHttpClientTransportTest {
private List<Header> grpcResponseTrailers() {
return ImmutableList.of(
new Header(Status.CODE_KEY.name(), "0"),
// Adding Content-Type for testing responses with only a single HEADERS frame.
// Adding Content-Type and :status for testing responses with only a single HEADERS frame.
new Header(":status", "200"),
CONTENT_TYPE_HEADER);
}