Skip to content
Merged
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
13 changes: 12 additions & 1 deletion src/http1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ function _body_allowed_for_status(status::Integer)::Bool
return true
end

# RFC 9110 §8.6 forbids Content-Length on 1xx and 204 responses, but other
# bodyless responses may still advertise one: a HEAD response carries the
# header fields a GET would have produced (§9.3.2), and a 304 may repeat the
# Content-Length of the representation it revalidates (§15.4.5).
function _content_length_allowed_for_status(status::Integer)::Bool
status == 304 && return true
return _body_allowed_for_status(status)
end

function _read_exact!(io::IO, dst::Vector{UInt8}, nbytes::Integer)::Int
nbytes < 0 && throw(ArgumentError("nbytes must be >= 0"))
nbytes == 0 && return 0
Expand Down Expand Up @@ -842,7 +851,9 @@ function write_response!(io::IO, response::Response)
allows_body = status_allows_body && !response_to_head
use_chunked = allows_body && _parse_transfer_encoding!(headers, response.proto_major, response.proto_minor)
if !status_allows_body
removeheader(headers, "Content-Length")
# 1xx/204 must not advertise framing; a 304 keeps an explicitly
# provided Content-Length.
_content_length_allowed_for_status(response.status) || removeheader(headers, "Content-Length")
removeheader(headers, "Transfer-Encoding")
elseif response_to_head
removeheader(headers, "Transfer-Encoding")
Expand Down
8 changes: 6 additions & 2 deletions src/http_server_streams.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ function _write_server_stream_head!(stream::Stream)::Nothing
stream.write_mode = mode
if _server_stream_live_h2(stream)
if mode == _ServerStreamWriteMode.NONE
removeheader(headers, "Content-Length")
# Only the body is suppressed: HEAD and 304 responses keep an
# explicitly provided Content-Length describing the representation.
_content_length_allowed_for_status(response.status) || removeheader(headers, "Content-Length")
elseif response.content_length >= 0
setheader(headers, "Content-Length", string(response.content_length))
end
Expand All @@ -92,7 +94,9 @@ function _write_server_stream_head!(stream::Stream)::Nothing
return nothing
end
if mode == _ServerStreamWriteMode.NONE
removeheader(headers, "Content-Length")
# Only the body is suppressed: HEAD and 304 responses keep an
# explicitly provided Content-Length describing the representation.
_content_length_allowed_for_status(response.status) || removeheader(headers, "Content-Length")
removeheader(headers, "Transfer-Encoding")
elseif mode == _ServerStreamWriteMode.FIXED
if response.content_length >= 0
Expand Down
33 changes: 33 additions & 0 deletions test/http2_server_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,39 @@ end
end
end

@testset "HTTP/2 server stream handlers keep an explicit Content-Length for HEAD" begin
server = HT.listen!("127.0.0.1", 0; listenany = true) do stream
request = HT.startread(stream)
HT.setheader(stream, "Content-Length" => "5")
HT.setstatus(stream, 200)
if request.method == "HEAD"
HT.startwrite(stream)
else
write(stream, "hello")
end
return nothing
end
address = HT.server_addr(server)
conn = HT.connect_h2!(address; secure = false)
try
get_req = HT.Request("GET", "/sized"; host = address, body = HT.EmptyBody(), content_length = 0, proto_major = 2, proto_minor = 0)
get_res = HT.h2_roundtrip!(conn, get_req)
@test get_res.status == 200
@test HT.header(get_res.headers, "Content-Length") == "5"
@test String(_read_all_h2_server(get_res.body)) == "hello"

head_req = HT.Request("HEAD", "/sized"; host = address, body = HT.EmptyBody(), content_length = 0, proto_major = 2, proto_minor = 0)
head_res = HT.h2_roundtrip!(conn, head_req)
@test head_res.status == 200
@test HT.header(head_res.headers, "Content-Length") == "5"
@test isempty(_read_all_h2_server(head_res.body))
finally
close(conn)
HT.forceclose(server)
HTTP.@try_ignore wait(server.serve_task::Task)
end
end

@testset "HTTP/2 server stream handlers flush DATA before handler return" begin
first_written = Channel{Nothing}(1)
release = Channel{Nothing}(1)
Expand Down
58 changes: 58 additions & 0 deletions test/http_server_http1_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,45 @@ end
end
end

@testset "HTTP server stream handlers keep an explicit Content-Length for HEAD and 304" begin
server = HT.listen!("127.0.0.1", 0; listenany = true) do stream
request = HT.startread(stream)
HT.setheader(stream, "Content-Length" => "12345")
if request.target == "/nocontent"
HT.setstatus(stream, 204)
elseif request.target == "/notmodified"
HT.setstatus(stream, 304)
else
HT.setstatus(stream, 200)
end
HT.startwrite(stream)
return nothing
end
address = HT.server_addr(server)
try
head_raw = _raw_http_request(HT.port(server), "HEAD /head HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n")
@test occursin("HTTP/1.1 200 OK", head_raw)
@test occursin("Content-Length: 12345\r\n", head_raw)
head_parts = split(head_raw, "\r\n\r\n"; limit = 2)
@test length(head_parts) == 2
@test head_parts[2] == ""

not_modified_raw = _raw_http_request(HT.port(server), "GET /notmodified HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n")
@test occursin("HTTP/1.1 304 Not Modified", not_modified_raw)
@test occursin("Content-Length: 12345\r\n", not_modified_raw)
not_modified_parts = split(not_modified_raw, "\r\n\r\n"; limit = 2)
@test length(not_modified_parts) == 2
@test not_modified_parts[2] == ""

no_content_raw = _raw_http_request(HT.port(server), "GET /nocontent HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n")
@test occursin("HTTP/1.1 204 No Content", no_content_raw)
@test !occursin("content-length", lowercase(no_content_raw))
finally
_run_test_operation(() -> HT.forceclose(server))
_run_test_operation(() -> wait(server))
end
end

@testset "HTTP server timeout and handler error responses" begin
timeout_server = HT.Server(
address = "127.0.0.1:0",
Expand Down Expand Up @@ -1090,6 +1129,25 @@ end
end
end

@testset "HTTP server ordinary handlers keep an explicit Content-Length for 304" begin
server = HT.serve!("127.0.0.1", 0; listenany = true) do request
return HT.Response(304; headers = ["Content-Length" => "12345"], request = request)
end
address = HT.server_addr(server)
try
raw = _raw_http_request(HT.port(server), "GET /notmodified HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n")
@test occursin("HTTP/1.1 304 Not Modified", raw)
@test occursin("Content-Length: 12345\r\n", raw)
@test !occursin("transfer-encoding", lowercase(raw))
parts = split(raw, "\r\n\r\n"; limit = 2)
@test length(parts) == 2
@test parts[2] == ""
finally
_run_test_operation(() -> HT.forceclose(server))
_run_test_operation(() -> wait(server))
end
end

@testset "HTTP server ordinary handlers receive buffered request bodies" begin
@test HT.Server(handler = _ -> HT.Response(200), max_body_bytes = 0).max_body_bytes == 0
@test_throws ArgumentError HT.Server(handler = _ -> HT.Response(200), max_body_bytes = -1)
Expand Down
Loading