Adding default User-Agent for netty and okhttp.

This commit is contained in:
nmittler 2015-06-01 08:31:00 -07:00
parent efbb65522b
commit 8c1d38a0d8
11 changed files with 225 additions and 75 deletions

View File

@ -28,11 +28,21 @@ subprojects {
mavenLocal()
}
[compileJava, compileTestJava].each() {
it.options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation", "-Xlint:-options"]
it.options.encoding = "UTF-8"
}
jar.manifest {
attributes('Implementation-Title': name,
'Implementation-Version': version,
'Built-By': System.getProperty('user.name'),
'Built-JDK': System.getProperty('java.version'),
'Source-Compatibility': sourceCompatibility,
'Target-Compatibility': targetCompatibility)
}
javadoc.options {
encoding = 'UTF-8'
links 'https://docs.oracle.com/javase/8/docs/api/'

View File

@ -71,6 +71,9 @@ public abstract class AbstractChannelBuilder<BuilderT extends AbstractChannelBui
@Nullable
private ExecutorService userExecutor;
@Nullable
private String userAgent;
/**
* Provides a custom executor.
*
@ -86,6 +89,18 @@ public abstract class AbstractChannelBuilder<BuilderT extends AbstractChannelBui
return (BuilderT) this;
}
/**
* Provides a custom {@code User-Agent} for the application.
*
* <p>It's an optional parameter. If provided, the given agent will be prepended by the
* grpc {@code User-Agent}.
*/
@SuppressWarnings("unchecked")
public final BuilderT userAgent(String userAgent) {
this.userAgent = userAgent;
return (BuilderT) this;
}
/**
* Builds a channel using the given parameters.
*/
@ -101,7 +116,7 @@ public abstract class AbstractChannelBuilder<BuilderT extends AbstractChannelBui
}
final ChannelEssentials essentials = buildEssentials();
ChannelImpl channel = new ChannelImpl(essentials.transportFactory, executor);
ChannelImpl channel = new ChannelImpl(essentials.transportFactory, executor, userAgent);
channel.setTerminationRunnable(new Runnable() {
@Override
public void run() {

View File

@ -50,6 +50,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
@ -81,6 +82,8 @@ public final class ChannelImpl extends Channel {
private final ClientTransportFactory transportFactory;
private final ExecutorService executor;
private final String userAgent;
/**
* All transports that are not stopped. At the very least {@link #activeTransport} will be
* present, but previously used transports that still have streams or are stopping may also be
@ -99,9 +102,11 @@ public final class ChannelImpl extends Channel {
private boolean terminated;
private Runnable terminationRunnable;
public ChannelImpl(ClientTransportFactory transportFactory, ExecutorService executor) {
ChannelImpl(ClientTransportFactory transportFactory, ExecutorService executor,
@Nullable String userAgent) {
this.transportFactory = transportFactory;
this.executor = executor;
this.userAgent = userAgent;
}
/** Hack to allow executors to auto-shutdown. Not for general use. */
@ -334,13 +339,18 @@ public final class ChannelImpl extends Channel {
headers.put(TIMEOUT_KEY, timeoutMicros);
}
// Fill out the User-Agent header.
headers.removeAll(HttpUtil.USER_AGENT_KEY);
if (userAgent != null) {
headers.put(HttpUtil.USER_AGENT_KEY, userAgent);
}
try {
stream = transport.newStream(method, headers, listener);
} catch (IllegalStateException ex) {
// We can race with the transport and end up trying to use a terminated transport.
// TODO(ejona86): Improve the API to remove the possibility of the race.
closeCallPrematurely(listener, Status.fromThrowable(ex));
return;
}
}

View File

@ -357,27 +357,27 @@ public abstract class Metadata {
/**
* Marshaller for metadata values that are serialized into raw binary.
*/
public static interface BinaryMarshaller<T> {
public interface BinaryMarshaller<T> {
/**
* Serialize a metadata value to bytes.
* @param value to serialize
* @return serialized version of value
*/
public byte[] toBytes(T value);
byte[] toBytes(T value);
/**
* Parse a serialized metadata value from bytes.
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
public T parseBytes(byte[] serialized);
T parseBytes(byte[] serialized);
}
/**
* Marshaller for metadata values that are serialized into ASCII strings that contain only
* printable characters and space.
*/
public static interface AsciiMarshaller<T> {
public interface AsciiMarshaller<T> {
/**
* Serialize a metadata value to a ASCII string that contains only printable characters and
* space.
@ -385,14 +385,14 @@ public abstract class Metadata {
* @param value to serialize
* @return serialized version of value, or null if value cannot be transmitted.
*/
public String toAsciiString(T value);
String toAsciiString(T value);
/**
* Parse a serialized metadata value from an ASCII string.
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
public T parseAsciiString(String serialized);
T parseAsciiString(String serialized);
}
/**

View File

@ -206,7 +206,7 @@ public abstract class Http2ClientStream extends AbstractClientStream<Integer> {
return null;
}
contentTypeChecked = true;
String contentType = headers.get(HttpUtil.CONTENT_TYPE);
String contentType = headers.get(HttpUtil.CONTENT_TYPE_KEY);
if (TEMP_CHECK_CONTENT_TYPE && !HttpUtil.CONTENT_TYPE_GRPC.equalsIgnoreCase(contentType)) {
// Malformed content-type so report an error
return Status.INTERNAL.withDescription("invalid content-type " + contentType);
@ -218,7 +218,7 @@ public abstract class Http2ClientStream extends AbstractClientStream<Integer> {
* Inspect the raw metadata and figure out what charset is being used.
*/
private static Charset extractCharset(Metadata headers) {
String contentType = headers.get(HttpUtil.CONTENT_TYPE);
String contentType = headers.get(HttpUtil.CONTENT_TYPE_KEY);
if (contentType != null) {
String[] split = contentType.split("charset=");
try {

View File

@ -36,16 +36,24 @@ import io.grpc.Status;
import java.net.HttpURLConnection;
import javax.annotation.Nullable;
/**
* Constants for GRPC-over-HTTP (or HTTP/2).
*/
public final class HttpUtil {
/**
* The Content-Type header name. Defined here since it is not explicitly defined by the HTTP/2
* spec.
* {@link Metadata.Key} for the Content-Type request/response header.
*/
public static final Metadata.Key<String> CONTENT_TYPE =
Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER);
public static final Metadata.Key<String> CONTENT_TYPE_KEY =
Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER);
/**
* {@link Metadata.Key} for the Content-Type request/response header.
*/
public static final Metadata.Key<String> USER_AGENT_KEY =
Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER);
/**
* Content-Type used for GRPC-over-HTTP/2.
@ -57,12 +65,6 @@ public final class HttpUtil {
*/
public static final String HTTP_METHOD = "POST";
/**
* The TE header name. Defined here since it is not explicitly defined by the HTTP/2 spec.
*/
public static final Metadata.Key<String> TE = Metadata.Key.of("te",
Metadata.ASCII_STRING_MARSHALLER);
/**
* The TE (transport encoding) header for requests over HTTP/2.
*/
@ -136,7 +138,7 @@ public final class HttpUtil {
private final int code;
private final Status status;
private Http2Error(int code, Status status) {
Http2Error(int code, Status status) {
this.code = code;
this.status = status.augmentDescription("HTTP/2 error code: " + this.name());
}
@ -191,5 +193,23 @@ public final class HttpUtil {
}
}
/**
* Gets the User-Agent string for the gRPC transport.
*/
public static String getGrpcUserAgent(String transportName,
@Nullable String applicationUserAgent) {
StringBuilder builder = new StringBuilder("grpc-java-").append(transportName);
String version = HttpUtil.class.getPackage().getImplementationVersion();
if (version != null) {
builder.append("/");
builder.append(version);
}
if (applicationUserAgent != null) {
builder.append(' ');
builder.append(applicationUserAgent);
}
return builder.toString();
}
private HttpUtil() {}
}

View File

@ -94,7 +94,7 @@ public class ChannelImplTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
channel = new ChannelImpl(mockTransportFactory, executor);
channel = new ChannelImpl(mockTransportFactory, executor, null);
when(mockTransportFactory.newClientTransport()).thenReturn(mockTransport);
}

View File

@ -31,6 +31,8 @@
package io.grpc.transport.netty;
import static io.grpc.transport.HttpUtil.CONTENT_TYPE_KEY;
import static io.grpc.transport.HttpUtil.USER_AGENT_KEY;
import static io.netty.util.CharsetUtil.UTF_8;
import com.google.common.base.Preconditions;
@ -40,8 +42,7 @@ import io.grpc.Metadata;
import io.grpc.SharedResourceHolder.Resource;
import io.grpc.transport.HttpUtil;
import io.grpc.transport.TransportFrameUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
@ -50,7 +51,6 @@ import io.netty.util.ByteString;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -66,12 +66,13 @@ class Utils {
public static final ByteString HTTP_METHOD = new ByteString(HttpUtil.HTTP_METHOD.getBytes(UTF_8));
public static final ByteString HTTPS = new ByteString("https".getBytes(UTF_8));
public static final ByteString HTTP = new ByteString("http".getBytes(UTF_8));
public static final ByteString CONTENT_TYPE_HEADER = new ByteString(HttpUtil.CONTENT_TYPE.name()
public static final ByteString CONTENT_TYPE_HEADER = new ByteString(CONTENT_TYPE_KEY.name()
.getBytes(UTF_8));
public static final ByteString CONTENT_TYPE_GRPC = new ByteString(
HttpUtil.CONTENT_TYPE_GRPC.getBytes(UTF_8));
public static final ByteString TE_HEADER = new ByteString(HttpUtil.TE.name().getBytes(UTF_8));
public static final ByteString TE_HEADER = new ByteString("te".getBytes(UTF_8));
public static final ByteString TE_TRAILERS = new ByteString(HttpUtil.TE_TRAILERS.getBytes(UTF_8));
public static final ByteString USER_AGENT = new ByteString(USER_AGENT_KEY.name().getBytes(UTF_8));
public static final Resource<EventLoopGroup> DEFAULT_BOSS_EVENT_LOOP_GROUP =
new DefaultEventLoopGroupResource(1, "grpc-default-boss-ELG");
@ -79,15 +80,6 @@ class Utils {
public static final Resource<EventLoopGroup> DEFAULT_WORKER_EVENT_LOOP_GROUP =
new DefaultEventLoopGroupResource(0, "grpc-default-worker-ELG");
/**
* Copies the content of the given {@link ByteBuffer} to a new {@link ByteBuf} instance.
*/
static ByteBuf toByteBuf(ByteBufAllocator alloc, ByteBuffer source) {
ByteBuf buf = alloc.buffer(source.remaining());
buf.writeBytes(source);
return buf;
}
public static Metadata.Headers convertHeaders(Http2Headers http2Headers) {
Metadata.Headers headers = new Metadata.Headers(convertHeadersToArray(http2Headers));
if (http2Headers.authority() != null) {
@ -137,6 +129,10 @@ class Utils {
http2Headers.path(new ByteString(headers.getPath().getBytes(UTF_8)));
}
// Set the User-Agent header.
String userAgent = HttpUtil.getGrpcUserAgent("netty", headers.get(USER_AGENT_KEY));
http2Headers.set(USER_AGENT, new ByteString(userAgent.getBytes(UTF_8)));
return http2Headers;
}
@ -165,9 +161,11 @@ class Utils {
Http2Headers http2Headers = new DefaultHttp2Headers();
byte[][] serializedHeaders = TransportFrameUtil.toHttp2Headers(headers);
for (int i = 0; i < serializedHeaders.length; i += 2) {
http2Headers.add(new ByteString(serializedHeaders[i], false),
new ByteString(serializedHeaders[i + 1], false));
ByteString name = new ByteString(serializedHeaders[i], false);
ByteString value = new ByteString(serializedHeaders[i + 1], false);
http2Headers.add(name, value);
}
return http2Headers;
}

View File

@ -32,7 +32,9 @@
package io.grpc.transport.netty;
import static com.google.common.base.Charsets.UTF_8;
import static io.grpc.transport.HttpUtil.USER_AGENT_KEY;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.common.io.ByteStreams;
@ -48,6 +50,7 @@ import io.grpc.testing.TestUtils;
import io.grpc.transport.ClientStream;
import io.grpc.transport.ClientStreamListener;
import io.grpc.transport.ClientTransport;
import io.grpc.transport.HttpUtil;
import io.grpc.transport.ServerListener;
import io.grpc.transport.ServerStream;
import io.grpc.transport.ServerStreamListener;
@ -74,6 +77,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -115,6 +119,41 @@ public class NettyClientTransportTest {
group.shutdownGracefully(0, 10, TimeUnit.SECONDS);
}
@Test
public void headersShouldAddDefaultUserAgent() throws Exception {
startServer();
NettyClientTransport transport = newTransport(newNegotiator());
transport.start(clientTransportListener);
// Send a single RPC and wait for the response.
new Rpc(transport).halfClose().waitForResponse();
// Verify that the received headers contained the User-Agent.
assertEquals(1, serverListener.streamListeners.size());
Metadata.Headers headers = serverListener.streamListeners.get(0).headers;
assertEquals(HttpUtil.getGrpcUserAgent("netty", null), headers.get(USER_AGENT_KEY));
}
@Test
public void headersShouldOverrideDefaultUserAgent() throws Exception {
startServer();
NettyClientTransport transport = newTransport(newNegotiator());
transport.start(clientTransportListener);
// Send a single RPC and wait for the response.
String userAgent = "testUserAgent";
Metadata.Headers sentHeaders = new Metadata.Headers();
sentHeaders.put(USER_AGENT_KEY, userAgent);
new Rpc(transport, sentHeaders).halfClose().waitForResponse();
// Verify that the received headers contained the User-Agent.
assertEquals(1, serverListener.streamListeners.size());
Metadata.Headers receivedHeaders = serverListener.streamListeners.get(0).headers;
assertEquals(HttpUtil.getGrpcUserAgent("netty", userAgent),
receivedHeaders.get(USER_AGENT_KEY));
}
/**
* Verifies that we can create multiple TLS client transports from the same builder.
*/
@ -265,8 +304,44 @@ public class NettyClientTransportTest {
}
}
private static final class EchoServerStreamListener implements ServerStreamListener {
final ServerStream stream;
final String method;
final Metadata.Headers headers;
EchoServerStreamListener(ServerStream stream, String method, Metadata.Headers headers) {
this.stream = stream;
this.method = method;
this.headers = headers;
stream.request(1);
}
@Override
public void messageRead(InputStream message) {
// Just echo back the message.
stream.writeMessage(message);
stream.flush();
}
@Override
public void onReady() {
}
@Override
public void halfClosed() {
// Just close when the client closes.
stream.close(Status.OK, new Metadata.Trailers());
}
@Override
public void closed(Status status) {
}
}
private static class EchoServerListener implements ServerListener {
final List<NettyServerTransport> transports = new ArrayList<NettyServerTransport>();
final List<EchoServerStreamListener> streamListeners =
Collections.synchronizedList(new ArrayList<EchoServerStreamListener>());
@Override
public ServerTransportListener transportCreated(final ServerTransport transport) {
@ -276,30 +351,9 @@ public class NettyClientTransportTest {
@Override
public ServerStreamListener streamCreated(final ServerStream stream, String method,
Metadata.Headers headers) {
stream.request(1);
return new ServerStreamListener() {
@Override
public void messageRead(InputStream message) {
// Just echo back the message.
stream.writeMessage(message);
stream.flush();
}
@Override
public void onReady() {
}
@Override
public void halfClosed() {
// Just close when the client closes.
stream.close(Status.OK, new Metadata.Trailers());
}
@Override
public void closed(Status status) {
}
};
EchoServerStreamListener listener = new EchoServerStreamListener(stream, method, headers);
streamListeners.add(listener);
return listener;
}
@Override

View File

@ -31,6 +31,9 @@
package io.grpc.transport.okhttp;
import static io.grpc.transport.HttpUtil.CONTENT_TYPE_KEY;
import static io.grpc.transport.HttpUtil.USER_AGENT_KEY;
import com.google.common.base.Preconditions;
import com.squareup.okhttp.internal.spdy.Header;
@ -52,8 +55,8 @@ public class Headers {
public static final Header SCHEME_HEADER = new Header(Header.TARGET_SCHEME, "https");
public static final Header METHOD_HEADER = new Header(Header.TARGET_METHOD, HttpUtil.HTTP_METHOD);
public static final Header CONTENT_TYPE_HEADER =
new Header(HttpUtil.CONTENT_TYPE.name(), HttpUtil.CONTENT_TYPE_GRPC);
public static final Header TE_HEADER = new Header(HttpUtil.TE.name(), HttpUtil.TE_TRAILERS);
new Header(CONTENT_TYPE_KEY.name(), HttpUtil.CONTENT_TYPE_GRPC);
public static final Header TE_HEADER = new Header("te", HttpUtil.TE_TRAILERS);
/**
* Serializes the given headers and creates a list of OkHttp {@link Header}s to be used when
@ -76,6 +79,9 @@ public class Headers {
String path = headers.getPath() != null ? headers.getPath() : defaultPath;
okhttpHeaders.add(new Header(Header.TARGET_PATH, path));
String userAgent = HttpUtil.getGrpcUserAgent("okhttp", headers.get(USER_AGENT_KEY));
okhttpHeaders.add(new Header(HttpUtil.USER_AGENT_KEY.name(), userAgent));
// All non-pseudo headers must come after pseudo headers.
okhttpHeaders.add(CONTENT_TYPE_HEADER);
okhttpHeaders.add(TE_HEADER);
@ -84,8 +90,9 @@ public class Headers {
byte[][] serializedHeaders = TransportFrameUtil.toHttp2Headers(headers);
for (int i = 0; i < serializedHeaders.length; i += 2) {
ByteString key = ByteString.of(serializedHeaders[i]);
ByteString value = ByteString.of(serializedHeaders[i + 1]);
if (isApplicationHeader(key)) {
String keyString = key.utf8();
if (isApplicationHeader(keyString)) {
ByteString value = ByteString.of(serializedHeaders[i + 1]);
okhttpHeaders.add(new Header(key, value));
}
}
@ -97,10 +104,10 @@ public class Headers {
* Returns {@code true} if the given header is an application-provided header. Otherwise, returns
* {@code false} if the header is reserved by GRPC.
*/
private static boolean isApplicationHeader(ByteString key) {
String keyString = key.utf8();
private static boolean isApplicationHeader(String key) {
// Don't allow HTTP/2 pseudo headers or content-type to be added by the application.
return (!keyString.startsWith(":")
&& !HttpUtil.CONTENT_TYPE.name().equalsIgnoreCase(keyString));
return (!key.startsWith(":")
&& !CONTENT_TYPE_KEY.name().equalsIgnoreCase(key))
&& !USER_AGENT_KEY.name().equalsIgnoreCase(key);
}
}

View File

@ -32,6 +32,10 @@
package io.grpc.transport.okhttp;
import static com.google.common.base.Charsets.UTF_8;
import static io.grpc.transport.okhttp.Headers.CONTENT_TYPE_HEADER;
import static io.grpc.transport.okhttp.Headers.METHOD_HEADER;
import static io.grpc.transport.okhttp.Headers.SCHEME_HEADER;
import static io.grpc.transport.okhttp.Headers.TE_HEADER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -68,6 +72,7 @@ import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.transport.ClientStreamListener;
import io.grpc.transport.ClientTransport;
import io.grpc.transport.HttpUtil;
import io.grpc.transport.okhttp.OkHttpClientTransport.ClientFrameHandler;
import okio.Buffer;
@ -89,6 +94,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -115,7 +121,6 @@ public class OkHttpClientTransportTest {
private ClientTransport.Listener transportListener;
private OkHttpClientTransport clientTransport;
private MockFrameReader frameReader;
private MockSocket socket;
private Map<Integer, OkHttpClientStream> streams;
private ClientFrameHandler frameHandler;
private ExecutorService executor;
@ -127,7 +132,7 @@ public class OkHttpClientTransportTest {
MockitoAnnotations.initMocks(this);
streams = new HashMap<Integer, OkHttpClientStream>();
frameReader = new MockFrameReader();
socket = new MockSocket(frameReader);
MockSocket socket = new MockSocket(frameReader);
executor = Executors.newCachedThreadPool();
Ticker ticker = new Ticker() {
@Override
@ -206,7 +211,7 @@ public class OkHttpClientTransportTest {
public void receivedHeadersForInvalidStreamShouldKillConnection() throws Exception {
// Empty headers block without correct content type or status
frameHandler.headers(false, false, 3, 0, new ArrayList<Header>(),
HeadersMode.HTTP_20_HEADERS);
HeadersMode.HTTP_20_HEADERS);
verify(frameWriter).goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class));
verify(transportListener).transportShutdown();
verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated();
@ -270,6 +275,37 @@ public class OkHttpClientTransportTest {
listener.status.getCode());
}
@Test
public void headersShouldAddDefaultUserAgent() throws Exception {
MockStreamListener listener = new MockStreamListener();
clientTransport.newStream(method, new Metadata.Headers(), listener);
Header userAgentHeader = new Header(HttpUtil.USER_AGENT_KEY.name(),
HttpUtil.getGrpcUserAgent("okhttp", null));
List<Header> expectedHeaders = Arrays.asList(SCHEME_HEADER, METHOD_HEADER,
new Header(Header.TARGET_AUTHORITY, "notarealauthority:80"),
new Header(Header.TARGET_PATH, "/fakemethod"),
userAgentHeader, CONTENT_TYPE_HEADER, TE_HEADER);
verify(frameWriter).synStream(eq(false), eq(false), eq(3), eq(0), eq(expectedHeaders));
streams.get(3).cancel();
}
@Test
public void headersShouldOverrideDefaultUserAgent() throws Exception {
MockStreamListener listener = new MockStreamListener();
String userAgent = "fakeUserAgent";
Metadata.Headers metadata = new Metadata.Headers();
metadata.put(HttpUtil.USER_AGENT_KEY, userAgent);
clientTransport.newStream(method, metadata, listener);
List<Header> expectedHeaders = Arrays.asList(SCHEME_HEADER, METHOD_HEADER,
new Header(Header.TARGET_AUTHORITY, "notarealauthority:80"),
new Header(Header.TARGET_PATH, "/fakemethod"),
new Header(HttpUtil.USER_AGENT_KEY.name(),
HttpUtil.getGrpcUserAgent("okhttp", userAgent)),
CONTENT_TYPE_HEADER, TE_HEADER);
verify(frameWriter).synStream(eq(false), eq(false), eq(3), eq(0), eq(expectedHeaders));
streams.get(3).cancel();
}
@Test
public void writeMessage() throws Exception {
final String message = "Hello Server";
@ -1013,7 +1049,7 @@ public class OkHttpClientTransportTest {
private List<Header> grpcResponseHeaders() {
return ImmutableList.<Header>builder()
.add(new Header(":status", "200"))
.add(Headers.CONTENT_TYPE_HEADER)
.add(CONTENT_TYPE_HEADER)
.build();
}