Skip to content

Remove redundant CanonicalizeHeaderKey from metadata iteration paths - #2443

Open
kshitijsuri90 wants to merge 2 commits into
yarpc:mainfrom
kshitijsuri90:kshitij/remove-canonicalize-metadata
Open

Remove redundant CanonicalizeHeaderKey from metadata iteration paths#2443
kshitijsuri90 wants to merge 2 commits into
yarpc:mainfrom
kshitijsuri90:kshitij/remove-canonicalize-metadata

Conversation

@kshitijsuri90

@kshitijsuri90 kshitijsuri90 commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Remove redundant transport.CanonicalizeHeaderKey calls from metadataToTransportRequest and getApplicationHeaders in transport/grpc/headers.go. gRPC metadata keys are guaranteed lowercase by HTTP/2 spec and grpc-go implementation.

Problem

Both functions call CanonicalizeHeaderKey (strings.ToLower) on every key while iterating metadata.MD, but these keys are already guaranteed lowercase — making the calls redundant CPU work.

Safety — three independent guarantees:

  1. HTTP/2 spec (RFC 7540 §8.1.2): header names MUST be lowercase
  2. grpc-go PR #4416 (v1.39): FromIncomingContext explicitly lowercases all keys — metadata: convert keys to lowercase in FromContext() grpc/grpc-go#4416
  3. yarpc-go uses grpc-go v1.67.3, well past v1.39

Benchmark

count=6, AMD EPYC 9B45:

Benchmark Before (main) After (this PR) Change
MetadataToTransportRequest 984.8 ns/op 907.7 ns/op -7.83% (p=0.002)
GetApplicationHeaders 992.0 ns/op 895.9 ns/op -9.69% (p=0.002)

Test plan

  • All gRPC header tests pass
  • Benchmarks added and run with count=6

RELEASE NOTES: N/a


Update (2026-07-26): rebased onto current main. One positional conflict in transport/grpc/headers_test.go: main's newly-added BenchmarkIsReserved and this PR's benchmarks were both appended at the same location — resolved by keeping both functions.

@kshitijsuri90
kshitijsuri90 force-pushed the kshitij/remove-canonicalize-metadata branch from bb5c41a to fc376ba Compare April 8, 2026 12:28
Comment thread transport/grpc/headers.go
headers := transport.NewHeadersWithCapacity(md.Len())
for header, values := range md {
header = transport.CanonicalizeHeaderKey(header)
// gRPC metadata keys are already lowercase.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: comment is a bit confusing for first time readers, maybe rephrase as method comment, saying, "this function assumes incoming headers are lowercase because ..."

@kshitijsuri90
kshitijsuri90 force-pushed the kshitij/remove-canonicalize-metadata branch from fc376ba to 8ef7dcc Compare July 27, 2026 08:32
@bananacocodrilo bananacocodrilo added this to the v1.89.8 milestone Aug 14, 2026
Problem:
metadataToTransportRequest and getApplicationHeaders both call
transport.CanonicalizeHeaderKey (strings.ToLower) on every key while
iterating gRPC metadata.MD. These keys are already guaranteed to be
lowercase, making the calls redundant CPU work on every inbound and
response path.

Fix:
Remove the CanonicalizeHeaderKey call from both functions. Also
normalize a test case in TestGetApplicationHeaders that used a
mixed-case metadata key via a raw metadata.MD literal, which no
longer reflects a realistic scenario.

Safety — three independent guarantees that metadata keys are lowercase:

1. HTTP/2 spec (RFC 7540 s8.1.2): header field names MUST be
   converted to lowercase prior to their encoding in HTTP/2.
   https://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2

2. grpc-go PR #4416 (merged v1.39, June 2021): FromIncomingContext
   now explicitly lowercases all keys.
   grpc/grpc-go#4416

3. yarpc-go uses grpc-go v1.67.3 (go.mod), well past v1.39.

Call sites:
- metadataToTransportRequest: called from handler.go via
  metadata.FromIncomingContext (server inbound path)
- getApplicationHeaders: called from outbound.go with response
  trailer metadata (client response path)

Impact:
Eliminates one strings.ToLower scan per metadata entry on both the
server inbound and client response paths.

Benchmark (count=6, AMD EPYC 9B45):
  MetadataToTransportRequest: 984.8 ns/op → 907.7 ns/op (-7.83%, p=0.002)
  GetApplicationHeaders:      992.0 ns/op → 895.9 ns/op (-9.69%, p=0.002)

Rebase note (2026-07-26): rebased onto current main; resolved a
positional conflict in transport/grpc/headers_test.go where main's
newly-added BenchmarkIsReserved and this PR's benchmarks were both
appended at the same spot — kept both functions.

Made-with: Cursor
@kshitijsuri90
kshitijsuri90 force-pushed the kshitij/remove-canonicalize-metadata branch from 8ef7dcc to fc90a11 Compare August 18, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants