diff --git a/src/http2_server.jl b/src/http2_server.jl index 92e0074da..e388778b9 100644 --- a/src/http2_server.jl +++ b/src/http2_server.jl @@ -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. diff --git a/test/http2_server_tests.jl b/test/http2_server_tests.jl index be9bebccf..f2ba86082 100644 --- a/test/http2_server_tests.jl +++ b/test/http2_server_tests.jl @@ -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) diff --git a/test/http_server_http1_tests.jl b/test/http_server_http1_tests.jl index b83495664..4330814f0 100644 --- a/test/http_server_http1_tests.jl +++ b/test/http_server_http1_tests.jl @@ -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)) diff --git a/test/trim_compile_tests.jl b/test/trim_compile_tests.jl index 20a9fd4a3..0d237b17c 100644 --- a/test/trim_compile_tests.jl +++ b/test/trim_compile_tests.jl @@ -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