Skip to content

Commit b78b7f4

Browse files
committed
fix: validate grpc-accept-encoding header when client sends gzip request
- Add validation in Http2ClientStreamTransportState to check if server response includes grpc-accept-encoding: gzip when client sent gzip-encoded request - Log warning at FINE level when server misbehavior detected (missing or invalid header) - Add setMessageCompression(boolean, String) method to ClientStream interface and implementations to track when client sends gzip-compressed requests - Add unit tests for grpc-accept-encoding validation Fixes #1804
1 parent d51ec50 commit b78b7f4

12 files changed

Lines changed: 392 additions & 2 deletions

File tree

binder/src/main/java/io/grpc/binder/internal/MultiMessageClientStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ public final void setMessageCompression(boolean enable) {
159159
// Ignore.
160160
}
161161

162+
@Override
163+
public void setMessageCompression(boolean enabled, String compressorName) {
164+
// Ignore.
165+
}
166+
162167
@Override
163168
public void setAuthority(String authority) {
164169
// Ignore.

binder/src/main/java/io/grpc/binder/internal/SingleMessageClientStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ public final void setMessageCompression(boolean enable) {
151151
// Ignore.
152152
}
153153

154+
@Override
155+
public void setMessageCompression(boolean enabled, String compressorName) {
156+
// Ignore.
157+
}
158+
154159
@Override
155160
public void setAuthority(String authority) {
156161
// Ignore.

core/src/main/java/io/grpc/internal/AbstractClientStream.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,18 @@ private void setDecompressorRegistry(DecompressorRegistry decompressorRegistry)
261261
checkNotNull(decompressorRegistry, "decompressorRegistry");
262262
}
263263

264+
/**
265+
* Sets whether the client is sending a gzip-compressed request. This is called by
266+
* {@link ClientCallImpl#setMessageCompression(boolean)} when the compressor is gzip.
267+
* This information is used to validate the server's {@code grpc-accept-encoding} response header.
268+
*
269+
* @param enabled whether message compression is enabled
270+
* @param compressorName the name of the compressor being used (e.g., "gzip")
271+
*/
272+
public void setMessageCompression(boolean enabled, String compressorName) {
273+
// Default implementation does nothing. Override in Http2ClientStreamTransportState.
274+
}
275+
264276
@VisibleForTesting
265277
public final void setListener(ClientStreamListener listener) {
266278
checkState(this.listener == null, "Already called setListener");

core/src/main/java/io/grpc/internal/ClientCallImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,8 @@ private void sendMessageInternal(ReqT message) {
542542
@Override
543543
public void setMessageCompression(boolean enabled) {
544544
checkState(stream != null, "Not started");
545-
stream.setMessageCompression(enabled);
545+
String compressorName = callOptions.getCompressor();
546+
stream.setMessageCompression(enabled, compressorName);
546547
}
547548

548549
@Override

core/src/main/java/io/grpc/internal/ClientStream.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ public interface ClientStream extends Stream {
8888
*/
8989
void setMaxOutboundMessageSize(int maxSize);
9090

91+
/**
92+
* Sets whether the client is sending a gzip-compressed request. This is called by
93+
* {@link ClientCallImpl#setMessageCompression(boolean)} when the compressor is gzip.
94+
* This information is used to validate the server's {@code grpc-accept-encoding} response header.
95+
*
96+
* @param enabled whether message compression is enabled
97+
* @param compressorName the name of the compressor being used (e.g., "gzip")
98+
*/
99+
void setMessageCompression(boolean enabled, String compressorName);
100+
91101
/**
92102
* Sets the effective deadline of the RPC.
93103
*/

core/src/main/java/io/grpc/internal/DelayedStream.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,21 @@ public void run() {
447447
}
448448
}
449449

450+
@Override
451+
public void setMessageCompression(boolean enabled, String compressorName) {
452+
checkState(listener != null, "May only be called after start");
453+
if (passThrough) {
454+
realStream.setMessageCompression(enabled, compressorName);
455+
} else {
456+
delayOrExecute(new Runnable() {
457+
@Override
458+
public void run() {
459+
realStream.setMessageCompression(enabled, compressorName);
460+
}
461+
});
462+
}
463+
}
464+
450465
@VisibleForTesting
451466
ClientStream getRealStream() {
452467
return realStream;

core/src/main/java/io/grpc/internal/ForwardingClientStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public void setMessageCompression(boolean enable) {
6262
delegate().setMessageCompression(enable);
6363
}
6464

65+
@Override
66+
public void setMessageCompression(boolean enabled, String compressorName) {
67+
delegate().setMessageCompression(enabled, compressorName);
68+
}
69+
6570
@Override
6671
public void cancel(Status reason) {
6772
delegate().cancel(reason);

core/src/main/java/io/grpc/internal/Http2ClientStreamTransportState.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import io.grpc.Status;
2525
import java.nio.charset.Charset;
2626
import java.nio.charset.StandardCharsets;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
2729
import javax.annotation.Nullable;
2830

2931
/**
@@ -65,6 +67,14 @@ public Integer parseAsciiString(byte[] serialized) {
6567
private Charset errorCharset = StandardCharsets.UTF_8;
6668
private boolean headersReceived;
6769

70+
/**
71+
* Tracks whether the client sent a gzip-encoded request. This is set by {@link
72+
* #setMessageCompression(boolean, String)} when the compressor is gzip.
73+
*/
74+
private boolean clientSentGzipRequest = false;
75+
76+
private static final Logger log = Logger.getLogger(Http2ClientStreamTransportState.class.getName());
77+
6878
protected Http2ClientStreamTransportState(
6979
int maxMessageSize,
7080
StatsTraceContext statsTraceCtx,
@@ -73,6 +83,20 @@ protected Http2ClientStreamTransportState(
7383
super(maxMessageSize, statsTraceCtx, transportTracer, options);
7484
}
7585

86+
/**
87+
* Sets whether the client is sending a gzip-compressed request. This is called by
88+
* {@link ClientCallImpl#setMessageCompression(boolean)} when the compressor is gzip.
89+
* This information is used to validate the server's {@code grpc-accept-encoding} response header.
90+
*
91+
* @param enabled whether message compression is enabled
92+
* @param compressorName the name of the compressor being used (e.g., "gzip")
93+
*/
94+
public final void setMessageCompression(boolean enabled, String compressorName) {
95+
if (enabled && "gzip".equals(compressorName)) {
96+
clientSentGzipRequest = true;
97+
}
98+
}
99+
76100
/**
77101
* Called to process a failure in HTTP/2 processing. It should notify the transport to cancel the
78102
* stream and call {@code transportReportStatus()}.
@@ -109,6 +133,9 @@ protected void transportHeadersReceived(Metadata headers) {
109133
return;
110134
}
111135

136+
// Validate grpc-accept-encoding header if client sent gzip request
137+
validateGrpcAcceptEncoding(headers);
138+
112139
stripTransportDetails(headers);
113140
inboundHeadersReceived(headers);
114141
} finally {
@@ -257,4 +284,47 @@ private static void stripTransportDetails(Metadata metadata) {
257284
metadata.discardAll(InternalStatus.CODE_KEY);
258285
metadata.discardAll(InternalStatus.MESSAGE_KEY);
259286
}
260-
}
287+
288+
/**
289+
* Validates that the server's response includes a {@code grpc-accept-encoding} header that
290+
* includes {@code gzip} when the client sent a gzip-encoded request.
291+
*
292+
* <p>According to the gRPC spec, when a client sends a gzip-encoded request, the server must
293+
* respond with {@code grpc-accept-encoding: gzip} in the response headers to indicate it can
294+
* accept gzip-encoded responses. If this header is missing or doesn't include gzip, it's a
295+
* server misbehavior that we log at FINE level.
296+
*
297+
* @param headers the response headers from the server
298+
*/
299+
private void validateGrpcAcceptEncoding(Metadata headers) {
300+
if (!clientSentGzipRequest) {
301+
// No validation needed if client didn't send gzip
302+
return;
303+
}
304+
305+
byte[] acceptEncodingBytes = headers.get(MESSAGE_ACCEPT_ENCODING_KEY);
306+
if (acceptEncodingBytes == null) {
307+
log.log(Level.FINE,
308+
"Server sent gzip-encoded request but response missing grpc-accept-encoding header. "
309+
+ "This is server misbehavior.");
310+
return;
311+
}
312+
313+
String acceptEncoding = new String(acceptEncodingBytes, StandardCharsets.US_ASCII);
314+
// Check if gzip is in the accepted encodings (comma-separated list)
315+
String[] encodings = GrpcUtil.ACCEPT_ENCODING_SPLITTER.split(acceptEncoding);
316+
boolean gzipAccepted = false;
317+
for (String encoding : encodings) {
318+
if ("gzip".equalsIgnoreCase(encoding.trim())) {
319+
gzipAccepted = true;
320+
break;
321+
}
322+
}
323+
324+
if (!gzipAccepted) {
325+
log.log(Level.FINE,
326+
"Server sent gzip-encoded request but grpc-accept-encoding ({0}) does not include gzip. "
327+
+ "This is server misbehavior.", acceptEncoding);
328+
}
329+
}
330+
}

core/src/main/java/io/grpc/internal/NoopClientStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public void setMessageCompression(boolean enable) {
6868
// noop
6969
}
7070

71+
@Override
72+
public void setMessageCompression(boolean enabled, String compressorName) {
73+
// noop
74+
}
75+
7176
@Override
7277
public void optimizeForDirectExecutor() {}
7378

core/src/main/java/io/grpc/internal/RetriableStream.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,18 @@ public void runWith(Substream substream) {
689689
delayOrExecute(new MessageCompressionEntry());
690690
}
691691

692+
@Override
693+
public final void setMessageCompression(boolean enabled, String compressorName) {
694+
class MessageCompressionEntry implements BufferEntry {
695+
@Override
696+
public void runWith(Substream substream) {
697+
substream.stream.setMessageCompression(enabled, compressorName);
698+
}
699+
}
700+
701+
delayOrExecute(new MessageCompressionEntry());
702+
}
703+
692704
@Override
693705
public final void halfClose() {
694706
class HalfCloseEntry implements BufferEntry {

0 commit comments

Comments
 (0)