@@ -154,63 +154,102 @@ for (const path of paths) {
154154const upstreamSockets = new WeakMap < object , WebSocket > ( ) ;
155155const messageBuffers = new WeakMap < object , ( string | ArrayBuffer ) [ ] > ( ) ;
156156
157- function wsOpen ( ws : any ) {
158- const { params, requestUrl } = ws . data ;
159- const editorId = params . editorId ;
160-
161- const editor = codeEditorService . getEditor ( editorId ) ;
162- if ( ! editor || ( editor . status !== 'running' && editor . status !== 'starting' ) ) {
163- ws . close ( ) ;
164- return ;
157+ function buildUpstreamWsUrl ( ws : any , editorId : string , port : number ) : string {
158+ // Primary: use requestUrl captured by derive()
159+ const requestUrl : string | undefined = ws . data . requestUrl ;
160+ if ( requestUrl ) {
161+ const url = new URL ( requestUrl ) ;
162+ const strippedPath = stripProxyPrefix ( url . pathname , editorId ) ;
163+ return `ws://127.0.0.1:${ port } ${ strippedPath } ${ url . search } ` ;
165164 }
166165
167- const url = new URL ( requestUrl ) ;
168- const strippedPath = stripProxyPrefix ( url . pathname , editorId ) ;
169- const targetUrl = `ws://127.0.0.1:${ editor . port } ${ strippedPath } ${ url . search } ` ;
170-
171- // Forward Sec-WebSocket-Protocol if the client sent one
172- const protocolHeader : string | undefined = ws . data . headers ?. [ 'sec-websocket-protocol' ] ;
173- const protocols = protocolHeader
174- ? protocolHeader . split ( ',' ) . map ( ( p : string ) => p . trim ( ) )
175- : undefined ;
166+ // Fallback: reconstruct from route params and query
167+ console . warn ( '[EditorProxy] WS: requestUrl missing from context, reconstructing from params/query' ) ;
168+ const wildcardPath = ws . data . params [ '*' ] || '' ;
169+ const forwardPath = wildcardPath ? `/${ wildcardPath } ` : '/' ;
170+ const queryObj = ws . data . query || { } ;
171+ const qs = new URLSearchParams ( queryObj as Record < string , string > ) . toString ( ) ;
172+ return `ws://127.0.0.1:${ port } ${ forwardPath } ${ qs ? '?' + qs : '' } ` ;
173+ }
176174
177- const upstream = new WebSocket ( targetUrl , protocols ) ;
178- upstream . binaryType = 'arraybuffer' ;
179- upstreamSockets . set ( ws , upstream ) ;
175+ function wsOpen ( ws : any ) {
176+ const editorId = ws . data . params . editorId ;
180177
181- const buffer : ( string | ArrayBuffer ) [ ] = [ ] ;
182- messageBuffers . set ( ws , buffer ) ;
178+ try {
179+ const editor = codeEditorService . getEditor ( editorId ) ;
180+ if ( ! editor || ( editor . status !== 'running' && editor . status !== 'starting' ) ) {
181+ console . warn ( `[EditorProxy] WS: Editor ${ editorId } not available (status: ${ editor ?. status ?? 'not found' } )` ) ;
182+ ws . close ( ) ;
183+ return ;
184+ }
183185
184- upstream . addEventListener ( 'open' , ( ) => {
185- for ( const msg of buffer ) upstream . send ( msg ) ;
186- buffer . length = 0 ;
187- } ) ;
186+ const targetUrl = buildUpstreamWsUrl ( ws , editorId , editor . port ) ;
187+ console . log ( `[EditorProxy] WS open: editor=${ editorId } → ${ targetUrl } ` ) ;
188+
189+ // Forward Sec-WebSocket-Protocol if the client sent one
190+ const headers = ws . data . headers || { } ;
191+ const protocolHeader : string | undefined = headers [ 'sec-websocket-protocol' ] ;
192+ const protocols = protocolHeader
193+ ? protocolHeader . split ( ',' ) . map ( ( p : string ) => p . trim ( ) )
194+ : undefined ;
195+
196+ const upstream = protocols ?. length
197+ ? new WebSocket ( targetUrl , protocols )
198+ : new WebSocket ( targetUrl ) ;
199+ upstream . binaryType = 'arraybuffer' ;
200+ upstreamSockets . set ( ws , upstream ) ;
201+
202+ const buffer : ( string | ArrayBuffer ) [ ] = [ ] ;
203+ messageBuffers . set ( ws , buffer ) ;
204+
205+ upstream . addEventListener ( 'open' , ( ) => {
206+ console . log ( `[EditorProxy] WS upstream connected: editor=${ editorId } ` ) ;
207+ for ( const msg of buffer ) upstream . send ( msg ) ;
208+ buffer . length = 0 ;
209+ } ) ;
188210
189- upstream . addEventListener ( 'message' , ( event : MessageEvent ) => {
190- try {
191- if ( typeof event . data === 'string' ) {
192- ws . send ( event . data ) ;
193- } else {
194- // Must use ws.raw.send() for binary data — Elysia's ws.send()
195- // JSON-stringifies objects (including Uint8Array), corrupting binary frames.
196- ws . raw . send ( new Uint8Array ( event . data as ArrayBuffer ) ) ;
211+ upstream . addEventListener ( 'message' , ( event : MessageEvent ) => {
212+ try {
213+ if ( typeof event . data === 'string' ) {
214+ ws . send ( event . data ) ;
215+ } else {
216+ // Must use ws.raw.send() for binary data — Elysia's ws.send()
217+ // JSON-stringifies objects (including Uint8Array), corrupting binary frames.
218+ ws . raw . send ( new Uint8Array ( event . data as ArrayBuffer ) ) ;
219+ }
220+ } catch {
221+ // Client already disconnected
197222 }
198- } catch {
199- // Client already disconnected
200- }
201- } ) ;
223+ } ) ;
202224
203- upstream . addEventListener ( 'close' , ( ) => {
204- upstreamSockets . delete ( ws ) ;
205- messageBuffers . delete ( ws ) ;
206- try { ws . close ( ) ; } catch { }
207- } ) ;
225+ upstream . addEventListener ( 'close' , ( event : CloseEvent ) => {
226+ console . log ( `[EditorProxy] WS upstream closed: editor=${ editorId } code=${ event . code } ` ) ;
227+ upstreamSockets . delete ( ws ) ;
228+ messageBuffers . delete ( ws ) ;
229+ try { ws . close ( ) ; } catch { }
230+ } ) ;
208231
209- upstream . addEventListener ( 'error' , ( ) => {
210- upstreamSockets . delete ( ws ) ;
211- messageBuffers . delete ( ws ) ;
232+ upstream . addEventListener ( 'error' , ( event : Event ) => {
233+ console . error ( `[EditorProxy] WS upstream error: editor=${ editorId } ` , event ) ;
234+ upstreamSockets . delete ( ws ) ;
235+ messageBuffers . delete ( ws ) ;
236+ try { ws . close ( ) ; } catch { }
237+ } ) ;
238+
239+ // Safety timeout: if upstream never connects, clean up
240+ setTimeout ( ( ) => {
241+ if ( upstream . readyState === WebSocket . CONNECTING ) {
242+ console . error ( `[EditorProxy] WS upstream connect timeout (10s): editor=${ editorId } ` ) ;
243+ upstream . close ( ) ;
244+ upstreamSockets . delete ( ws ) ;
245+ messageBuffers . delete ( ws ) ;
246+ try { ws . close ( ) ; } catch { }
247+ }
248+ } , 10_000 ) ;
249+ } catch ( err ) {
250+ console . error ( `[EditorProxy] WS open error: editor=${ editorId } ` , err ) ;
212251 try { ws . close ( ) ; } catch { }
213- } ) ;
252+ }
214253}
215254
216255function wsMessage ( ws : any , message : string | Buffer ) {
@@ -247,10 +286,11 @@ function wsClose(ws: any) {
247286
248287const wsHandler = { open : wsOpen , message : wsMessage , close : wsClose } ;
249288
250- // .resolve() captures request URL and headers for the ws open handler (runs before upgrade).
251- base . resolve ( ( { request } ) => ( {
289+ // derive() captures the raw request URL for the ws open handler.
290+ // Using derive (not resolve) because resolve can fail to pass data to ws handlers in Elysia.
291+ // We only store requestUrl — Elysia's default ws.data.headers already has request headers.
292+ base . derive ( ( { request } ) => ( {
252293 requestUrl : request . url ,
253- headers : Object . fromEntries ( request . headers . entries ( ) ) ,
254294} ) ) ;
255295
256296for ( const path of paths ) {
0 commit comments