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
21 changes: 21 additions & 0 deletions src/http2_server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,27 @@ function _write_response_body_h2_server!(
)::Nothing
body = response.body
body isa EmptyBody && return nothing
if body isa BytesBody
bytes_body = body::BytesBody
try
if body_closed(bytes_body)
end_stream && _write_frame_h2_server_threadsafe!(write_lock, conn, DataFrame(stream_id, true, UInt8[]), write_deadline_ns)
return nothing
end
first = bytes_body.next_index
last = length(bytes_body.data)
if first <= last
data = first == 1 ? bytes_body.data : @view(bytes_body.data[first:last])
_write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, data; end_stream=end_stream, write_deadline_ns=write_deadline_ns)
bytes_body.next_index = last + 1
elseif end_stream
_write_frame_h2_server_threadsafe!(write_lock, conn, DataFrame(stream_id, true, UInt8[]), write_deadline_ns)
end
finally
body_close!(bytes_body)
end
return nothing
end
if body isa AbstractString
# Zero-copy fast path: alias the String's codeunits (immutable) instead
# of allocating a fresh Vector{UInt8} of the same length.
Expand Down
101 changes: 101 additions & 0 deletions test/http2_server_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,107 @@ end
end
end

@testset "HTTP/2 server writes unread BytesBody response bytes directly" begin
payload = collect(codeunits("abcdef"))
returned_body = Ref{Union{Nothing,HT.BytesBody}}(nothing)
server = HT.serve!("127.0.0.1", 0; listenany = true) do request
_ = request
body = HT.BytesBody(copy(payload))
scratch = Vector{UInt8}(undef, 2)
@test HT.body_read!(body, scratch) == 2
@test scratch == UInt8[0x61, 0x62]
returned_body[] = body
return HT.Response(200, body; content_length = 4, proto_major = 2, proto_minor = 0)
end
address = HT.server_addr(server)
conn = HT.connect_h2!(address; secure = false)
try
req = HT.Request("GET", "/bytesbody"; host = address, body = HT.EmptyBody(), content_length = 0, proto_major = 2, proto_minor = 0)
res = HT.h2_roundtrip!(conn, req)
@test res.status == 200
@test String(_read_all_h2_server(res.body)) == "cdef"
@test returned_body[] !== nothing
returned = returned_body[]::HT.BytesBody
@test HT.body_closed(returned)
@test HT.body_read!(returned, Vector{UInt8}(undef, 1)) == 0
finally
close(conn)
HT.forceclose(server)
_ = timedwait(() -> istaskdone(server.serve_task::Task), 3.0; pollint = 0.001)
end
end

@testset "HTTP/2 server handles empty and closed BytesBody responses" begin
closed_body = Ref{Union{Nothing,HT.BytesBody}}(nothing)
empty_body = Ref{Union{Nothing,HT.BytesBody}}(nothing)
server = HT.serve!("127.0.0.1", 0; listenany = true) do request
if request.target == "/closed"
body = HT.BytesBody(UInt8[0x78])
HT.body_close!(body)
closed_body[] = body
return HT.Response(200, body; content_length = 0, proto_major = 2, proto_minor = 0)
end
body = HT.BytesBody(UInt8[])
empty_body[] = body
return HT.Response(200, body; content_length = 0, proto_major = 2, proto_minor = 0)
end
address = HT.server_addr(server)
conn = HT.connect_h2!(address; secure = false)
try
for target in ("/closed", "/empty")
req = HT.Request("GET", target; host = address, body = HT.EmptyBody(), content_length = 0, proto_major = 2, proto_minor = 0)
res = HT.h2_roundtrip!(conn, req)
@test res.status == 200
@test isempty(_read_all_h2_server(res.body))
end
@test closed_body[] !== nothing
@test empty_body[] !== nothing
@test HT.body_closed(closed_body[]::HT.BytesBody)
@test HT.body_closed(empty_body[]::HT.BytesBody)
finally
close(conn)
HT.forceclose(server)
_ = timedwait(() -> istaskdone(server.serve_task::Task), 3.0; pollint = 0.001)
end
end

@testset "HTTP/2 server writes vector responses with trailers" begin
server = HT.serve!("127.0.0.1", 0; listenany = true) do request
_ = request
trailers = HT.Headers()
HT.setheader(trailers, "X-Trailer", "done")
return HT.Response(200, UInt8[0x6f, 0x6b]; trailers = trailers, content_length = 2, proto_major = 2, proto_minor = 0)
end
address = HT.server_addr(server)
conn, reader = _open_raw_h2_server_conn(address)
encoder = HT.Encoder()
decoder = HT.Decoder()
try
_write_h2_server_request_headers!(conn, encoder, UInt32(1), address, "/vector-trailers")
headers_frame, header_block, _ = _read_h2_server_header_block!(conn, reader)
decoded_headers = HT.decode_header_block(decoder, header_block)
@test any(field -> field.name == ":status" && field.value == "200", decoded_headers)
@test !headers_frame.end_stream

data_frame = _read_h2_server_frame!(conn, reader)
while data_frame isa HT.WindowUpdateFrame || data_frame isa HT.SettingsFrame || data_frame isa HT.PingFrame
data_frame = _read_h2_server_frame!(conn, reader)
end
@test data_frame isa HT.DataFrame
@test String((data_frame::HT.DataFrame).data) == "ok"
@test !(data_frame::HT.DataFrame).end_stream

trailer_frame, trailer_block, _ = _read_h2_server_header_block!(conn, reader)
decoded_trailers = HT.decode_header_block(decoder, trailer_block)
@test any(field -> field.name == "x-trailer" && field.value == "done", decoded_trailers)
@test trailer_frame.end_stream
finally
HTTP.@try_ignore NC.close(conn)
HT.forceclose(server)
_ = timedwait(() -> istaskdone(server.serve_task::Task), 3.0; pollint = 0.001)
end
end

@testset "HTTP/2 server servecontent supports ranges and conditionals" begin
payload = collect(codeunits("abcdef"))
modtime = Dates.DateTime(2024, 1, 2, 3, 4, 5)
Expand Down
6 changes: 3 additions & 3 deletions test/http_server_http1_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -649,17 +649,17 @@ end
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"; settle_s = 0.3)
head_raw = _raw_http_request(HT.port(server), "HEAD /head HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n"; settle_s = 0.3, wait_for_first_byte = true)
@test occursin("HTTP/1.1 200 OK", head_raw)
@test !occursin("oops", head_raw)
@test !occursin("transfer-encoding: chunked", lowercase(head_raw))

no_content_raw = _raw_http_request(HT.port(server), "GET /nocontent HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n"; settle_s = 0.3)
no_content_raw = _raw_http_request(HT.port(server), "GET /nocontent HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n"; settle_s = 0.3, wait_for_first_byte = true)
@test occursin("HTTP/1.1 204 No Content", no_content_raw)
@test !occursin("oops", no_content_raw)
@test !occursin("transfer-encoding: chunked", lowercase(no_content_raw))

not_modified_raw = _raw_http_request(HT.port(server), "GET /notmodified HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n"; settle_s = 0.3)
not_modified_raw = _raw_http_request(HT.port(server), "GET /notmodified HTTP/1.1\r\nHost: $(address)\r\nConnection: close\r\n\r\n"; settle_s = 0.3, wait_for_first_byte = true)
@test occursin("HTTP/1.1 304 Not Modified", not_modified_raw)
@test !occursin("oops", not_modified_raw)
@test !occursin("transfer-encoding: chunked", lowercase(not_modified_raw))
Expand Down
9 changes: 8 additions & 1 deletion test/trim_compile_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ const _TRIM_SUPPORTED = VERSION >= v"1.12.0-rc1"
const _JULIAC_ENTRYPOINT_EXPR = "using JuliaC; if isdefined(JuliaC, :main); JuliaC.main(ARGS); else JuliaC._main_cli(ARGS); end"

function _trim_compile_timeout_s()::Float64
default = Sys.iswindows() ? "1200.0" : "120.0"
default = if Sys.iswindows()
"1200.0"
elseif (VERSION.major, VERSION.minor) >= (1, 13)
# Julia pre can spend most of the old budget precompiling before JuliaC compiles.
"240.0"
else
"120.0"
end
return parse(Float64, get(ENV, "HTTP_TRIM_COMPILE_TIMEOUT_S", default))
end

Expand Down
Loading