Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/http_display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,34 @@ function _body_summary_label(body, content_length::Int64)::String
return total > 0 ? string(total, "-byte body") : string(typeof(body), " body")
end

function _show_redacted_header_pair(io::IO, key::String, value::String)::Nothing
show(io, key)
print(io, " => ")
show(io, _http_render_header_value(key, value))
return nothing
end

function Base.show(io::IO, headers::Headers)
print(io, "HTTP.Headers([")
for (i, (key, value)) in enumerate(headers)
i > 1 && print(io, ", ")
_show_redacted_header_pair(io, key, value)
end
print(io, "])")
end

function Base.show(io::IO, ::MIME"text/plain", headers::Headers)
summary(io, headers)
isempty(headers) && return
print(io, ":")
key_width = maximum(length(repr(key)) for (key, _) in headers)
for (key, value) in headers
print(io, "\n ", lpad(repr(key), key_width), " => ")
show(io, _http_render_header_value(key, value))
end
return
end

@inline function _request_summary_target(request::Request)::String
request.host === nothing || return string(request.host::String, request.target)
return request.target
Expand Down
46 changes: 46 additions & 0 deletions test/http_core_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,52 @@ end
@test occursin("elapsed", msg)
end

@testset "HTTP core headers display" begin
headers = HT.Headers([
"Authorization" => "Bearer super-secret",
"Proxy-Authorization" => "Basic dXNlcjpwYXNz",
"Cookie" => "session=secret-cookie",
"Set-Cookie" => "id=secret-id",
"Content-Type" => "text/plain",
])

# Sensitive header values must be masked in both the compact (2-arg) and
# text/plain (3-arg) show forms, so they never leak into stacktraces,
# logging output, or the REPL.
compact = sprint(show, headers)
@test occursin("HTTP.Headers([", compact)
@test occursin("\"Authorization\" => \"******\"", compact)
@test occursin("\"Proxy-Authorization\" => \"******\"", compact)
@test occursin("\"Cookie\" => \"******\"", compact)
@test occursin("\"Set-Cookie\" => \"******\"", compact)
@test occursin("\"Content-Type\" => \"text/plain\"", compact)
@test !occursin("secret", compact)
@test !occursin("dXNlcjpwYXNz", compact)

plain = sprint(io -> show(io, MIME"text/plain"(), headers))
@test occursin("5-element", plain)
@test occursin("\"Authorization\" => \"******\"", plain)
@test occursin("\"Proxy-Authorization\" => \"******\"", plain)
@test occursin("\"Cookie\" => \"******\"", plain)
@test occursin("\"Set-Cookie\" => \"******\"", plain)
@test occursin("\"Content-Type\" => \"text/plain\"", plain)
@test !occursin("secret", plain)
@test !occursin("dXNlcjpwYXNz", plain)

# Redaction is case-insensitive.
lower = HT.Headers()
push!(lower.entries, "authorization" => "Bearer raw-secret")
@test occursin("\"authorization\" => \"******\"", sprint(show, lower))
@test !occursin("raw-secret", sprint(io -> show(io, MIME"text/plain"(), lower)))

# Redaction is display-only; programmatic access returns the real value.
@test HT.header(headers, "Authorization") == "Bearer super-secret"

# Empty collections render without a trailing colon or entries.
@test sprint(show, HT.Headers()) == "HTTP.Headers([])"
@test endswith(sprint(io -> show(io, MIME"text/plain"(), HT.Headers())), "0-element HTTP.Headers")
end

@testset "HTTP core request/response display" begin
request_headers = HT.Headers()
HT.setheader(request_headers, "Authorization", "Bearer super-secret")
Expand Down
Loading