Skip to content

Commit 23c14bc

Browse files
committed
Fix thread-safety of Headers::Normalizer cache
Move the normalizer's header name cache from a shared instance variable (@cache) to per-thread storage (Thread.current), eliminating a race condition when multiple threads share a normalizer instance.
1 parent 2b8e779 commit 23c14bc

4 files changed

Lines changed: 10 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4747

4848
### Fixed
4949

50+
- Thread-safety: `Headers::Normalizer` cache is now per-thread via
51+
`Thread.current`, eliminating a potential race condition when multiple
52+
threads share a normalizer instance
5053
- Instrumentation feature now correctly starts a new span for each retry
5154
attempt, fixing `NoMethodError` with `ActiveSupport::Notifications` when
5255
using `.retriable` with the instrumentation feature (#826)

lib/http/headers/normalizer.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,8 @@ class Normalizer
1111
# Pattern matching header name part separators (hyphens and underscores)
1212
NAME_PARTS_SEPARATOR_RE = /[-_]/
1313

14-
# Creates a new Normalizer with an empty cache
15-
#
16-
# @example
17-
# normalizer = HTTP::Headers::Normalizer.new
18-
#
19-
# @return [Normalizer]
20-
# @api public
21-
def initialize
22-
@cache = {} #: Hash[String, String]
23-
end
14+
# Thread-local cache key for normalized header names
15+
CACHE_KEY = :http_headers_normalizer_cache
2416

2517
# Normalizes a header name to canonical form
2618
#
@@ -31,7 +23,8 @@ def initialize
3123
# @api public
3224
def call(name)
3325
name = name.to_s
34-
value = (@cache[name] ||= normalize_header(name))
26+
cache = (Thread.current[CACHE_KEY] ||= {})
27+
value = (cache[name] ||= normalize_header(name))
3528

3629
value.dup
3730
end

sig/http.rbs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -248,25 +248,10 @@ module HTTP
248248

249249
class Headers
250250
class Normalizer
251-
CANONICAL_NAME_RE: Regexp
252251
COMPLIANT_NAME_RE: Regexp
253252
NAME_PARTS_SEPARATOR_RE: Regexp
253+
CACHE_KEY: Symbol
254254

255-
@cache: Cache
256-
257-
class Cache
258-
MAX_SIZE: Integer
259-
260-
@store: Hash[String, String]
261-
262-
def initialize: () -> void
263-
def get: (String key) -> String?
264-
alias [] get
265-
def set: (String key, String value) -> String
266-
alias []= set
267-
end
268-
269-
def initialize: () -> void
270255
def call: (String | Symbol name) -> String
271256

272257
private

test/http/headers/normalizer_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
cover "HTTP::Headers::Normalizer*"
77
let(:normalizer) { HTTP::Headers::Normalizer.new }
88

9+
before { Thread.current[HTTP::Headers::Normalizer::CACHE_KEY] = nil }
10+
911
describe "#call" do
1012
it "normalizes the header" do
1113
assert_equal "Content-Type", normalizer.call("content_type")

0 commit comments

Comments
 (0)