@@ -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
@@ -463,6 +464,40 @@ function _start_read_task!(ws::WebSocket, buffer_bytes::Int=DEFAULT_READ_BUFFER_
463464 return nothing
464465end
465466
467+ # Shared client-side handshake completion for both `open` forms: build the
468+ # client `WebSocket` over an already-established `stream`, record the handshake
469+ # messages, replay any early frames the server already sent (`buffered`), and
470+ # start the read task.
471+ function _finish_client_websocket (
472+ stream,
473+ close_transport!,
474+ request:: Request ,
475+ response:: Response ,
476+ negotiated:: Union{Nothing,String} ;
477+ maxframesize:: Integer ,
478+ maxfragmentation:: Integer ,
479+ read_idle_timeout_ns:: Integer = 0 ,
480+ buffered:: Vector{UInt8} = UInt8[],
481+ ):: WebSocket
482+ ws = WebSocket (
483+ stream,
484+ close_transport!;
485+ subprotocol= negotiated,
486+ maxframesize= maxframesize,
487+ maxfragmentation= maxfragmentation,
488+ read_idle_timeout_ns= read_idle_timeout_ns,
489+ is_client= true ,
490+ )
491+ ws. handshake_request = request
492+ ws. handshake_response = response
493+ if ! isempty (buffered)
494+ ws_on_incoming_data! (frame -> _process_incoming_frame! (ws, frame), ws. codec, buffered)
495+ _flush_ws_output! (ws)
496+ end
497+ _start_read_task! (ws)
498+ return ws
499+ end
500+
466501"""
467502 send(ws, message) -> Int
468503
@@ -812,23 +847,17 @@ function _open_client_websocket(
812847 return nothing
813848 end
814849 end
815- ws = WebSocket (
850+ return _finish_client_websocket (
816851 _conn_stream (conn),
817852 close_transport!,
818- subprotocol= negotiated,
853+ send_request,
854+ response,
855+ negotiated;
819856 maxframesize= maxframesize,
820857 maxfragmentation= maxfragmentation,
821858 read_idle_timeout_ns= _request_read_idle_timeout_ns (send_request),
822- is_client = true ,
859+ buffered = attempt . buffered ,
823860 )
824- ws. handshake_request = send_request
825- ws. handshake_response = response
826- if ! isempty (attempt. buffered)
827- ws_on_incoming_data! (frame -> _process_incoming_frame! (ws, frame), ws. codec, attempt. buffered)
828- _flush_ws_output! (ws)
829- end
830- _start_read_task! (ws)
831- return ws
832861 end
833862 if ! _is_redirect_status (response. status) || redirect_policy. max_redirects == 0
834863 owns_client && close (req_client)
@@ -968,6 +997,105 @@ function open(
968997 end
969998end
970999
1000+ # Perform the websocket client handshake directly over a caller-provided `io`,
1001+ # bypassing the connection pool and TLS/proxy machinery. The handshake request is
1002+ # serialized and written to `io`, then the 101 response is parsed back off it. The
1003+ # returned WebSocket's `close_transport!` is a no-op: the caller retains ownership
1004+ # of `io` and is responsible for closing it.
1005+ function _open_client_websocket_io (
1006+ io:: IO ;
1007+ target:: AbstractString = " /" ,
1008+ host:: AbstractString = " " ,
1009+ headers= Pair{String,String}[],
1010+ maxframesize:: Integer = typemax (Int),
1011+ maxfragmentation:: Integer = DEFAULT_MAX_FRAG,
1012+ subprotocols:: AbstractVector{<:AbstractString} = String[],
1013+ ):: WebSocket
1014+ req_headers = _normalize_headers_input (headers)
1015+ isempty (host) || setheader (req_headers, " Host" , host)
1016+ key = ws_random_handshake_key ()
1017+ _apply_websocket_request_headers! (req_headers, key, subprotocols)
1018+ request = Request (" GET" , String (target); headers= req_headers, host= String (host), body= EmptyBody (), content_length= 0 )
1019+ expected_accept = ws_compute_accept_key (header (request. headers, " Sec-WebSocket-Key" ):: String )
1020+ raw = _RawIOConn (io)
1021+
1022+ # Serialize and write the handshake request to the caller's transport.
1023+ request_buf = IOBuffer ()
1024+ write_request! (request_buf, request)
1025+ write (raw, take! (request_buf))
1026+
1027+ incoming = _read_incoming_response (raw, request)
1028+ @try_ignore body_close! (incoming. rawbody)
1029+ response = _streaming_response (incoming)
1030+ negotiated = _validate_websocket_upgrade! (response, expected_accept, subprotocols)
1031+
1032+ # No-op `close_transport!`: the caller retains ownership of `io`.
1033+ return _finish_client_websocket (
1034+ raw,
1035+ () -> nothing ,
1036+ request,
1037+ response,
1038+ negotiated;
1039+ maxframesize= maxframesize,
1040+ maxfragmentation= maxfragmentation,
1041+ )
1042+ end
1043+
1044+ """
1045+ open(io::IO; target="/", host="", kwargs...) -> WebSocket
1046+ open(f, io::IO; target="/", host="", kwargs...) -> Any
1047+
1048+ Perform the WebSocket client handshake directly over an already-connected `io`
1049+ (a raw `TCPSocket`, a TLS stream, or any other byte `IO`) instead of dialing a
1050+ new connection from a URL. This is useful when the transport is established
1051+ out-of-band, for testing, or for tunnelling WebSockets over a custom stream.
1052+
1053+ Because there is no URL to derive them from, the request-line `target` and `Host`
1054+ header default to `"/"` and unset; override them with the `target` and `host`
1055+ keyword arguments. The remaining `headers`, `subprotocols`, `maxframesize`, and
1056+ `maxfragmentation` keywords match the URL-based [`open`](@ref); pool, TLS, proxy,
1057+ redirect, cookie, and timeout options do not apply.
1058+
1059+ `io` is **not** closed by `open` — the caller retains ownership of the underlying
1060+ stream's lifetime. As with the URL form, calling `open` with a function closes
1061+ the WebSocket with status code `1000` when `f` returns (the close frame is sent
1062+ over `io`, but `io` itself stays open).
1063+ """
1064+ function open (
1065+ io:: IO ;
1066+ suppress_close_error:: Bool = false ,
1067+ kwargs... ,
1068+ )
1069+ return _open_client_websocket_io (io; kwargs... )
1070+ end
1071+
1072+ function open (
1073+ f:: Function ,
1074+ io:: IO ;
1075+ suppress_close_error:: Bool = false ,
1076+ kwargs... ,
1077+ )
1078+ ws = open (io; kwargs... )
1079+ try
1080+ return f (ws)
1081+ catch err
1082+ if err isa WebSocketError && isok (err)
1083+ return nothing
1084+ end
1085+ rethrow (err)
1086+ finally
1087+ if ! isclosed (ws)
1088+ try
1089+ close (ws, CloseFrameBody (1000 , " " ))
1090+ catch err
1091+ if ! (suppress_close_error && err isa WebSocketError)
1092+ rethrow (err)
1093+ end
1094+ end
1095+ end
1096+ end
1097+ end
1098+
9711099"""
9721100 Server(; network="tcp", address="127.0.0.1:0", handler, tls_config=nothing, ...)
9731101
0 commit comments