@@ -31,6 +31,7 @@ import ..CookieJar
3131import .. COOKIEJAR
3232import .. Cookies
3333import .. _ConnReader
34+ import .. _RawIOConn
3435import .. _USE_TRANSPORT_PROXY
3536import .. _close_conn!
3637import .. _client_for_request
@@ -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,105 @@ 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+ raw = _RawIOConn (io)
1099+
1100+ # Serialize and write the handshake request to the caller's transport.
1101+ request_buf = IOBuffer ()
1102+ write_request! (request_buf, request)
1103+ write (raw, take! (request_buf))
1104+
1105+ incoming = _read_incoming_response (raw, request)
1106+ @try_ignore body_close! (incoming. rawbody)
1107+ response = _streaming_response (incoming)
1108+ negotiated = _validate_websocket_upgrade! (response, expected_accept, subprotocols)
1109+
1110+ # No-op `close_transport!`: the caller retains ownership of `io`.
1111+ return _finish_client_websocket (
1112+ raw,
1113+ () -> nothing ,
1114+ request,
1115+ response,
1116+ negotiated;
1117+ maxframesize= maxframesize,
1118+ maxfragmentation= maxfragmentation,
1119+ )
1120+ end
1121+
1122+ """
1123+ open(io::IO; target="/", host="", kwargs...) -> WebSocket
1124+ open(f, io::IO; target="/", host="", kwargs...) -> Any
1125+
1126+ Perform the WebSocket client handshake directly over an already-connected `io`
1127+ (a raw `TCPSocket`, a TLS stream, or any other byte `IO`) instead of dialing a
1128+ new connection from a URL. This is useful when the transport is established
1129+ out-of-band, for testing, or for tunnelling WebSockets over a custom stream.
1130+
1131+ Because there is no URL to derive them from, the request-line `target` and `Host`
1132+ header default to `"/"` and unset; override them with the `target` and `host`
1133+ keyword arguments. The remaining `headers`, `subprotocols`, `maxframesize`, and
1134+ `maxfragmentation` keywords match the URL-based [`open`](@ref); pool, TLS, proxy,
1135+ redirect, cookie, and timeout options do not apply.
1136+
1137+ `io` is **not** closed by `open` — the caller retains ownership of the underlying
1138+ stream's lifetime. As with the URL form, calling `open` with a function closes
1139+ the WebSocket with status code `1000` when `f` returns (the close frame is sent
1140+ over `io`, but `io` itself stays open).
1141+ """
1142+ function open (
1143+ io:: IO ;
1144+ suppress_close_error:: Bool = false ,
1145+ kwargs... ,
1146+ )
1147+ return _open_client_websocket_io (io; kwargs... )
1148+ end
1149+
1150+ function open (
1151+ f:: Function ,
1152+ io:: IO ;
1153+ suppress_close_error:: Bool = false ,
1154+ kwargs... ,
1155+ )
1156+ ws = open (io; kwargs... )
1157+ try
1158+ return f (ws)
1159+ catch err
1160+ if err isa WebSocketError && isok (err)
1161+ return nothing
1162+ end
1163+ rethrow (err)
1164+ finally
1165+ if ! isclosed (ws)
1166+ try
1167+ close (ws, CloseFrameBody (1000 , " " ))
1168+ catch err
1169+ if ! (suppress_close_error && err isa WebSocketError)
1170+ rethrow (err)
1171+ end
1172+ end
1173+ end
1174+ end
1175+ end
1176+
10471177"""
10481178 Server(; network="tcp", address="127.0.0.1:0", handler, tls_config=nothing, ...)
10491179
0 commit comments