mirror of https://github.com/grpc/grpc-java.git
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:
parent
c38611a230
commit
78107a69c0
|
|
@ -185,7 +185,7 @@ public final class GrpcUtil {
|
|||
}
|
||||
if (httpStatusCode < 300) {
|
||||
// 2xx
|
||||
return Status.OK;
|
||||
return Status.UNKNOWN;
|
||||
}
|
||||
return Status.UNKNOWN;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue