Skip to content
Closed
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
5 changes: 4 additions & 1 deletion 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"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
Expand All @@ -20,6 +21,7 @@ Zlib_jll = "83775a58-1f1d-513f-b197-d71354ab007a"
[compat]
CodecZlib = "0.7"
EnumX = "1"
Preferences = "1"
PrecompileTools = "1.2.1"
Reseau = "1.4.3"
URIs = "1.6.1"
Expand All @@ -31,6 +33,7 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2"
Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[targets]
test = ["Base64", "JuliaC", "Reseau", "Test"]
test = ["Base64", "JuliaC", "Reseau", "Test", "TOML"]
25 changes: 25 additions & 0 deletions docs/src/guides/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ A non-yielding handler can still monopolize the interactive pool. The separate
pool protects HTTP work from compute tasks assigned to `:default`; it cannot
make non-yielding handler code cooperative.

## Native Builds

For JuliaC `--trim=safe` builds of a server, enable the `trim_strict_bodies`
preference before compiling:

```julia
using HTTP, Preferences
Preferences.set_preferences!(HTTP, "trim_strict_bodies" => true)
```

With it enabled:

- Handler responses must have a `String`, `Vector{UInt8}`, or `HTTP.EmptyBody`
body, or an `HTTP.BytesBody` over a byte vector, `codeunits(::String)`, or a
contiguous view of a byte vector (what `servecontent` returns for a range of a
byte vector). Constructors convert substring and `nothing` bodies to `String`
and `HTTP.EmptyBody`. The server answers other body types, including
`servecontent` over an `IO`, with a 500.
- `Router` handlers accept the requests and streams that `serve!`,
`streamhandler`, and `listen!` deliver. Calling a router directly with another
request body type throws `ArgumentError`.
- HTTP's precompile workload is skipped.

The default, `false`, places no limits on handler or body types.

## Request Handlers

Use `HTTP.serve!` or `HTTP.serve` when your application naturally maps
Expand Down
22 changes: 9 additions & 13 deletions src/http2_server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1541,12 +1531,18 @@ function _handle_h2_stream!(
end
response_obj = response::Response
try
_check_response_body_unsent(response_obj, handler_request)
let buffered_request = handler_request
_with_response_narrowed(r -> _check_response_body_unsent(r, buffered_request), response_obj)
end
catch err
@error "h2 server handler returned an unusable response body" exception = err
response_obj = Response(500; proto_major=2, proto_minor=0, request=handler_request)
end
_write_h2_response!(conn, write_lock, send_state, stream_id, handler_request, response_obj, _server_write_deadline_ns(server))
let buffered_request = handler_request
_with_response_narrowed(response_obj) do r
_write_h2_response!(conn, write_lock, send_state, stream_id, buffered_request, r, _server_write_deadline_ns(server))
end
end
end
_request_body_fully_consumed(request) || body_close!(request.body)
catch err
Expand Down Expand Up @@ -1743,7 +1739,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, _H2ServerShutdownHook(write_lock, conn_control))
_set_conn_state!(tracked, _ConnState.IDLE)
while true
_server_shutting_down(server) && return nothing
Expand Down
26 changes: 26 additions & 0 deletions src/http_core.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Core HTTP request/response/header/body types and errors.
using Preferences: @load_preference
using Reseau.TCP
using Reseau.TLS
using Reseau.IOPoll
Expand Down Expand Up @@ -1974,6 +1975,31 @@ mutable struct Response{B}
redirect_count::Int
end

# Native builds cannot dispatch over an open set of Response{B} types after a
# handler or middleware returns an inference-widened response. This preference
# closes that boundary to buffered body types; ordinary builds fall back to
# dynamic dispatch for other body types.
const _TRIM_STRICT_BODIES = @load_preference("trim_strict_bodies", false)::Bool

@inline function _with_response_narrowed(f::F, @nospecialize(response::Response)) where {F}
if response isa Response{String}
return f(response)
elseif response isa Response{Vector{UInt8}}
return f(response)
elseif response isa Response{EmptyBody}
return f(response)
elseif response isa Response{BytesBody{Vector{UInt8}}}
return f(response)
elseif response isa Response{BytesBody{Base.CodeUnits{UInt8,String}}}
return f(response)
elseif response isa Response{BytesBody{SubArray{UInt8,1,Vector{UInt8},Tuple{UnitRange{Int}},true}}}
# `servecontent` range responses over a byte vector
return f(response)
end
_TRIM_STRICT_BODIES && throw(ArgumentError("unsupported response body type with trim_strict_bodies enabled"))
return f(response)
end

# Check single-use bodies before committing a response head to the wire.
function _check_response_body_unsent(response::Response, request=response.request)::Nothing
_body_allowed_for_status(response.status) || return nothing
Expand Down
81 changes: 65 additions & 16 deletions src/http_handlers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import ..peeraddr
import .._server_error_status
import ..@_spawn_interactive
import ..@try_ignore
import ..EmptyBody
import .._TRIM_STRICT_BODIES

using Logging: Logging, AbstractLogger, LogLevel, with_logger, @logmsg

Expand Down Expand Up @@ -84,6 +86,51 @@ function _route_variable_matches(pattern::Regex, segment::AbstractString)
return m !== nothing && m.match == segment
end

# A router stores unrelated callable types in one trie. In strict native builds,
# call through a per-callable C entrypoint instead of runtime specialization.
# Ref boxes carry arguments/results across a primitive-only C signature.
function _handler_entry(f::F, xptr::Ptr{Cvoid}, outptr::Ptr{Cvoid}) where {F}
xref = unsafe_pointer_to_objref(xptr)::Base.RefValue{Any}
out = unsafe_pointer_to_objref(outptr)::Base.RefValue{Any}
out[] = _call_handler_narrowed(f, xref[])
return nothing
end

function _call_handler_narrowed(f::F, @nospecialize(x)) where {F}
if x isa Request{EmptyBody}
return f(x)
elseif x isa Request{BytesBody{Vector{UInt8}}}
return f(x)
elseif x isa Stream{false, Request{EmptyBody}}
# Server streams keep live bytes separately from their bodyless metadata.
return f(x)
else
throw(ArgumentError("unsupported request type for a routed handler"))
end
end

struct _HandlerFn
ptr::Ptr{Cvoid} # @cfunction pointer (specialized per callable type F)
objptr::Ptr{Cvoid} # pointer to the callable object
_root::Any # Retain the callable storage throughout the leaf lifetime.
end

function _HandlerFn(callable::F) where F
ptr = @cfunction(_handler_entry, Cvoid, (Ref{F}, Ptr{Cvoid}, Ptr{Cvoid}))
objref = Base.cconvert(Ref{F}, callable)
objptr = Ptr{Cvoid}(Base.unsafe_convert(Ref{F}, objref))
return _HandlerFn(ptr, objptr, objref)
end

@inline function (h::_HandlerFn)(x)
xref = Ref{Any}(x)
out = Ref{Any}(nothing)
GC.@preserve h xref out begin
ccall(h.ptr, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), h.objptr, pointer_from_objref(xref), pointer_from_objref(out))
end
return out[]
end

struct Leaf{H}
method::String
variables::Vector{Tuple{Int,String}}
Expand Down Expand Up @@ -116,7 +163,7 @@ segment(x) = isvariable(x) ? Variable(x) : String(x)
Node(x) = Node(x, Node[], Node[], nothing, nothing, Leaf[])
Node() = Node("*")

function find(y, itr; by=identity, eq=(==))
function find(y, itr; by::B=identity, eq::E=(==)) where {B,E}
for (i, x) in enumerate(itr)
eq(by(x), y) && return i
end
Expand All @@ -138,19 +185,19 @@ function insert!(node::Node, leaf, segments, i)
if segment_value isa Variable
push!(leaf.variables, (i, segment_value.name))
end
if segment_value == "*" || (segment_value isa Variable && segment_value.pattern === nothing)
if (segment_value isa String && segment_value == "*") || (segment_value isa Variable && segment_value.pattern === nothing)
if node.wildcard === nothing
node.wildcard = Node(segment_value)
end
return insert!(node.wildcard::Node, leaf, segments, i + 1)
elseif segment_value == "**"
elseif segment_value isa String && segment_value == "**"
if node.doublestar === nothing
node.doublestar = Node(segment_value)
end
i < length(segments) && error("/** double wildcard must be last segment in path")
return insert!(node.doublestar::Node, leaf, segments, i + 1)
elseif segment_value isa Variable
j = find(segment_value.pattern, node.conditional; by=x -> x.segment.pattern)
j = find(segment_value.pattern, node.conditional; by=x -> (x.segment::Variable).pattern)
if j === nothing
n = Node(segment_value)
push!(node.conditional, n)
Expand All @@ -159,11 +206,11 @@ function insert!(node::Node, leaf, segments, i)
end
return insert!(n, leaf, segments, i + 1)
else
j = find(segment_value, node.exact; by=x -> x.segment)
j = find(segment_value, node.exact; by=x -> (x.segment::String))
if j === nothing
n = Node(segment_value)
push!(node.exact, n)
sort!(node.exact; by=x -> x.segment)
sort!(node.exact; by=x -> (x.segment::String))
return insert!(n, leaf, segments, i + 1)
end
return insert!(node.exact[j], leaf, segments, i + 1)
Expand All @@ -178,15 +225,15 @@ function match(node::Node, method, segments, i)
end
segment_value = segments[i]
anymissing = false
j = find(segment_value, node.exact; by=x -> x.segment)
j = find(segment_value, node.exact; by=x -> (x.segment::String))
if j !== nothing
m = match(node.exact[j], method, segments, i + 1)
anymissing = m === missing
m = coalesce(m, nothing)
m !== nothing && return m
end
for conditional_node in node.conditional
if _route_variable_matches(conditional_node.segment.pattern, segment_value)
if _route_variable_matches((conditional_node.segment::Variable).pattern::Regex, segment_value)
m = match(conditional_node, method, segments, i + 1)
anymissing = m === missing
m = coalesce(m, nothing)
Expand Down Expand Up @@ -238,7 +285,7 @@ default405(::Request) = Response(405)
default404(stream::Stream) = setstatus(stream, 404)
default405(stream::Stream) = setstatus(stream, 405)

Router(_404=default404, _405=default405, middleware=nothing) = Router(_404, _405, Node(), middleware)
Router(_404::T=default404, _405::S=default405, middleware::F=nothing) where {T,S,F} = Router(_404, _405, Node(), middleware)

"""
register!(router, method, path, handler) -> Nothing
Expand Down Expand Up @@ -266,19 +313,19 @@ end
"""
function register! end

function register!(r::Router, method, path, handler)
segments = map(segment, split(path, '/'; keepempty=false))
function register!(r::Router, method, path, handler::F) where {F}
segments = Union{String,Variable}[segment(part) for part in split(path, '/'; keepempty=false)]
if r.middleware !== nothing
handler = r.middleware(handler)
end
insert!(r.routes, Leaf(method, Tuple{Int,String}[], path, handler), segments, 1)
insert!(r.routes, Leaf(method, Tuple{Int,String}[], path, _TRIM_STRICT_BODIES ? _HandlerFn(handler) : handler), segments, 1)
return nothing
end

register!(r::Router, path, handler) = register!(r, "*", path, handler)
register!(r::Router, path, handler::F) where {F} = register!(r, "*", path, handler)

register!(handler, r::Router, method, path) = register!(r, method, path, handler)
register!(handler, r::Router, path) = register!(r, "*", path, handler)
register!(handler::F, r::Router, method, path) where {F} = register!(r, method, path, handler)
register!(handler::F, r::Router, path) where {F} = register!(r, "*", path, handler)

const Params = Dict{String,String}

Expand Down Expand Up @@ -314,7 +361,8 @@ function gethandler(r::Router, req::Request)
params[v] = segments[i]
end
end
return leaf.handler, leaf.path, params
handler = _TRIM_STRICT_BODIES ? (leaf::Leaf{_HandlerFn}).handler : leaf.handler
return handler, leaf.path, params
end
return leaf, "", params
end
Expand Down Expand Up @@ -370,6 +418,7 @@ Retrieve a matched path parameter with name `name` from request context.
function getparam(req, name, default=nothing)
params = getparams(req)
params === nothing && return default
_TRIM_STRICT_BODIES && (params = params::Params)
return get(params, name, default)
end

Expand Down
29 changes: 23 additions & 6 deletions src/http_server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,27 @@ 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 an erased closure so shutdown is callable in native builds.
struct _H2ServerShutdownHook
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,_H2ServerShutdownHook}
@atomic state::_ConnState.T
@atomic state_unix_sec::Int64
end
Expand Down Expand Up @@ -425,7 +442,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,_H2ServerShutdownHook})::Nothing
lock(conn.lock)
try
conn.shutdown_hook = hook
Expand All @@ -444,7 +461,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

Expand Down Expand Up @@ -1248,9 +1265,9 @@ function _serve_h1_conn!(server::Server, tracked::_ServerConn, reader_source)::N
# serialized again. Answer 500 before any bytes go out instead
# of failing mid-response and dropping the connection (#1333).
try
_check_response_body_unsent(response_obj)
_with_response_narrowed(_check_response_body_unsent, response_obj)
catch err
@error "server handler returned a response whose body was already sent or closed" exception = err
@error "server handler returned an unusable response body" exception = err
_try_write_server_error!(tracked.conn, request, 500)
return nothing
end
Expand All @@ -1261,7 +1278,7 @@ function _serve_h1_conn!(server::Server, tracked::_ServerConn, reader_source)::N
end
end
_set_write_deadline!(server, tracked.conn)
_write_all_response!(tracked.conn, response_obj)
_with_response_narrowed(r -> _write_all_response!(tracked.conn, r), response_obj)
_clear_deadlines!(server, tracked.conn)
_server_shutting_down(server) && return nothing
if _request_wants_close(handler_request) || _response_wants_close(response_obj)
Expand Down
4 changes: 3 additions & 1 deletion src/precompile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,9 @@ function _precompile_host_resolver_available()::Bool
end
end

if _precompile_workload_enabled()
# The built-in workload includes file responses with custom streaming bodies,
# which are outside the buffered response set enabled by strict native builds.
if _precompile_workload_enabled() && !_TRIM_STRICT_BODIES
try
_precompile_trace("enabled")
@setup_workload begin
Expand Down
Loading
Loading