Skip to content

Commit 7ac1710

Browse files
committed
Merge remote-tracking branch 'origin/master' into fature/access-logging
2 parents 7404478 + ffeb40d commit 7ac1710

4 files changed

Lines changed: 44 additions & 13 deletions

File tree

src/extractors.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ Extracts Headers from a request and convert it into a custom struct
190190
"""
191191
function extract(param::Param{Header{T}}, request::LazyRequest) :: Header{T} where {T}
192192
headers = Types.headers(request)
193-
instance = safe_extract(param) do
194-
struct_builder(T, headers)
193+
instance = safe_extract(param) do
194+
struct_builder(T, headers; casesensitive=false)
195195
end
196196
valid_instance = try_validate(param, instance)
197197
return Header(valid_instance)

src/middleware/extract_ip.jl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,21 @@ function extract_ip(req::HTTP.Request) :: IPAddr
4848
xff :: Union{String,Nothing} = nothing
4949
xri :: Union{String,Nothing} = nothing
5050

51+
# HTTP header names are case-insensitive, and HTTP.jl canonicalizes them to
52+
# Title-Case (e.g. X-Real-IP -> X-Real-Ip), so match on the lowercased name.
5153
for (k, v) in req.headers
54+
key = lowercase(k)
5255
# Case 1: Cloudflare's direct client IP header (return early since it's priority 1)
53-
if k == "CF-Connecting-IP"
56+
if key == "cf-connecting-ip"
5457
return parse(IPAddr, v)
5558
# Case 2: Akamai/Enterprise proxies (True-Client-IP)
56-
elseif isnothing(tci) && k == "True-Client-IP"
59+
elseif isnothing(tci) && key == "true-client-ip"
5760
tci = v
5861
# Case 3: Standard X-Forwarded-For header (may be a list)
59-
elseif isnothing(xff) && k == "X-Forwarded-For"
62+
elseif isnothing(xff) && key == "x-forwarded-for"
6063
xff = v
6164
# Case 4: Nginx or other proxies (X-Real-IP)
62-
elseif isnothing(xri) && k == "X-Real-IP"
65+
elseif isnothing(xri) && key == "x-real-ip"
6366
xri = v
6467
end
6568
end

src/middleware/rate_limiter.jl

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,18 +361,21 @@ function set_rate_headers!(resp::HTTP.Response, rate_limit::Int, remaining_reque
361361
has_reset = false
362362
has_retry = false
363363

364-
# Loop over the headers once and try to find each header
364+
# Loop over the headers once and try to find each header. HTTP header names
365+
# are case-insensitive, and HTTP.jl canonicalizes them on write (e.g.
366+
# X-RateLimit-Limit -> X-Ratelimit-Limit), so match on the lowercased name.
365367
for (k, _) in resp.headers
368+
key = lowercase(k)
366369
# End if all headers are found
367370
if has_retry && has_limit && has_remaining && has_reset
368371
break
369-
elseif !has_retry && k == "Retry-After"
372+
elseif !has_retry && key == "retry-after"
370373
has_retry = true
371-
elseif !has_limit && k == "X-RateLimit-Limit"
374+
elseif !has_limit && key == "x-ratelimit-limit"
372375
has_limit = true
373-
elseif !has_remaining && k == "X-RateLimit-Remaining"
376+
elseif !has_remaining && key == "x-ratelimit-remaining"
374377
has_remaining = true
375-
elseif !has_reset && k == "X-RateLimit-Reset"
378+
elseif !has_reset && key == "x-ratelimit-reset"
376379
has_reset = true
377380
end
378381
end

src/reflection.jl

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,14 +426,39 @@ function parsetype(target_type::Type{T}, value::Any) :: T where {T}
426426
end
427427
end
428428

429+
"""
430+
match_field_names(::Type{T}, params::AbstractDict{Symbol}) where {T}
431+
432+
Return a copy of `params` whose keys are rewritten to the field names of `T`
433+
matched case-insensitively. Used for sources like HTTP headers, whose names are
434+
case-insensitive (and which HTTP.jl canonicalizes to Title-Case), so that a
435+
struct field `name` still matches an incoming `Name` header.
436+
"""
437+
function match_field_names(::Type{T}, params::AbstractDict{Symbol}) where {T}
438+
lookup = Dict(lowercase(String(k)) => v for (k, v) in params)
439+
matched = empty(params)
440+
for name in fieldnames(T)
441+
value = get(lookup, lowercase(String(name)), nothing)
442+
if !isnothing(value)
443+
matched[name] = value
444+
end
445+
end
446+
return matched
447+
end
448+
429449
"""
430450
struct_builder(::Type{T}, parameters::Dict{String,String}) where {T}
431451
432452
Constructs an object of type `T` using the parameters in the dictionary `parameters`.
453+
When `casesensitive` is false, parameter names are matched to the struct's field
454+
names ignoring case (used for case-insensitive sources like HTTP headers).
433455
"""
434-
function struct_builder(::Type{T}, params::AbstractDict) :: T where {T}
456+
function struct_builder(::Type{T}, params::AbstractDict; casesensitive::Bool=true) :: T where {T}
435457
has_kwdef = has_kwdef_constructor(T)
436-
params_with_symbols = Dict(Symbol(k) => v for (k, v) in params)
458+
params_with_symbols = Dict(Symbol(k) => v for (k, v) in params)
459+
if !casesensitive
460+
params_with_symbols = match_field_names(T, params_with_symbols)
461+
end
437462
if has_kwdef
438463
# case 1: Use slower converter to handle structs with default values
439464
return kwarg_struct_builder(T, params_with_symbols)

0 commit comments

Comments
 (0)