@@ -100,7 +100,7 @@ async function handleResponses(
100100 req : Request ,
101101 config : OcxConfig ,
102102 logCtx : { model : string ; provider : string } ,
103- options : { forceEmptyResponseId ?: boolean } = { } ,
103+ options : { forceEmptyResponseId ?: boolean ; abortSignal ?: AbortSignal } = { } ,
104104) : Promise < Response > {
105105 let body : unknown ;
106106 try {
@@ -161,6 +161,7 @@ async function handleResponses(
161161 // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
162162 // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
163163 const upstream = new AbortController ( ) ;
164+ linkAbortSignal ( upstream , options . abortSignal ) ;
164165 let upstreamResponse : Response ;
165166 try {
166167 upstreamResponse = await fetch ( request . url , {
@@ -199,6 +200,7 @@ async function handleResponses(
199200 // Abort the upstream fetch if the client (Codex) disconnects mid-stream, so a cancelled turn does
200201 // not leak the upstream connection or keep draining tokens. The bridge's cancel() fires upstream.abort() (RC2).
201202 const upstream = new AbortController ( ) ;
203+ linkAbortSignal ( upstream , options . abortSignal ) ;
202204 let upstreamResponse : Response ;
203205 try {
204206 upstreamResponse = await fetch ( request . url , {
@@ -259,6 +261,15 @@ async function handleResponses(
259261 return formatErrorResponse ( 500 , "internal_error" , "Non-streaming not supported by this adapter" ) ;
260262}
261263
264+ export function linkAbortSignal ( upstream : AbortController , signal ?: AbortSignal ) : void {
265+ if ( ! signal ) return ;
266+ if ( signal . aborted ) {
267+ upstream . abort ( signal . reason ) ;
268+ return ;
269+ }
270+ signal . addEventListener ( "abort" , ( ) => upstream . abort ( signal . reason ) , { once : true } ) ;
271+ }
272+
262273const requestLog : { timestamp : number ; model : string ; provider : string ; status : number ; durationMs : number } [ ] = [ ] ;
263274const MAX_LOG_SIZE = 200 ;
264275
@@ -620,12 +631,18 @@ export function startServer(port?: number) {
620631 const turnId = ( ws . data . turnId ?? 0 ) + 1 ;
621632 ws . data . turnId = turnId ;
622633 const isCurrent = ( ) => ws . data . turnId === turnId ;
634+ const turnAbort = new AbortController ( ) ;
635+ const cancelTurn = ( ) => {
636+ turnAbort . abort ( "websocket turn superseded or closed" ) ;
637+ } ;
638+ ws . data . cancel = cancelTurn ;
623639
624640 if ( frame . generate === false ) {
625641 for ( const payload of buildWarmupCompletionFrames ( frame ) ) {
626642 if ( ! isCurrent ( ) ) return ;
627643 sendTextFrame ( ws , payload ) ;
628644 }
645+ if ( ws . data . cancel === cancelTurn ) ws . data . cancel = undefined ;
629646 return ;
630647 }
631648
@@ -641,7 +658,10 @@ export function startServer(port?: number) {
641658 body : JSON . stringify ( { ...payload , stream : true } ) ,
642659 } ) ;
643660 try {
644- const response = await handleResponses ( req , config , logCtx , { forceEmptyResponseId : true } ) ;
661+ const response = await handleResponses ( req , config , logCtx , {
662+ forceEmptyResponseId : true ,
663+ abortSignal : turnAbort . signal ,
664+ } ) ;
645665 await sendResponseToWebSocket ( ws , response , isCurrent ) ;
646666 } catch ( err ) {
647667 if ( ! isCurrent ( ) ) return ;
@@ -653,6 +673,8 @@ export function startServer(port?: number) {
653673 } catch {
654674 /* socket already gone or send dropped */
655675 }
676+ } finally {
677+ if ( ws . data . cancel === cancelTurn ) ws . data . cancel = undefined ;
656678 }
657679 } ) ( ) ;
658680 } ,
0 commit comments