diff --git a/src/http2_server.jl b/src/http2_server.jl index 829d8d57d..9af1bbc67 100644 --- a/src/http2_server.jl +++ b/src/http2_server.jl @@ -87,16 +87,6 @@ end return ProtocolError("HTTP/2 connection is closed") end -mutable struct _H2ServerConnControl - @atomic shutdown_requested::Bool - @atomic goaway_sent::Bool - @atomic graceful_last_stream_id::UInt32 -end - -function _H2ServerConnControl() - return _H2ServerConnControl(false, false, UInt32(0)) -end - mutable struct _H2ServerBody <: AbstractBody conn::Union{TCP.Conn,TLS.Conn} write_lock::ReentrantLock @@ -1743,7 +1733,7 @@ function _serve_h2_conn!(server::Server, tracked::_ServerConn, reader_source)::N _write_frame_h2_server_threadsafe!(write_lock, conn, WindowUpdateFrame(UInt32(0), UInt32(server.http2_settings.connection_window_size - _H2_DEFAULT_WINDOW_SIZE)), _server_write_deadline_ns(server)) end _clear_deadlines!(conn) - _set_conn_shutdown_hook!(tracked, () -> _request_h2_conn_shutdown!(conn, write_lock, conn_control)) + _set_conn_shutdown_hook!(tracked, _H2ShutdownHook(write_lock, conn_control)) _set_conn_state!(tracked, _ConnState.IDLE) while true _server_shutting_down(server) && return nothing diff --git a/src/http_core.jl b/src/http_core.jl index f10900a2d..c18894efd 100644 --- a/src/http_core.jl +++ b/src/http_core.jl @@ -2010,10 +2010,15 @@ struct _IncomingResponse{B<:AbstractBody} rawbody::B end +# Default for `Response`'s `body` keyword. A singleton (rather than the +# `nobody` vector) lets inference fold the "no keyword body" check, so a +# response built from a positional body has one concrete type. +struct _NoBodyKeyword end + function Response( status::Integer, response_body::B=EmptyBody(); - body=nobody, + body=_NoBodyKeyword(), reason::AbstractString="", headers=Headers(), trailers=Headers(), @@ -2026,9 +2031,10 @@ function Response( previous::Union{Nothing,Response}=nothing, redirect_count::Integer=0, ) where {B} - actual_body = body === nobody ? _response_body_arg(response_body) : _response_body_arg(body) + positional = body isa _NoBodyKeyword || body === nobody + actual_body = positional ? _response_body_arg(response_body) : _response_body_arg(body) actual_content_length = if content_length < 0 - body === nobody ? _compat_body_length(response_body) : _compat_body_length(body) + positional ? _compat_body_length(response_body) : _compat_body_length(body) else Int64(content_length) end diff --git a/src/http_server.jl b/src/http_server.jl index 78c81e3b1..93f528c88 100644 --- a/src/http_server.jl +++ b/src/http_server.jl @@ -30,10 +30,28 @@ end IDENTITY = 4 end +mutable struct _H2ServerConnControl + @atomic shutdown_requested::Bool + @atomic goaway_sent::Bool + @atomic graceful_last_stream_id::UInt32 +end + +function _H2ServerConnControl() + return _H2ServerConnControl(false, false, UInt32(0)) +end + +# HTTP/2 is the only protocol with a shutdown notification. Store its concrete +# state rather than a closure, so graceful shutdown is statically dispatched +# under juliac --trim. +struct _H2ShutdownHook + write_lock::ReentrantLock + control::_H2ServerConnControl +end + mutable struct _ServerConn conn::Union{TCP.Conn,TLS.Conn} lock::ReentrantLock - shutdown_hook::Union{Nothing,Function} + shutdown_hook::Union{Nothing,_H2ShutdownHook} @atomic state::_ConnState.T @atomic state_unix_sec::Int64 end @@ -425,7 +443,7 @@ end return nothing end -function _set_conn_shutdown_hook!(conn::_ServerConn, hook::Union{Nothing,Function})::Nothing +function _set_conn_shutdown_hook!(conn::_ServerConn, hook::Union{Nothing,_H2ShutdownHook})::Nothing lock(conn.lock) try conn.shutdown_hook = hook @@ -444,7 +462,7 @@ function _notify_conn_shutdown!(conn::_ServerConn)::Nothing unlock(conn.lock) end hook === nothing && return nothing - hook() + _request_h2_conn_shutdown!(conn.conn, hook.write_lock, hook.control) return nothing end diff --git a/src/http_server_files.jl b/src/http_server_files.jl index d8139cd57..7a157a34f 100644 --- a/src/http_server_files.jl +++ b/src/http_server_files.jl @@ -388,7 +388,9 @@ function servecontent( etag::Union{Nothing,AbstractString}=nothing, headers=Headers(), allow_ranges::Bool=true, -)::Response +) + # No `::Response` return annotation: it widens every caller to the abstract + # type, which juliac --trim cannot dispatch on when writing the response. response_headers = copy(mkheaders(headers)) modtime_value = modtime === nothing ? nothing : _servecontent_modtime(modtime::DateTime) etag === nothing || setheader(response_headers, "ETag", String(etag)) diff --git a/test/http_trim_client_server.jl b/test/http_trim_client_server.jl index 629b58606..77a0c1a87 100644 --- a/test/http_trim_client_server.jl +++ b/test/http_trim_client_server.jl @@ -19,7 +19,8 @@ function run_http_trim_client_server()::Nothing startswith(response, "HTTP/1.1 200 OK\r\n") || error("expected 200 response, got $(repr(response))") occursin("\r\n\r\nhello", response) || error("unexpected response body: $(repr(response))") finally - server === nothing || trim_close_http_server(server::HT.Server) + # graceful close: also notifies open HTTP/2 connections to shut down + server === nothing || close(server::HT.Server) yield() GC.gc() HTTP.@try_ignore Reseau.IOPoll.shutdown!() diff --git a/test/http_trim_open_fileserver.jl b/test/http_trim_open_fileserver.jl index ab37ad828..360bc30ca 100644 --- a/test/http_trim_open_fileserver.jl +++ b/test/http_trim_open_fileserver.jl @@ -32,6 +32,12 @@ function run_http_trim_open_fileserver()::Nothing sniffed = HT.servecontent(HT.Request("GET", "/"), collect(codeunits("
hi
"))) HT.header(sniffed, "Content-Type") == "text/html; charset=utf-8" || error("unexpected sniffed content type") HT.sniff("{\"ok\":true}") == "application/json; charset=utf-8" || error("unexpected sniffed JSON content type") + + # servecontent returns concrete response types, so callers can read the + # body (here a byte range) without dynamic dispatch. + ranged = HT.servecontent(HT.Request("GET", "/", ["Range" => "bytes=0-3"]), collect(codeunits(""))) + ranged.status == 206 || error("expected range response status 206") + trim_body_string(ranged.body) == "