@@ -31,6 +31,7 @@ import ..CookieJar
3131import .. COOKIEJAR
3232import .. Cookies
3333import .. _ConnReader
34+ import .. _blocking_readbytes!
3435import .. _USE_TRANSPORT_PROXY
3536import .. _close_conn!
3637import .. _client_for_request
@@ -442,8 +443,8 @@ function _ws_read_loop!(ws::WebSocket, buffer_bytes::Int=DEFAULT_READ_BUFFER_BYT
442443 # Read directly into the reusable `buf`. `readavailable` would
443444 # allocate a fresh Base.SZ_UNBUFFERED_IO (64KB) buffer on every
444445 # frame read (16× the bytes/RTT vs HTTP 1.x), driving GC pressure;
445- # `readbytes!(...; all=false) ` does one socket read into `buf`.
446- n = readbytes ! (ws. stream, buf, length (buf); all = false )
446+ # `_blocking_readbytes! ` does one blocking socket read into `buf`.
447+ n = _blocking_readbytes ! (ws. stream, buf, length (buf))
447448 n == 0 && break
448449 ws_on_incoming_data! (on_frame, ws. codec, buf, n)
449450 _flush_ws_output! (ws)
@@ -495,6 +496,42 @@ function _start_read_task!(ws::WebSocket, buffer_bytes::Int=DEFAULT_READ_BUFFER_
495496 return nothing
496497end
497498
499+ # Shared client-side handshake completion for both `open` forms: build the
500+ # client `WebSocket` over an already-established `stream`, record the handshake
501+ # messages, replay any early frames the server already sent (`buffered`), and
502+ # start the read task.
503+ function _finish_client_websocket (
504+ stream,
505+ close_transport!,
506+ request:: Request ,
507+ response:: Response ,
508+ negotiated:: Union{Nothing,String} ;
509+ maxframesize:: Integer ,
510+ maxfragmentation:: Integer ,
511+ read_idle_timeout_ns:: Integer = 0 ,
512+ buffered:: Vector{UInt8} = UInt8[],
513+ pmce:: Union{Nothing,PMCEContext} = nothing ,
514+ ):: WebSocket
515+ ws = WebSocket (
516+ stream,
517+ close_transport!;
518+ subprotocol= negotiated,
519+ maxframesize= maxframesize,
520+ maxfragmentation= maxfragmentation,
521+ read_idle_timeout_ns= read_idle_timeout_ns,
522+ is_client= true ,
523+ pmce= pmce,
524+ )
525+ ws. handshake_request = request
526+ ws. handshake_response = response
527+ if ! isempty (buffered)
528+ ws_on_incoming_data! (frame -> _process_incoming_frame! (ws, frame), ws. codec, buffered)
529+ _flush_ws_output! (ws)
530+ end
531+ _start_read_task! (ws)
532+ return ws
533+ end
534+
498535"""
499536 send(ws, message) -> Int
500537
@@ -885,24 +922,18 @@ function _open_client_websocket(
885922 return nothing
886923 end
887924 end
888- ws = WebSocket (
925+ return _finish_client_websocket (
889926 _conn_stream (conn),
890927 close_transport!,
891- subprotocol= negotiated,
928+ send_request,
929+ response,
930+ negotiated;
892931 maxframesize= maxframesize,
893932 maxfragmentation= maxfragmentation,
894933 read_idle_timeout_ns= _request_read_idle_timeout_ns (send_request),
895- is_client = true ,
934+ buffered = attempt . buffered ,
896935 pmce= pmce_ctx,
897936 )
898- ws. handshake_request = send_request
899- ws. handshake_response = response
900- if ! isempty (attempt. buffered)
901- ws_on_incoming_data! (frame -> _process_incoming_frame! (ws, frame), ws. codec, attempt. buffered)
902- _flush_ws_output! (ws)
903- end
904- _start_read_task! (ws)
905- return ws
906937 end
907938 if ! _is_redirect_status (response. status) || redirect_policy. max_redirects == 0
908939 owns_client && close (req_client)
@@ -1044,6 +1075,98 @@ function open(
10441075 end
10451076end
10461077
1078+ # Perform the websocket client handshake directly over a caller-provided `io`,
1079+ # bypassing the connection pool and TLS/proxy machinery. The handshake request is
1080+ # serialized and written to `io`, then the 101 response is parsed back off it. The
1081+ # returned WebSocket's `close_transport!` is a no-op: the caller retains ownership
1082+ # of `io` and is responsible for closing it.
1083+ function _open_client_websocket_io (
1084+ io:: IO ;
1085+ target:: AbstractString = " /" ,
1086+ host:: AbstractString = " " ,
1087+ headers= Pair{String,String}[],
1088+ maxframesize:: Integer = typemax (Int),
1089+ maxfragmentation:: Integer = DEFAULT_MAX_FRAG,
1090+ subprotocols:: AbstractVector{<:AbstractString} = String[],
1091+ ):: WebSocket
1092+ req_headers = _normalize_headers_input (headers)
1093+ isempty (host) || setheader (req_headers, " Host" , host)
1094+ key = ws_random_handshake_key ()
1095+ _apply_websocket_request_headers! (req_headers, key, subprotocols)
1096+ request = Request (" GET" , String (target); headers= req_headers, host= String (host), body= EmptyBody (), content_length= 0 )
1097+ expected_accept = ws_compute_accept_key (header (request. headers, " Sec-WebSocket-Key" ):: String )
1098+
1099+ # Serialize and write the handshake request to the caller's transport.
1100+ request_buf = IOBuffer ()
1101+ write_request! (request_buf, request)
1102+ write (io, take! (request_buf))
1103+
1104+ incoming = _read_incoming_response (io, request)
1105+ @try_ignore body_close! (incoming. rawbody)
1106+ response = _streaming_response (incoming)
1107+ negotiated = _validate_websocket_upgrade! (response, expected_accept, subprotocols)
1108+
1109+ # No-op `close_transport!`: the caller retains ownership of `io`.
1110+ return _finish_client_websocket (
1111+ io,
1112+ () -> nothing ,
1113+ request,
1114+ response,
1115+ negotiated;
1116+ maxframesize= maxframesize,
1117+ maxfragmentation= maxfragmentation,
1118+ )
1119+ end
1120+
1121+ """
1122+ open(io::IO; target="/", host="", kwargs...) -> WebSocket
1123+ open(f, io::IO; target="/", host="", kwargs...) -> Any
1124+
1125+ Perform the WebSocket client handshake directly over an already-connected `io`
1126+ (a raw `TCPSocket`, a TLS stream, or any other byte `IO`) instead of dialing a
1127+ new connection from a URL. This is useful when the transport is established
1128+ out-of-band, for testing, or for tunnelling WebSockets over a custom stream.
1129+
1130+ Because there is no URL to derive them from, the request-line `target` and `Host`
1131+ header default to `"/"` and unset; override them with the `target` and `host`
1132+ keyword arguments. The remaining `headers`, `subprotocols`, `maxframesize`, and
1133+ `maxfragmentation` keywords match the URL-based [`open`](@ref); pool, TLS, proxy,
1134+ redirect, cookie, and timeout options do not apply.
1135+
1136+ `io` is **not** closed by `open` — the caller retains ownership of the underlying
1137+ stream's lifetime. As with the URL form, calling `open` with a function closes
1138+ the WebSocket with status code `1000` when `f` returns (the close frame is sent
1139+ over `io`, but `io` itself stays open).
1140+ """
1141+ open (io:: IO ; kwargs... ) = _open_client_websocket_io (io; kwargs... )
1142+
1143+ function open (
1144+ f:: Function ,
1145+ io:: IO ;
1146+ suppress_close_error:: Bool = false ,
1147+ kwargs... ,
1148+ )
1149+ ws = open (io; kwargs... )
1150+ try
1151+ return f (ws)
1152+ catch err
1153+ if err isa WebSocketError && isok (err)
1154+ return nothing
1155+ end
1156+ rethrow (err)
1157+ finally
1158+ if ! isclosed (ws)
1159+ try
1160+ close (ws, CloseFrameBody (1000 , " " ))
1161+ catch err
1162+ if ! (suppress_close_error && err isa WebSocketError)
1163+ rethrow (err)
1164+ end
1165+ end
1166+ end
1167+ end
1168+ end
1169+
10471170"""
10481171 Server(; network="tcp", address="127.0.0.1:0", handler, tls_config=nothing, ...)
10491172
0 commit comments