11# High-level HTTP client orchestration, HTTP/2 integration, cookies, response sinks, and convenience APIs.
22
3+ # Copy-loop buffer for response bodies that are known (or likely) to be large:
4+ # 64 KiB measured faster than 8 KiB and 256 KiB for high-throughput downloads.
5+ # Loops whose payload is typically small — unknown-size accumulation of
6+ # chunked responses and decompression output — keep 8 KiB buffers so a tiny
7+ # response doesn't pay a 64 KiB allocation per request.
8+ const _RESPONSE_COPY_BUFFER_BYTES = 64 * 1024
9+ const _RESPONSE_SMALL_COPY_BUFFER_BYTES = 8 * 1024
10+
311"""
412 Client(; ...)
513
@@ -15,7 +23,11 @@ Keyword arguments:
1523 followed
1624- `cookiejar`: cookie jar implementation, or `nothing` to disable cookies
1725- `max_redirects`: maximum redirect hops before failing
18- - `prefer_http2`: whether secure requests should try HTTP/2 when available
26+ - `prefer_http2`: whether secure requests should try HTTP/2 when available.
27+ When automatic negotiation (`protocol = :auto`) learns an origin only
28+ speaks HTTP/1.1, that result is cached and later automatic requests skip
29+ the HTTP/2 attempt; [`close_idle_connections!`](@ref) clears the cache.
30+ An explicit `protocol = :h2` always attempts HTTP/2.
1931- `http2_settings`: an [`HTTP2Settings`](@ref) configuring HTTP/2 receive
2032 flow-control windows for connections this client opens
2133- `default_headers`: headers applied to every request issued through this
@@ -48,6 +60,12 @@ mutable struct Client{CR}
4860 http2_settings:: HTTP2Settings
4961 h2_lock:: ReentrantLock
5062 h2_conns:: Dict{String,Vector{H2Connection}}
63+ # Origins that negotiated HTTP/1.1 during `protocol = :auto`. Guarded by
64+ # its own lock (never `h2_lock`): `h2_lock` is held across full TCP+TLS
65+ # dials in `_acquire_h2_conn!`, and the hot-path membership check in
66+ # `_use_h2` must not serialize behind those dials.
67+ h1_origins_lock:: ReentrantLock
68+ h1_origins:: Set{String}
5169 default_headers:: Headers
5270 default_query:: Union{Nothing,Vector{Pair{String,String}}}
5371 default_basicauth:: Any
@@ -342,6 +360,8 @@ function Client(;
342360 http2_settings,
343361 ReentrantLock (),
344362 Dict {String,Vector{H2Connection}} (),
363+ ReentrantLock (),
364+ Set {String} (),
345365 _normalize_headers_input (default_headers),
346366 _normalize_default_query (default_query),
347367 default_basicauth,
@@ -445,10 +465,16 @@ function _acquire_h2_conn!(
445465 address:: String ,
446466 secure:: Bool ,
447467 request:: Union{Nothing,Request} = nothing ,
448- server_name:: Union{Nothing,String} = nothing ,
468+ server_name:: Union{Nothing,String} = nothing ;
449469 allow_h1_alpn:: Bool = false ,
470+ auto_protocol:: Bool = false ,
450471):: H2Connection
451472 key = _h2_key (plan)
473+ # Fail fast (and without touching `h2_lock`) when another task cached this
474+ # origin as HTTP/1.1 after our caller's `_use_h2` check.
475+ if auto_protocol && _h1_origin_cached (client, key)
476+ throw (H2NegotiationError (" http2: origin previously negotiated HTTP/1.1" ))
477+ end
452478 base_host_resolver = client. transport. host_resolver
453479 connect_host_resolver = request === nothing ? base_host_resolver : _request_connect_host_resolver (base_host_resolver, request:: Request )
454480 connect_deadline_ns = request === nothing ? _phase_deadline_ns (base_host_resolver. timeout_ns, base_host_resolver. deadline_ns) : _request_connect_phase_deadline_ns (base_host_resolver, request:: Request )
@@ -457,8 +483,15 @@ function _acquire_h2_conn!(
457483 # closing an H2Connection waits for its read loop to exit, and that must
458484 # not happen while holding the pool lock.
459485 to_close = H2Connection[]
460- lock (client. h2_lock)
486+ waited_for_h2_lock = ! trylock (client. h2_lock)
487+ waited_for_h2_lock && lock (client. h2_lock)
461488 try
489+ # The origin can be cached while this task waits for `h2_lock`. Only
490+ # contended acquisitions need this second check, so the uncontended h2
491+ # fast path does not pay for another cache lookup.
492+ if waited_for_h2_lock && auto_protocol && _h1_origin_cached (client, key)
493+ throw (H2NegotiationError (" http2: origin previously negotiated HTTP/1.1" ))
494+ end
462495 conns = get (() -> H2Connection[], client. h2_conns, key)
463496 idle_timeout_ns = client. transport. idle_timeout_ns
464497 now_ns = Int64 (time_ns ())
@@ -504,36 +537,43 @@ function _acquire_h2_conn!(
504537 nothing
505538 end
506539 conn = nothing
507- conn = if plan. mode == _ProxyPlanMode. DIRECT
508- connect_h2! (
509- address;
510- secure= secure,
511- host_resolver= connect_host_resolver,
512- tls_config= tls_cfg,
513- connect_deadline_ns= connect_deadline_ns,
514- http2_settings= client. http2_settings,
515- )
516- elseif plan. mode == _ProxyPlanMode. HTTP_TUNNEL || _proxy_plan_is_socks (plan)
517- proxy = plan. proxy
518- proxy === nothing && throw (ProtocolError (" proxy connection is missing proxy config" ))
519- tcp = _new_tcp_conn! (plan, address, connect_host_resolver, connect_deadline_ns)
520- try
540+ conn = try
541+ if plan. mode == _ProxyPlanMode. DIRECT
521542 connect_h2! (
522- tcp,
523543 address;
524544 secure= secure,
545+ host_resolver= connect_host_resolver,
525546 tls_config= tls_cfg,
526547 connect_deadline_ns= connect_deadline_ns,
527548 http2_settings= client. http2_settings,
528549 )
529- catch
530- @try_ignore begin
531- TCP. close (tcp)
550+ elseif plan. mode == _ProxyPlanMode. HTTP_TUNNEL || _proxy_plan_is_socks (plan)
551+ proxy = plan. proxy
552+ proxy === nothing && throw (ProtocolError (" proxy connection is missing proxy config" ))
553+ tcp = _new_tcp_conn! (plan, address, connect_host_resolver, connect_deadline_ns)
554+ try
555+ connect_h2! (
556+ tcp,
557+ address;
558+ secure= secure,
559+ tls_config= tls_cfg,
560+ connect_deadline_ns= connect_deadline_ns,
561+ http2_settings= client. http2_settings,
562+ )
563+ catch
564+ @try_ignore begin
565+ TCP. close (tcp)
566+ end
567+ rethrow ()
532568 end
533- rethrow ()
569+ else
570+ throw (ArgumentError (" HTTP/2 is not supported for proxy plan mode $(plan. mode) " ))
534571 end
535- else
536- throw (ArgumentError (" HTTP/2 is not supported for proxy plan mode $(plan. mode) " ))
572+ catch err
573+ if auto_protocol && _should_fallback_h2_to_h1 (err)
574+ _cache_h1_origin! (client, key)
575+ end
576+ rethrow ()
537577 end
538578 # Claim a slot on the freshly opened connection up front so subsequent
539579 # acquirers that race in see this caller's pending request.
@@ -581,7 +621,7 @@ function _drop_h2_conn!(client::Client, plan::_ProxyPlan, target::Union{Nothing,
581621 return nothing
582622end
583623
584- function _use_h2 (client:: Client , secure:: Bool , protocol:: Symbol ):: Bool
624+ function _use_h2 (client:: Client , plan :: _ProxyPlan , secure:: Bool , protocol:: Symbol ):: Bool
585625 protocol == :h1 && return false
586626 protocol == :h2 && return true
587627 protocol == :auto || throw (ArgumentError (" protocol must be :auto, :h1, or :h2" ))
@@ -594,7 +634,26 @@ function _use_h2(client::Client, secure::Bool, protocol::Symbol)::Bool
594634 if cfg != = nothing && ! isempty (cfg. alpn_protocols) && ! in (" h2" , cfg. alpn_protocols)
595635 return false
596636 end
597- return true
637+ return ! _h1_origin_cached (client, _h2_key (plan))
638+ end
639+
640+ @inline function _h1_origin_cached (client:: Client , key:: String ):: Bool
641+ lock (client. h1_origins_lock)
642+ try
643+ return key in client. h1_origins
644+ finally
645+ unlock (client. h1_origins_lock)
646+ end
647+ end
648+
649+ function _cache_h1_origin! (client:: Client , key:: String ):: Nothing
650+ lock (client. h1_origins_lock)
651+ try
652+ push! (client. h1_origins, key)
653+ finally
654+ unlock (client. h1_origins_lock)
655+ end
656+ return nothing
598657end
599658
600659function _host_path_from_request (address:: String , request:: Request ):: Tuple{String,String}
@@ -678,11 +737,10 @@ function _store_set_cookies!(
678737end
679738
680739function _clone_bytes_body (body:: BytesBody ):: BytesBody
681- remaining = (length (body. data) - body. next_index) + 1
682- remaining <= 0 && return BytesBody (UInt8[])
683- copied = Vector {UInt8} (undef, remaining)
684- copyto! (copied, 1 , body. data, body. next_index, remaining)
685- return BytesBody (copied)
740+ # BytesBody retains its backing bytes by contract. A replay only needs an
741+ # independent cursor and closed flag; copying the payload makes every
742+ # buffered upload allocate and copy the complete request body again.
743+ return BytesBody (body. data, body. next_index, false )
686744end
687745
688746function _clone_body (body:: AbstractBody ):: AbstractBody
@@ -792,7 +850,8 @@ function _do_incoming!(
792850 send_request = _copy_request_for_send (current_request, retry_attempt == 1 )
793851 request_url = _request_url (current_secure, current_address, current_request. target)
794852 proxy_plan = _proxy_plan (proxy_config, current_secure, current_address)
795- use_h2 = _use_h2 (client, current_secure, protocol) && proxy_plan. mode != _ProxyPlanMode. HTTP_FORWARD
853+ use_h2 = proxy_plan. mode != _ProxyPlanMode. HTTP_FORWARD &&
854+ _use_h2 (client, proxy_plan, current_secure, protocol)
796855 _emit_trace (trace, RequestEvent (send_request, request_url, retry_attempt, redirect_count, use_h2 ? :h2 : :h1 ))
797856 host, path = _host_path_from_request (current_address, current_request)
798857 manual_cookies = cookies === false ? Cookie[] : Cookies. readcookies (send_request. headers, " " )
@@ -808,8 +867,9 @@ function _do_incoming!(
808867 current_address,
809868 current_secure,
810869 send_request,
811- current_server_name,
812- protocol == :auto ,
870+ current_server_name;
871+ allow_h1_alpn= (protocol == :auto ),
872+ auto_protocol= (protocol == :auto ),
813873 )
814874 _h2_roundtrip_incoming! (conn:: H2Connection , send_request; pending_slot_claimed= true )
815875 catch err
@@ -1186,6 +1246,14 @@ end
11861246
11871247function close_idle_connections! (client:: Client )
11881248 close_idle_connections! (client. transport)
1249+ # Drop cached HTTP/1.1 negotiation results so long-lived clients re-probe
1250+ # origins that may have enabled HTTP/2 since they were first contacted.
1251+ lock (client. h1_origins_lock)
1252+ try
1253+ empty! (client. h1_origins)
1254+ finally
1255+ unlock (client. h1_origins_lock)
1256+ end
11891257 # Also close pooled HTTP/2 connections with no in-flight streams — these
11901258 # live on the Client (not the Transport pool) and are equally subject to
11911259 # silent idle drops by NATs/load balancers (#1331). Connections carrying
@@ -1238,7 +1306,7 @@ end
12381306
12391307function _read_all_response_bytes (io:: IO , limit:: Int = 0 ):: Vector{UInt8}
12401308 out = UInt8[]
1241- buf = Vector {UInt8} (undef, 8192 )
1309+ buf = Vector {UInt8} (undef, _RESPONSE_SMALL_COPY_BUFFER_BYTES )
12421310 total = 0
12431311 while true
12441312 n = readbytes! (io, buf, length (buf))
@@ -1260,7 +1328,10 @@ function _read_all_response_bytes(body::AbstractBody, content_length_hint::Int64
12601328 end
12611329 out = UInt8[]
12621330 content_length_hint > 0 && sizehint! (out, Int (min (content_length_hint, _MAX_EAGER_RESPONSE_PREALLOC)))
1263- buf = Vector {UInt8} (undef, 8192 )
1331+ # A hint above the prealloc cap means a known-large download; without a
1332+ # hint (chunked/EOF-framed) the body is usually small, so stay at 8 KiB.
1333+ buf_bytes = content_length_hint > _MAX_EAGER_RESPONSE_PREALLOC ? _RESPONSE_COPY_BUFFER_BYTES : _RESPONSE_SMALL_COPY_BUFFER_BYTES
1334+ buf = Vector {UInt8} (undef, buf_bytes)
12641335 while true
12651336 n = body_read! (body, buf)
12661337 n == 0 && return out
@@ -1269,7 +1340,7 @@ function _read_all_response_bytes(body::AbstractBody, content_length_hint::Int64
12691340end
12701341
12711342function _copy_response_bytes! (dest:: IO , io:: IO , limit:: Int = 0 ):: Int64
1272- buf = Vector {UInt8} (undef, 8192 )
1343+ buf = Vector {UInt8} (undef, _RESPONSE_SMALL_COPY_BUFFER_BYTES )
12731344 total = Int64 (0 )
12741345 while true
12751346 n = readbytes! (io, buf, length (buf))
@@ -1281,9 +1352,12 @@ function _copy_response_bytes!(dest::IO, io::IO, limit::Int=0)::Int64
12811352end
12821353
12831354function _copy_response_bytes! (dest:: AbstractVector{UInt8} , io:: IO , limit:: Int = 0 ):: Int64
1284- buf = Vector {UInt8} (undef, 8192 )
1285- total = 0
12861355 capacity = length (dest)
1356+ # `dest` is preallocated to the expected size; a small response should not
1357+ # pay a 64 KiB scratch allocation. The 1-byte floor keeps the capacity
1358+ # overflow check below reachable when `dest` is empty.
1359+ buf = Vector {UInt8} (undef, min (max (capacity, 1 ), _RESPONSE_COPY_BUFFER_BYTES))
1360+ total = 0
12871361 while true
12881362 n = readbytes! (io, buf, length (buf))
12891363 n == 0 && break
@@ -1298,7 +1372,7 @@ function _copy_response_bytes!(dest::AbstractVector{UInt8}, io::IO, limit::Int=0
12981372end
12991373
13001374function _copy_response_bytes! (dest:: IO , body:: AbstractBody ):: Int64
1301- buf = Vector {UInt8} (undef, 8192 )
1375+ buf = Vector {UInt8} (undef, _RESPONSE_COPY_BUFFER_BYTES )
13021376 total = Int64 (0 )
13031377 while true
13041378 n = body_read! (body, buf)
@@ -1309,9 +1383,10 @@ function _copy_response_bytes!(dest::IO, body::AbstractBody)::Int64
13091383end
13101384
13111385function _copy_response_bytes! (dest:: AbstractVector{UInt8} , body:: AbstractBody ):: Int64
1312- buf = Vector {UInt8} (undef, 8192 )
1313- total = 0
13141386 capacity = length (dest)
1387+ # See the sizing rationale on the `io::IO` method above.
1388+ buf = Vector {UInt8} (undef, min (max (capacity, 1 ), _RESPONSE_COPY_BUFFER_BYTES))
1389+ total = 0
13151390 while true
13161391 n = body_read! (body, buf)
13171392 n == 0 && break
@@ -1356,7 +1431,7 @@ end
13561431end
13571432
13581433function _pump_response_body! (stream:: Base.BufferStream , body:: AbstractBody ):: Nothing
1359- buf = Vector {UInt8} (undef, 8192 )
1434+ buf = Vector {UInt8} (undef, _RESPONSE_COPY_BUFFER_BYTES )
13601435 try
13611436 while true
13621437 n = body_read! (body, buf)
@@ -1466,7 +1541,7 @@ end
14661541
14671542function Base. read (io:: _BodyIO ):: Vector{UInt8}
14681543 out = UInt8[]
1469- buf = Vector {UInt8} (undef, 8192 )
1544+ buf = Vector {UInt8} (undef, _RESPONSE_SMALL_COPY_BUFFER_BYTES )
14701545 while true
14711546 n = readbytes! (io, buf)
14721547 n == 0 && break
0 commit comments