Skip to content

Commit 7a9a28c

Browse files
committed
okhttp: optimize HPACK to index :path and fix dynamic table eviction bugs (grpc#12819)
Allow the :path pseudo-header to be added to the HPACK dynamic table in the gRPC-Java OkHttp transport, resolving the original goal of grpc#12819 and redoing reverted commit 397d3e7 (grpc#12799, grpc#12820). Background & Root Cause: In gRPC over HTTP/2, :path headers represent static service/method names (e.g., /package.Service/Method), which are constant across RPC calls. Previously, indexing :path was reverted (grpc#12820) because high dynamic table churn uncovered latent bugs in legacy OkHttp Hpack.java: 1. Off-by-one NPE loop bound in evictToRecoverBytes: When bytesToRecover exceeded total dynamic table size, the loop condition (j >= nextDynamicTableIndex) inspected the unallocated nextDynamicTableIndex slot containing null, throwing a NullPointerException. Fixed by changing the loop bound to (j > nextDynamicTableIndex). 2. Reference leak post-eviction: System.arraycopy shifted active entries rightward, but left stale Header/ByteString references in vacated array slots. Fixed by setting vacated array slots to null via Arrays.fill(). 3. Case sensitivity normalization: Header entries inserted into dynamicTable are now guaranteed to store lowercased name keys, preventing failed dynamic table header lookups. 4. Table Size Setting Handling: Works in conjunction with grpc#12818 (SETTINGS_HEADER_TABLE_SIZE handling). Fixes grpc#12819 Related: grpc#12799, grpc#12818, grpc#12820, b/514688016
1 parent bc01994 commit 7a9a28c

2 files changed

Lines changed: 39 additions & 14 deletions

File tree

  • okhttp/third_party/okhttp

okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Hpack.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,15 @@ private int evictToRecoverBytes(int bytesToRecover) {
189189
int entriesToEvict = 0;
190190
if (bytesToRecover > 0) {
191191
// determine how many headers need to be evicted.
192-
for (int j = dynamicTable.length - 1; j >= nextDynamicTableIndex && bytesToRecover > 0; j--) {
192+
for (int j = dynamicTable.length - 1; j > nextDynamicTableIndex && bytesToRecover > 0; j--) {
193193
bytesToRecover -= dynamicTable[j].hpackSize;
194194
dynamicTableByteCount -= dynamicTable[j].hpackSize;
195195
dynamicTableHeaderCount--;
196196
entriesToEvict++;
197197
}
198198
System.arraycopy(dynamicTable, nextDynamicTableIndex + 1, dynamicTable,
199199
nextDynamicTableIndex + 1 + entriesToEvict, dynamicTableHeaderCount);
200+
Arrays.fill(dynamicTable, nextDynamicTableIndex + 1, nextDynamicTableIndex + 1 + entriesToEvict, null);
200201
nextDynamicTableIndex += entriesToEvict;
201202
}
202203
return entriesToEvict;
@@ -489,17 +490,21 @@ void writeHeaders(List<io.grpc.okhttp.internal.framed.Header> headerBlock) throw
489490
out.writeByte(0x40);
490491
writeByteString(name);
491492
writeByteString(value);
492-
insertIntoDynamicTable(header);
493-
} else if (name.startsWith(PSEUDO_PREFIX) && !io.grpc.okhttp.internal.framed.Header.TARGET_AUTHORITY.equals(name)) {
494-
// Follow Chromes lead - only include the :authority pseudo header, but exclude all other
495-
// pseudo headers. Literal Header Field without Indexing - Indexed Name.
493+
insertIntoDynamicTable(new io.grpc.okhttp.internal.framed.Header(name, value));
494+
} else if (name.startsWith(PSEUDO_PREFIX)
495+
&& !io.grpc.okhttp.internal.framed.Header.TARGET_AUTHORITY.equals(name)
496+
&& !io.grpc.okhttp.internal.framed.Header.TARGET_PATH.equals(name)) {
497+
// Allow :authority and :path pseudo headers to be indexed. Other pseudo headers are not
498+
// indexed.
499+
// This is a departure from original Chrome behavior, as gRPC paths (ServiceName/MethodName)
500+
// are stable and benefit from indexing.
496501
writeInt(headerNameIndex, PREFIX_4_BITS, 0);
497502
writeByteString(value);
498503
} else {
499504
// Literal Header Field with Incremental Indexing - Indexed Name.
500505
writeInt(headerNameIndex, PREFIX_6_BITS, 0x40);
501506
writeByteString(value);
502-
insertIntoDynamicTable(header);
507+
insertIntoDynamicTable(new io.grpc.okhttp.internal.framed.Header(name, value));
503508
}
504509
}
505510
}
@@ -557,14 +562,15 @@ private int evictToRecoverBytes(int bytesToRecover) {
557562
int entriesToEvict = 0;
558563
if (bytesToRecover > 0) {
559564
// determine how many headers need to be evicted.
560-
for (int j = dynamicTable.length - 1; j >= nextDynamicTableIndex && bytesToRecover > 0; j--) {
565+
for (int j = dynamicTable.length - 1; j > nextDynamicTableIndex && bytesToRecover > 0; j--) {
561566
bytesToRecover -= dynamicTable[j].hpackSize;
562567
dynamicTableByteCount -= dynamicTable[j].hpackSize;
563568
dynamicTableHeaderCount--;
564569
entriesToEvict++;
565570
}
566571
System.arraycopy(dynamicTable, nextDynamicTableIndex + 1, dynamicTable,
567572
nextDynamicTableIndex + 1 + entriesToEvict, dynamicTableHeaderCount);
573+
Arrays.fill(dynamicTable, nextDynamicTableIndex + 1, nextDynamicTableIndex + 1 + entriesToEvict, null);
568574
nextDynamicTableIndex += entriesToEvict;
569575
}
570576
return entriesToEvict;

okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/HpackTest.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,22 +1108,41 @@ public void doNotIndexPseudoHeaders() throws IOException {
11081108
hpackWriter.writeHeaders(headerEntries(":method", "PUT"));
11091109
assertBytes(0x02, 3, 'P', 'U', 'T');
11101110
assertEquals(0, hpackWriter.dynamicTableHeaderCount);
1111-
1112-
hpackWriter.writeHeaders(headerEntries(":path", "/okhttp"));
1113-
assertBytes(0x04, 7, '/', 'o', 'k', 'h', 't', 't', 'p');
1114-
assertEquals(0, hpackWriter.dynamicTableHeaderCount);
11151111
}
11161112

11171113
@Test
1118-
public void incrementalIndexingWithAuthorityPseudoHeader() throws IOException {
1119-
hpackWriter.writeHeaders(headerEntries(":authority", "foo.com"));
1120-
assertBytes(0x41, 7, 'f', 'o', 'o', '.', 'c', 'o', 'm');
1114+
public void pseudoHeaderIndexingForPathAndAuthority() throws IOException {
1115+
// :path should now be indexed
1116+
hpackWriter.writeHeaders(headerEntries(":path", "/okhttp"));
1117+
assertBytes(0x44, 7, '/', 'o', 'k', 'h', 't', 't', 'p');
1118+
assertEquals(1, hpackWriter.dynamicTableHeaderCount);
1119+
// Second call to same :path should be an index reference byte (0xbe)
1120+
hpackWriter.writeHeaders(headerEntries(":path", "/okhttp"));
1121+
assertBytes(0xbe);
11211122
assertEquals(1, hpackWriter.dynamicTableHeaderCount);
11221123

1124+
// :authority should be indexed
1125+
hpackWriter.writeHeaders(headerEntries(":authority", "foo.com"));
1126+
assertBytes(0x41, 7, 'f', 'o', 'o', '.', 'c', 'o', 'm');
1127+
assertEquals(2, hpackWriter.dynamicTableHeaderCount);
1128+
// Second call to same :authority should be an index reference byte (0xbe)
11231129
hpackWriter.writeHeaders(headerEntries(":authority", "foo.com"));
11241130
assertBytes(0xbe);
1131+
assertEquals(2, hpackWriter.dynamicTableHeaderCount);
1132+
}
1133+
1134+
@Test
1135+
public void evictToRecoverBytesDoesNotNpeWhenBytesToRecoverExceedsTable() throws IOException {
1136+
hpackWriter.writeHeaders(headerEntries("custom-key", "custom-value"));
11251137
assertEquals(1, hpackWriter.dynamicTableHeaderCount);
11261138

1139+
// Force resize to table size 0, requiring total eviction exceeding table capacity
1140+
hpackWriter.resizeHeaderTable(0);
1141+
1142+
assertEquals(0, hpackWriter.dynamicTableHeaderCount);
1143+
assertEquals(0, hpackWriter.dynamicTableByteCount);
1144+
}
1145+
11271146
// If the :authority header somehow changes, it should be re-added to the dynamic table.
11281147
hpackWriter.writeHeaders(headerEntries(":authority", "bar.com"));
11291148
assertBytes(0x41, 7, 'b', 'a', 'r', '.', 'c', 'o', 'm');

0 commit comments

Comments
 (0)