@@ -155,7 +155,7 @@ export async function listen(handler: RequestListener, options: ListenOptions =
155155 const isolatedEnvironment = options . hostname === undefined && ! options . public && detectIsolatedEnvironment ( )
156156 const hostname = validateHostname ( options . hostname , options . public ) ?? ( options . public || isolatedEnvironment ? '' : 'localhost' )
157157
158- const requestedPort = options . port === undefined || options . port === '' ? undefined : Number ( options . port )
158+ const requestedPort = parsePort ( options . port )
159159 const port = options . handover && requestedPort
160160 ? requestedPort
161161 : await resolvePort ( requestedPort , hostname , options . strictPort )
@@ -191,15 +191,16 @@ export async function listen(handler: RequestListener, options: ListenOptions =
191191 tunnel = await startTunnel ( `${ protocol } ://localhost:${ address . port } ` , ! ! certificate )
192192 }
193193
194+ const tunnelURL = tunnel ?. url && tunnel . url + baseURL
194195 const portless = resolvePortlessURLs ( )
195196 const portlessURL = portless . url && portless . url + baseURL
196197 const portlessShareURL = portless . shareURL && portless . shareURL + baseURL
197198 const stackblitzURL = resolveStackblitzURL ( )
198199
199200 function getURLs ( ) : ListenURL [ ] {
200201 const urls : ListenURL [ ] = [ ]
201- if ( tunnel ) {
202- urls . push ( { url : tunnel . url , type : 'tunnel' } )
202+ if ( tunnelURL ) {
203+ urls . push ( { url : tunnelURL , type : 'tunnel' } )
203204 }
204205 for ( const portlessURL of portless . all ) {
205206 urls . push ( { url : portlessURL + baseURL , type : 'public' } )
@@ -221,7 +222,7 @@ export async function listen(handler: RequestListener, options: ListenOptions =
221222
222223 // The StackBlitz URL points at the editor rather than at a host another
223224 // device can open, so it is not a QR code candidate.
224- const shareableURL = options . publicURL || tunnel ?. url || portlessShareURL || portlessURL
225+ const shareableURL = options . publicURL || tunnelURL || portlessShareURL || portlessURL
225226 const publicURL = shareableURL || stackblitzURL
226227
227228 const qrURL = options . qr === false
@@ -278,31 +279,35 @@ export async function listen(handler: RequestListener, options: ListenOptions =
278279 https : certificate ,
279280 getURLs,
280281 showURLs,
281- close : async ( ) => {
282- await tunnel ?. close ( )
283- return new Promise < void > ( ( resolve , reject ) => {
284- let forceClose : NodeJS . Timeout | undefined
285- server . close ( ( error ) => {
286- if ( forceClose ) {
287- clearTimeout ( forceClose )
288- }
289- if ( error ) {
290- reject ( error )
291- }
292- else {
293- resolve ( )
294- }
295- } )
296- // Sockets waiting on keep-alive are closed at once, so shutdown is only
297- // delayed while a request is actually being served.
298- server . closeIdleConnections ?.( )
299- forceClose = setTimeout ( ( ) => server . closeAllConnections ?.( ) , CONNECTION_DRAIN_TIMEOUT_MS )
300- forceClose . unref ( )
301- } )
302- } ,
282+ close : ( ) => Promise . all ( [
283+ tunnel ?. close ( ) ,
284+ closeServer ( server ) ,
285+ ] ) . then ( ( ) => { } ) ,
303286 }
304287}
305288
289+ function closeServer ( server : HttpServer ) : Promise < void > {
290+ return new Promise < void > ( ( resolve , reject ) => {
291+ let forceClose : NodeJS . Timeout | undefined
292+ server . close ( ( error ) => {
293+ if ( forceClose ) {
294+ clearTimeout ( forceClose )
295+ }
296+ if ( error ) {
297+ reject ( error )
298+ }
299+ else {
300+ resolve ( )
301+ }
302+ } )
303+ // Sockets waiting on keep-alive are closed at once, so shutdown is only
304+ // delayed while a request is actually being served.
305+ server . closeIdleConnections ?.( )
306+ forceClose = setTimeout ( ( ) => server . closeAllConnections ?.( ) , CONNECTION_DRAIN_TIMEOUT_MS )
307+ forceClose . unref ( )
308+ } )
309+ }
310+
306311function bindServer ( server : HttpServer , port : number , hostname : string , reusePort : boolean ) : Promise < void > {
307312 return new Promise < void > ( ( resolve , reject ) => {
308313 const onError = ( error : NodeJS . ErrnoException ) => {
@@ -372,6 +377,17 @@ export function isReusePortSupported(): Promise<boolean> {
372377 return reusePortSupport
373378}
374379
380+ export function parsePort ( value : string | number | undefined ) : number | undefined {
381+ if ( value === undefined || value === '' ) {
382+ return undefined
383+ }
384+ const port = Number ( value )
385+ if ( ! Number . isInteger ( port ) || port < 0 || port > 65_535 ) {
386+ throw new Error ( `Invalid port \`${ value } \`; expected an integer between 0 and 65535.` )
387+ }
388+ return port
389+ }
390+
375391async function resolvePort ( requestedPort : number | undefined , hostname : string , strictPort ?: boolean ) : Promise < number > {
376392 if ( requestedPort === 0 ) {
377393 return getPort ( { random : true , host : hostname || undefined } )
0 commit comments