@@ -63,6 +63,7 @@ class HttpRequestConnection extends Connection {
6363 this . _capability = capability ;
6464 this . _payload = payload ;
6565 this . _replied = false ;
66+ this . _closed = false ;
6667 // Synthesised frame id so the dispatcher can correlate even though
6768 // the HTTP path doesn't carry one on the wire.
6869 this . _id = '__http__' ;
@@ -84,7 +85,9 @@ class HttpRequestConnection extends Connection {
8485 /**
8586 * Receive a reply from the dispatcher. Translates `{ok, payload,
8687 * error, code}` into the appropriate HTTP response. Subsequent calls
87- * are no-ops — HTTP is one-shot.
88+ * are no-ops — HTTP is one-shot. Always emits `'close'` exactly once
89+ * after the reply is written so the dispatcher's per-connection
90+ * cleanup runs and lazy-created Publishers/Clients are released.
8891 */
8992 send ( frame ) {
9093 if ( this . _replied ) return ;
@@ -98,7 +101,7 @@ class HttpRequestConnection extends Connection {
98101 'streaming_unsupported' ,
99102 'streaming events are not supported over HTTP'
100103 ) ;
101- return ;
104+ return this . _emitCloseOnce ( ) ;
102105 }
103106
104107 if ( frame . ok === true ) {
@@ -113,13 +116,14 @@ class HttpRequestConnection extends Connection {
113116 } ) ;
114117 this . res . end ( body ) ;
115118 }
116- return ;
119+ return this . _emitCloseOnce ( ) ;
117120 }
118121
119122 // Failure path.
120123 const code = frame . code || 'internal_error' ;
121124 const status = _STATUS_BY_CODE [ code ] || 500 ;
122125 this . _writeError ( status , code , frame . error || 'request failed' ) ;
126+ this . _emitCloseOnce ( ) ;
123127 }
124128
125129 /** Force-close the response. The dispatcher's `cleanup` will follow. */
@@ -128,6 +132,16 @@ class HttpRequestConnection extends Connection {
128132 if ( ! this . _replied ) {
129133 this . _writeError ( 500 , 'internal_error' , 'connection closed' ) ;
130134 }
135+ this . _emitCloseOnce ( ) ;
136+ }
137+
138+ /**
139+ * Idempotent `'close'` emitter — guarantees the dispatcher's
140+ * cleanup runs exactly once even if `send()` and `close()` race.
141+ */
142+ _emitCloseOnce ( ) {
143+ if ( this . _closed ) return ;
144+ this . _closed = true ;
131145 this . emit ( 'close' ) ;
132146 }
133147
@@ -222,12 +236,25 @@ class HttpTransport extends TransportAdapter {
222236 // ---------- internals ----------
223237
224238 _route ( req , res ) {
225- if ( this . verifyRequest && this . verifyRequest ( req ) === false ) {
226- return _writeJson ( res , 401 , {
227- ok : false ,
228- error : 'unauthorized' ,
229- code : 'unauthorized' ,
230- } ) ;
239+ if ( this . verifyRequest ) {
240+ let allowed ;
241+ try {
242+ allowed = this . verifyRequest ( req ) ;
243+ } catch ( e ) {
244+ debug ( 'verifyRequest threw: %s' , ( e && e . stack ) || e ) ;
245+ return _writeJson ( res , 500 , {
246+ ok : false ,
247+ error : 'verifyRequest hook failed' ,
248+ code : 'internal_error' ,
249+ } ) ;
250+ }
251+ if ( allowed === false ) {
252+ return _writeJson ( res , 401 , {
253+ ok : false ,
254+ error : 'unauthorized' ,
255+ code : 'unauthorized' ,
256+ } ) ;
257+ }
231258 }
232259
233260 let pathname ;
@@ -261,7 +288,16 @@ class HttpTransport extends TransportAdapter {
261288 } ) ;
262289 }
263290 const kind = tail . slice ( 0 , slash ) ;
264- const name = '/' + decodeURIComponent ( tail . slice ( slash + 1 ) ) ;
291+ let name ;
292+ try {
293+ name = '/' + decodeURIComponent ( tail . slice ( slash + 1 ) ) ;
294+ } catch {
295+ return _writeJson ( res , 400 , {
296+ ok : false ,
297+ error : `invalid percent-encoding in capability name: ${ tail . slice ( slash + 1 ) } ` ,
298+ code : 'invalid_url' ,
299+ } ) ;
300+ }
265301
266302 if ( kind !== 'call' && kind !== 'publish' ) {
267303 return _writeJson ( res , 404 , {
@@ -308,6 +344,16 @@ function _writeJson(res, status, body) {
308344}
309345
310346function _readJsonBody ( req , maxBytes , cb ) {
347+ // Guard against double-callback: the body may exceed maxBytes (we call
348+ // cb(err) and req.destroy()), and the destroy itself can synchronously
349+ // emit 'error' which would otherwise reach cb(err) a second time.
350+ let done = false ;
351+ const finish = ( err , value ) => {
352+ if ( done ) return ;
353+ done = true ;
354+ cb ( err , value ) ;
355+ } ;
356+
311357 const ctype = ( req . headers [ 'content-type' ] || '' ) . toLowerCase ( ) ;
312358 if (
313359 req . method === 'POST' &&
@@ -322,35 +368,38 @@ function _readJsonBody(req, maxBytes, cb) {
322368 `unsupported content-type: ${ ctype } (expected application/json)`
323369 ) ;
324370 e . code = 'invalid_content_type' ;
325- return cb ( e ) ;
371+ return finish ( e ) ;
326372 }
327373 }
328374 let total = 0 ;
329375 const chunks = [ ] ;
330376 req . on ( 'data' , ( chunk ) => {
377+ if ( done ) return ;
331378 total += chunk . length ;
332379 if ( total > maxBytes ) {
333- req . destroy ( ) ;
334380 const e = new Error ( `request body exceeds ${ maxBytes } bytes` ) ;
335381 e . code = 'payload_too_large' ;
336- return cb ( e ) ;
382+ finish ( e ) ;
383+ req . destroy ( ) ;
384+ return ;
337385 }
338386 chunks . push ( chunk ) ;
339387 } ) ;
340388 req . on ( 'end' , ( ) => {
389+ if ( done ) return ;
341390 const raw = Buffer . concat ( chunks ) . toString ( 'utf8' ) ;
342- if ( ! raw ) return cb ( null , { } ) ;
391+ if ( ! raw ) return finish ( null , { } ) ;
343392 try {
344- cb ( null , JSON . parse ( raw ) ) ;
393+ finish ( null , JSON . parse ( raw ) ) ;
345394 } catch ( e ) {
346395 const err = new Error ( `invalid JSON: ${ e . message } ` ) ;
347396 err . code = 'invalid_json' ;
348- cb ( err ) ;
397+ finish ( err ) ;
349398 }
350399 } ) ;
351400 req . on ( 'error' , ( e ) => {
352401 e . code = e . code || 'request_error' ;
353- cb ( e ) ;
402+ finish ( e ) ;
354403 } ) ;
355404}
356405
0 commit comments