Skip to content
Open
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
Expand All @@ -20,6 +21,7 @@ Zlib_jll = "83775a58-1f1d-513f-b197-d71354ab007a"
CodecZlib = "0.7"
EnumX = "1"
PrecompileTools = "1.2.1"
Preferences = "1"
Reseau = "1.3"
URIs = "1.6.1"
Zlib_jll = "1.2.12"
Expand Down
1 change: 1 addition & 0 deletions src/HTTP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ See the migration guide for the most important 1.x to 2.0 API changes.
module HTTP

using URIs
using Preferences

const VERSION = v"2.0.0"

Expand Down
25 changes: 12 additions & 13 deletions src/http1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -776,15 +776,11 @@ function _write_request_head!(
return use_chunked, trailer_values
end

# @nospecialize + concrete-first isa chain: called with widened responses from the
# server write path; abstract-narrowed isempty calls are dynamic under `juliac --trim`
function _response_has_body(response::Response)::Bool
_body_allowed_for_status(response.status) || return false
response.body === nothing && return false
response.body isa EmptyBody && return false
if response.body isa AbstractString
isempty(response.body::AbstractString) && return false
elseif response.body isa AbstractVector{UInt8}
isempty(response.body::AbstractVector{UInt8}) && return false
end
_with_body_narrowed(_response_body_known_empty, response.body) && return false
response.content_length == 0 && return false
return true
end
Expand Down Expand Up @@ -833,6 +829,8 @@ Body suppression rules for status codes like `1xx`, `204`, and `304` are
enforced here so callers can hand the function a regular `Response` object and
let the serializer apply wire-level HTTP/1 rules.
"""
# @nospecialize: compiled once for any Response{B}; the body write below dispatches
# through an explicit isa chain (see _write_all_response! for why)
function write_response!(io::IO, response::Response)
headers = copy(response.headers)
response_close = response.close || _should_close_connection(headers, response.proto_major, response.proto_minor)
Expand Down Expand Up @@ -878,15 +876,16 @@ function write_response!(io::IO, response::Response)
write(io, take!(head_buf))
allows_body || return nothing
if use_chunked
_write_chunked_body!(io, response.body, trailer_values)
_with_body_narrowed(b -> _write_chunked_body!(io, b, trailer_values), response.body)
return nothing
end
response.content_length < 0 && return nothing
body = response.body
if body isa BytesBody
_write_exact_bytes_body!(io, body::BytesBody, response.content_length)
else
_write_exact_body!(io, body, response.content_length)
_with_body_narrowed(response.body) do b
if b isa BytesBody
_write_exact_bytes_body!(io, b, response.content_length)
else
_write_exact_body!(io, b, response.content_length)
end
end
return nothing
end
Expand Down
142 changes: 86 additions & 56 deletions src/http2_server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,7 @@ 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
# _H2ServerConnControl is defined in http_server.jl (hoisted for the typed shutdown hook)

mutable struct _H2ServerBody <: AbstractBody
conn::Union{TCP.Conn,TLS.Conn}
Expand Down Expand Up @@ -1120,49 +1112,58 @@ function _write_response_body_h2_server!(
end_stream::Bool=true,
write_deadline_ns::Int64=Int64(0),
)::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)
# `_with_body_narrowed` hands each shape method a concretely-typed body, so the
# per-shape dispatch below is static (trim-resolvable); the shapes themselves are
# small methods rather than one isa ladder.
_with_body_narrowed(b -> _write_h2_body_shape!(conn, write_lock, send_state, stream_id, b, end_stream, write_deadline_ns), response.body)
return nothing
end

_write_h2_body_shape!(conn, write_lock, send_state, stream_id, ::Union{EmptyBody,Nothing}, end_stream::Bool, write_deadline_ns::Int64) = nothing

function _write_h2_body_shape!(conn, write_lock, send_state, stream_id, bytes_body::BytesBody, end_stream::Bool, write_deadline_ns::Int64)
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
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.
s = String(body)
ncodeunits(s) == 0 && return nothing
_write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, codeunits(s); end_stream=end_stream, write_deadline_ns=write_deadline_ns)
return nothing
end
if body isa AbstractVector{UInt8}
bytes = body isa Vector{UInt8} ? body : Vector{UInt8}(body)
isempty(bytes) || _write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, bytes; end_stream=end_stream, write_deadline_ns=write_deadline_ns)
return nothing
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
body isa AbstractBody || throw(ProtocolError("unsupported HTTP/2 response body type $(typeof(body))"))
return nothing
end

function _write_h2_body_shape!(conn, write_lock, send_state, stream_id, body::AbstractString, end_stream::Bool, write_deadline_ns::Int64)
# Zero-copy fast path for String: alias the codeunits (immutable) instead of
# allocating a fresh Vector{UInt8} of the same length.
s = body isa String ? body : String(body)
ncodeunits(s) == 0 && return nothing
_write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, codeunits(s); end_stream=end_stream, write_deadline_ns=write_deadline_ns)
return nothing
end

function _write_h2_body_shape!(conn, write_lock, send_state, stream_id, body::AbstractVector{UInt8}, end_stream::Bool, write_deadline_ns::Int64)
bytes = body isa Vector{UInt8} ? body : Vector{UInt8}(body)
isempty(bytes) || _write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, bytes; end_stream=end_stream, write_deadline_ns=write_deadline_ns)
return nothing
end

function _write_h2_body_shape!(conn, write_lock, send_state, stream_id, body::AbstractBody, end_stream::Bool, write_deadline_ns::Int64)
buf = Vector{UInt8}(undef, 16 * 1024)
pending = UInt8[]
have_pending = false
try
while true
n = body_read!(body::AbstractBody, buf)
n = body_read!(body, buf)
if n == 0
if have_pending
_write_data_frames_h2_server!(conn, write_lock, send_state, stream_id, pending; end_stream=end_stream, write_deadline_ns=write_deadline_ns)
Expand All @@ -1181,11 +1182,14 @@ function _write_response_body_h2_server!(
end
finally
@try_ignore begin
body_close!(body::AbstractBody)
body_close!(body)
end
end
end

_write_h2_body_shape!(conn, write_lock, send_state, stream_id, @nospecialize(body), end_stream::Bool, write_deadline_ns::Int64) =
throw(ProtocolError("unsupported HTTP/2 response body type"))

"""
_encode_h2_headers_frame_bytes!(send_state, write_lock, stream_id, status,
headers, end_stream, max_frame_size,
Expand Down Expand Up @@ -1214,6 +1218,23 @@ function _encode_h2_headers_frame_bytes_locked!(
return take!(out)
end


# Trim-aware dispatch shim for the widened handler response (see _write_all_response_dyn!)
@noinline function _write_h2_response_dyn!(
conn::Union{TCP.Conn,TLS.Conn},
write_lock::ReentrantLock,
send_state::_H2SendWindowState,
stream_id::UInt32,
request::Request,
@nospecialize(response::Response),
write_deadline_ns::Int64=Int64(0),
)::Nothing
_with_response_narrowed(response) do r
_write_h2_response!(conn, write_lock, send_state, stream_id, request, r, write_deadline_ns)
end
return nothing
end

function _write_h2_response!(
conn::Union{TCP.Conn,TLS.Conn},
write_lock::ReentrantLock,
Expand All @@ -1228,17 +1249,17 @@ function _write_h2_response!(
has_trailers = !isempty(response.trailers)
if !allows_body
@try_ignore begin
response.body isa AbstractBody && body_close!(response.body::AbstractBody)
_body_close_any!(response.body)
end
_write_h2_response_headers!(conn, write_lock, send_state, stream_id, response.status, response.headers, !has_trailers, write_deadline_ns)
has_trailers && _write_h2_trailers!(conn, write_lock, send_state, stream_id, response.trailers, write_deadline_ns)
return nothing
end
body_empty = response.body isa EmptyBody ||
(response.body isa AbstractString && isempty(response.body::AbstractString)) ||
(response.body isa AbstractVector{UInt8} && isempty(response.body::AbstractVector{UInt8}))
end_stream = body_empty && !has_trailers
body = response.body
# concrete-first isa chain: the response arrives @nospecialize'd, so
# abstract-narrowed isempty calls are dynamic under `juliac --trim`
body_empty = _with_body_narrowed(_response_body_known_empty, body)
end_stream = body_empty && !has_trailers
# Fallback path for streaming/empty bodies: original two-step path.
if body_empty || !(body isa AbstractString || body isa AbstractVector{UInt8})
_write_h2_response_headers!(conn, write_lock, send_state, stream_id, response.status, response.headers, end_stream, write_deadline_ns)
Expand All @@ -1260,9 +1281,18 @@ function _write_h2_response!(
finally
unlock(send_state.state_lock)
end
body_bytes::AbstractVector{UInt8} = body isa AbstractString ?
codeunits(String(body)) :
(body isa Vector{UInt8} ? body : Vector{UInt8}(body))
# normalize to a concrete Vector{UInt8} once (one copy for string bodies on the
# h2 fast path — the frame writer copies into the connection buffer anyway):
# a Union-typed bytes value would make the batched-write call dynamic under --trim
body_bytes::Vector{UInt8} = _with_body_narrowed(body) do b
if b isa Vector{UInt8}
b
elseif b isa AbstractString
Vector{UInt8}(codeunits(String(b)))
else
Vector{UInt8}(b::AbstractVector{UInt8})
end
end
_write_h2_headers_and_first_batch!(
conn, write_lock, send_state, stream_id,
response.status, response.headers,
Expand Down Expand Up @@ -1524,7 +1554,7 @@ function _handle_h2_stream!(
return nothing
end
response_obj = response::Response
_write_h2_response!(conn, write_lock, send_state, stream_id, handler_request, response_obj, _server_write_deadline_ns(server))
_write_h2_response_dyn!(conn, write_lock, send_state, stream_id, handler_request, response_obj, _server_write_deadline_ns(server))
end
_request_body_fully_consumed(request) || body_close!(request.body)
catch err
Expand Down Expand Up @@ -1718,7 +1748,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, _ConnShutdownHook(conn, write_lock, conn_control))
_set_conn_state!(tracked, _ConnState.IDLE)
while true
_server_shutting_down(server) && return nothing
Expand Down
2 changes: 1 addition & 1 deletion src/http_client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ function _acquire_h2_conn!(
rethrow()
end
else
throw(ArgumentError("HTTP/2 is not supported for proxy plan mode $(plan.mode)"))
throw(ArgumentError("HTTP/2 is not supported for this proxy plan mode"))
end
# Claim a slot on the freshly opened connection up front so subsequent
# acquirers that race in see this caller's pending request.
Expand Down
Loading
Loading