From d2a4f1c1e02707e03523d0f62536c976934da73c Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Thu, 10 Sep 2026 14:58:43 -0600 Subject: [PATCH] Add setheader!, appendheader! and removeheader! aliases `setheader`, `appendheader` and `removeheader` all mutate their first argument but lack the conventional `!` suffix, which misleads readers into expecting copies (#1277). Add the `!` spellings as the same function objects (so extending either name extends both), keep the historical names undeprecated, mark all six `public`, and cross-link the docstrings. Closes #1277 Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 5 +++++ docs/src/api/core.md | 3 +++ src/HTTP.jl | 6 ++--- src/http_core.jl | 49 +++++++++++++++++++++++++++++++++++++++-- test/http_core_tests.jl | 25 +++++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbce7d2a..afe725271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mislabeled `TLSHandshakeError`; that type is now reserved for actual connection-setup failures. `HTTP.isrecoverable` classifies both wrappers by their underlying cause. ([#1353]) +- Added `setheader!`, `appendheader!` and `removeheader!` as the conventional + mutating-name spellings of `setheader`, `appendheader` and `removeheader`. + Each pair is the same function (extending one name extends the other), the + historical names are not deprecated, and all six are `public`. ([#1277]) ### Fixed - Restored HTTP and WebSocket server task scheduling to Julia's `:interactive` @@ -864,5 +868,6 @@ See changes for 0.9.15: this release is equivalent to 0.9.15 with [#752] reverte [#1119]: https://github.com/JuliaWeb/HTTP.jl/issues/1119 [#1126]: https://github.com/JuliaWeb/HTTP.jl/issues/1126 [#1127]: https://github.com/JuliaWeb/HTTP.jl/issues/1127 +[#1277]: https://github.com/JuliaWeb/HTTP.jl/issues/1277 [#1342]: https://github.com/JuliaWeb/HTTP.jl/issues/1342 [#1353]: https://github.com/JuliaWeb/HTTP.jl/issues/1353 diff --git a/docs/src/api/core.md b/docs/src/api/core.md index 4900d6ad2..6ab3951b5 100644 --- a/docs/src/api/core.md +++ b/docs/src/api/core.md @@ -40,9 +40,12 @@ HTTP.headers HTTP.hasheader HTTP.headercontains HTTP.setheader +HTTP.setheader! HTTP.defaultheader! HTTP.appendheader +HTTP.appendheader! HTTP.removeheader +HTTP.removeheader! HTTP.mkheaders HTTP.get_request_context HTTP.set_deadline! diff --git a/src/HTTP.jl b/src/HTTP.jl index efc4596ef..31c5ae972 100644 --- a/src/HTTP.jl +++ b/src/HTTP.jl @@ -80,14 +80,14 @@ include("http_websockets.jl") :RequestEvent, :RequestRetryError, :Response, :ResponseHeadEvent, :RetryBucket, :RetryEvent, :RetrySkippedEvent, :SSEEvent, :SSEStream, :Server, :StatusError, :Stream, :TLSHandshakeError, :TLSTransportError, - :TimeoutError, :TooManyRedirectsError, :Transport, :addtrailer, :appendheader, + :TimeoutError, :TooManyRedirectsError, :Transport, :addtrailer, :appendheader, :appendheader!, :body_close!, :body_closed, :body_read!, :cancel!, :canceled, :canonical_header_key, :close_idle_connections!, :defaultheader!, :delete, :do!, :expired, :fileserver, :forceclose, :get, :get!, :get_request_context, :hasheader, :head, :header, :headercontains, :headers, :idle_connection_count, :isaborted, :isrecoverable, :listen, :listen!, :mkheaders, :nobody, :open, :options, :patch, :peeraddr, :port, :post, - :put, :query, :read_request, :removeheader, :request, :retry_attempts, :roundtrip!, - :serve, :serve!, :servecontent, :servefile, :set_deadline!, :setheader, :setstatus, + :put, :query, :read_request, :removeheader, :removeheader!, :request, :retry_attempts, :roundtrip!, + :serve, :serve!, :servecontent, :servefile, :set_deadline!, :setheader, :setheader!, :setstatus, :sse_stream, :startwrite, :streamhandler, :trailers, :write_request!, :write_response!, )) end diff --git a/src/http_core.jl b/src/http_core.jl index 769565e7d..09a8c4808 100644 --- a/src/http_core.jl +++ b/src/http_core.jl @@ -903,7 +903,8 @@ end Replace all stored values for `key` with `value`, preserving the first matching position if the key already exists and appending it otherwise. Returns the -mutated `headers`. +mutated `headers`. [`setheader!`](@ref) is the same function under the +conventional mutating-name spelling. """ function setheader(headers::Headers, header::Pair) item = _header_pair(header.first, header.second) @@ -948,7 +949,8 @@ If the previous stored header has the same name and the key is not `Set-Cookie`, the value is merged into the previous entry with a comma (no whitespace), as permitted by RFC 9110 ยง5.3 and required by common request-signing canonicalizations. -Otherwise a new pair is appended. +Otherwise a new pair is appended. [`appendheader!`](@ref) is the same +function under the conventional mutating-name spelling. """ function appendheader(headers::Headers, header::Pair) item = _header_pair(header.first, header.second) @@ -971,6 +973,8 @@ end removeheader(headers, key) -> Headers Remove every stored header for `key` and return the mutated `headers`. +[`removeheader!`](@ref) is the same function under the conventional +mutating-name spelling. """ function removeheader(headers::Headers, key::AbstractString) canon = canonical_header_key(key) @@ -990,6 +994,47 @@ function removeheader(headers::Headers, key::AbstractString) return headers end +# Conventional mutating-name spellings (#1277). Each is the *same* function as +# the historical name (extending one extends the other), so the old names stay +# available without deprecation; new code should prefer the `!` form since +# every method mutates its first argument. +""" + setheader!(headers, key => value) -> Headers + setheader!(headers, key, value) -> Headers + setheader!(message, key => value) -> Headers + setheader!(message, key, value) -> Headers + +Replace all stored values for `key` with `value` and return the mutated +`headers`, or the mutated `message` for the `Request`/`Response` forms. This +is the same function as [`setheader`](@ref) under the conventional +mutating-name spelling. +""" +const setheader! = setheader + +""" + appendheader!(headers, key => value) -> Headers + appendheader!(headers, key, value) -> Headers + appendheader!(message, key => value) -> Headers + appendheader!(message, key, value) -> Headers + +Append a value for `key` without removing existing values and return the +mutated `headers`, or the mutated `message` for the `Request`/`Response` +forms. This is the same function as [`appendheader`](@ref) under the +conventional mutating-name spelling. +""" +const appendheader! = appendheader + +""" + removeheader!(headers, key) -> Headers + removeheader!(message, key) -> Headers + +Remove every stored header for `key` and return the mutated `headers`, or the +mutated `message` for the `Request`/`Response` forms. This is the same +function as [`removeheader`](@ref) under the conventional mutating-name +spelling. +""" +const removeheader! = removeheader + @inline function _ascii_lowercase_string(s::AbstractString)::String chars = Vector{Char}(undef, ncodeunits(s)) i = 1 diff --git a/test/http_core_tests.jl b/test/http_core_tests.jl index 360ea74df..b411f54e7 100644 --- a/test/http_core_tests.jl +++ b/test/http_core_tests.jl @@ -514,3 +514,28 @@ end @test !endswith(empty_response_plain, "\r\n") @test !endswith(empty_response_plain, "\n") end + +@testset "Mutating-name header aliases (#1277)" begin + @test HT.setheader! === HT.setheader + @test HT.appendheader! === HT.appendheader + @test HT.removeheader! === HT.removeheader + headers = HT.Headers() + @test HT.setheader!(headers, "X-Test" => "1") === headers + @test HT.appendheader!(headers, "X-Test", "2") === headers + @test HT.headers(headers, "X-Test") == ["1,2"] + @test HT.setheader!(headers, "X-Other", "a") === headers + @test HT.removeheader!(headers, "x-test") === headers + @test !HT.hasheader(headers, "X-Test") + @test HT.header(headers, "X-Other") == "a" + request = HT.Request("GET", "/"; host = "example.test", body = HT.EmptyBody(), content_length = 0) + @test HT.setheader!(request, "X-Req" => "1") === request + @test HT.appendheader!(request, "X-Req", "2") === request + @test HT.header(request, "X-Req") == "1,2" + @test HT.removeheader!(request, "X-Req") === request + @test !HT.hasheader(request.headers, "X-Req") + if isdefined(Base, :ispublic) + @test Base.ispublic(HTTP, :setheader!) + @test Base.ispublic(HTTP, :appendheader!) + @test Base.ispublic(HTTP, :removeheader!) + end +end