@@ -27,6 +27,7 @@ interface IncomingMessage extends NodeIncomingMessage {
2727export class Request {
2828 #body: RequestBody | undefined ;
2929 #peerCertificate: any ;
30+ #abortController = new AbortController ( ) ;
3031 public _nodeRequest : IncomingMessage ;
3132 public _nodeResponse ?: NodeServerResponse ;
3233 public method : string ;
@@ -60,14 +61,39 @@ export class Request {
6061 public lastModified ?: number ;
6162 public lastRefreshed ?: number ;
6263
63- constructor ( nodeRequest : IncomingMessage , nodeResponse : NodeServerResponse ) {
64+ constructor ( nodeRequest : IncomingMessage , nodeResponse ? : NodeServerResponse ) {
6465 this . method = nodeRequest . method ;
6566 const url = nodeRequest . url ;
6667 this . _nodeRequest = nodeRequest ;
6768 this . _nodeResponse = nodeResponse ;
6869 this . url = url ;
6970 this . headers = new RequestHeaders ( nodeRequest . headers ) ;
7071 this . __harperRequestUpgraded = false ;
72+ // Abort the request's signal on premature client disconnect. nodeResponse 'close'
73+ // also fires on clean completion; the writableFinished guard restricts to disconnect.
74+ if ( typeof nodeResponse ?. on === 'function' ) {
75+ nodeResponse . on ( 'close' , ( ) => {
76+ if ( ! nodeResponse . writableFinished ) this . #abortController. abort ( ) ;
77+ } ) ;
78+ } else if ( typeof nodeRequest . socket ?. once === 'function' ) {
79+ // No response on this Request — typically the WebSocket-upgrade path
80+ // (http.ts creates the Request before the ws library takes over). The TCP
81+ // socket close is the fallback abort trigger; REST.ts's ws.on('close') hook
82+ // also calls _abort() and is the primary signal in the WS case. The two
83+ // are redundant by design (idempotent abort) so any future single-arg
84+ // caller still gets disconnect semantics without relying on the WS layer.
85+ nodeRequest . socket . once ( 'close' , ( ) => this . #abortController. abort ( ) ) ;
86+ }
87+ }
88+ get signal ( ) : AbortSignal {
89+ return this . #abortController. signal ;
90+ }
91+ /**
92+ * Abort this request's signal. Used by transports (e.g. WebSocket) that need to
93+ * signal client-side cancellation independently of the Node response lifecycle.
94+ */
95+ _abort ( ) : void {
96+ this . #abortController. abort ( ) ;
7197 }
7298 get absoluteURL ( ) {
7399 return this . protocol + '://' + this . host + this . url ;
@@ -119,8 +145,7 @@ export class Request {
119145 return this . _nodeRequest . httpVersion ;
120146 }
121147 get isAborted ( ) {
122- // TODO: implement this
123- return false ;
148+ return this . #abortController. signal . aborted ;
124149 }
125150 // Expose node request for cases that need direct access (e.g., replication)
126151 get nodeRequest ( ) {
@@ -391,7 +416,17 @@ export class BunRequest {
391416 return '1.1' ;
392417 }
393418 get isAborted ( ) {
394- return false ;
419+ return this . _webRequest . signal ?. aborted ?? false ;
420+ }
421+ get signal ( ) : AbortSignal {
422+ // Bun.serve() aborts this signal on HTTP client disconnect. Behavior on
423+ // WebSocket-upgrade is implementation-defined; if the WS path needs
424+ // guaranteed abort-on-close under Bun, wire it through Bun's ws close
425+ // handler with a Bun-side AbortController, similar to REST.ts on Node.
426+ return this . _webRequest . signal ;
427+ }
428+ _abort ( ) : void {
429+ // On Bun, abort is driven by the underlying Web Request's signal; no-op for parity with Node path.
395430 }
396431 get nodeRequest ( ) {
397432 return null ;
0 commit comments