@@ -9,10 +9,10 @@ import { Env } from '@catbee/utils/env';
99import { getLogger } from '@catbee/utils/logger' ;
1010import { InternalServerErrorException , ServiceUnavailableException , NotFoundException } from '@catbee/utils/exception' ;
1111import { getCatbeeServerGlobalConfig } from '@catbee/utils/config' ;
12- import { deepObjMerge } from '@catbee/utils/object' ;
12+ import { deepObjMerge , isPlainObject } from '@catbee/utils/object' ;
1313import { fileExists , readFileSync , readFile } from '@catbee/utils/fs' ;
1414import { CatbeeServerConfig , CatbeeServerHooks } from '@catbee/utils/types' ;
15- import { isPort } from '@catbee/utils/validation' ;
15+ import { isPort , isHostname } from '@catbee/utils/validation' ;
1616import { optionalRequire } from '@catbee/utils/async' ;
1717import { BUILD_MARKER } from './server.builder' ;
1818import { uuid } from '@catbee/utils/id' ;
@@ -109,14 +109,19 @@ export class ExpressServer {
109109 */
110110 constructor ( config : Partial < CatbeeServerConfig > , hooks : CatbeeServerHooks = { } ) {
111111 if ( this . hasBuildMarker ( config ) ) {
112- this . config = config as Required < CatbeeServerConfig > ;
112+ this . config = deepObjMerge ( { } , config ) as Required < CatbeeServerConfig > ;
113113 } else {
114114 // Deep merge config with user overrides
115115 this . config = deepObjMerge ( { } , getCatbeeServerGlobalConfig ( ) , config ) as Required < CatbeeServerConfig > ;
116116 }
117117
118- if ( ! isPort ( this . config . port ) ) {
119- const msg = `Port must be a valid number between 1 and 65535, got: ${ this . config . port } ` ;
118+ // Normalize host (strip brackets from IPv6 addresses for server.listen compatibility)
119+ if ( this . config . host ) {
120+ this . config . host = this . normalizeHost ( this . config . host ) ;
121+ }
122+
123+ if ( ! isPort ( this . config . port , true ) ) {
124+ const msg = `Port must be a valid number between 0 and 65535, got: ${ this . config . port } ` ;
120125 getLogger ( ) . error ( msg ) ;
121126 throw new Error ( msg ) ;
122127 }
@@ -415,7 +420,6 @@ export class ExpressServer {
415420
416421 const logger = getLogger ( ) ;
417422 const incomingRequestMetaData = {
418- requestId : req . id ,
419423 method : req . method ,
420424 url : req . originalUrl || req . url ,
421425 ip : req . ip
@@ -474,13 +478,23 @@ export class ExpressServer {
474478 * Set up body parsing middleware.
475479 */
476480 private setupBodyParsingMiddleware ( ) : void {
477- if ( this . config . bodyParser ) {
481+ if ( isPlainObject ( this . config . bodyParser ) ) {
478482 if ( this . config . bodyParser . json ) {
479483 this . app . use ( express . json ( this . config . bodyParser . json ) ) ;
480484 }
481485 if ( this . config . bodyParser . urlencoded ) {
482486 this . app . use ( express . urlencoded ( this . config . bodyParser . urlencoded ) ) ;
483487 }
488+ } else if ( this . config . bodyParser === true ) {
489+ const globalBodyParserConfig = getCatbeeServerGlobalConfig ( ) . bodyParser ;
490+ if ( isPlainObject ( globalBodyParserConfig ) ) {
491+ if ( globalBodyParserConfig . json ) {
492+ this . app . use ( express . json ( globalBodyParserConfig . json ) ) ;
493+ }
494+ if ( globalBodyParserConfig . urlencoded ) {
495+ this . app . use ( express . urlencoded ( globalBodyParserConfig . urlencoded ) ) ;
496+ }
497+ }
484498 }
485499 }
486500
@@ -864,7 +878,9 @@ export class ExpressServer {
864878 */
865879 private logServerStartInfo ( ) : void {
866880 const protocol = this . config . https ? 'https' : 'http' ;
867- const url = `${ protocol } ://${ this . config . host } :${ this . config . port } ` ;
881+ const port = this . getPort ( ) ;
882+ const host = this . formatHostForUrl ( this . config . host || 'localhost' ) ;
883+ const url = `${ protocol } ://${ host } :${ port } ` ;
868884 getLogger ( ) . info ( `Server running on ${ url } ` ) ;
869885
870886 if ( this . config . healthCheck ?. path ) {
@@ -1112,6 +1128,118 @@ export class ExpressServer {
11121128 return this . config ;
11131129 }
11141130
1131+ /**
1132+ * Get the port the server is listening on.
1133+ * Returns the actual port if server is running (useful when config.port was 0),
1134+ * otherwise returns the configured port.
1135+ *
1136+ * @returns The port number
1137+ */
1138+ public getPort ( ) : number {
1139+ const address = this . server ?. address ( ) ;
1140+ if ( address && typeof address === 'object' && 'port' in address ) {
1141+ return address . port ;
1142+ }
1143+ return this . config . port ;
1144+ }
1145+
1146+ /**
1147+ * Get the full URL the server is running on.
1148+ * Returns the actual URL if server is running (useful when config.port was 0),
1149+ * otherwise returns the configured URL.
1150+ *
1151+ * @returns The full server URL (e.g., "http://localhost:3000")
1152+ */
1153+ public getUrl ( ) : string {
1154+ const protocol = this . config . https ? 'https' : 'http' ;
1155+ const port = this . getPort ( ) ;
1156+ const host = this . formatHostForUrl ( this . config . host || 'localhost' ) ;
1157+ return `${ protocol } ://${ host } :${ port } ` ;
1158+ }
1159+
1160+ /**
1161+ * Check if the server is configured for dynamic port assignment.
1162+ * Returns true if the original port configuration was 0.
1163+ *
1164+ * @returns True if using dynamic port assignment, false otherwise
1165+ */
1166+ public isPortDynamic ( ) : boolean {
1167+ return this . config . port === 0 ;
1168+ }
1169+
1170+ /**
1171+ * Check if the server is currently running and listening for requests.
1172+ *
1173+ * @returns True if server is running, false otherwise
1174+ */
1175+ public isRunning ( ) : boolean {
1176+ return this . server !== null && this . server . listening ;
1177+ }
1178+
1179+ /**
1180+ * Get the host address the server is bound to.
1181+ *
1182+ * @returns The host address
1183+ */
1184+ public getHost ( ) : string {
1185+ return this . config . host || 'localhost' ;
1186+ }
1187+
1188+ /**
1189+ * Get the protocol the server is using ('http' or 'https').
1190+ *
1191+ * @returns The protocol string
1192+ */
1193+ public getProtocol ( ) : string {
1194+ return this . config . https ? 'https' : 'http' ;
1195+ }
1196+
1197+ /**
1198+ * Check if the server is configured to use HTTPS.
1199+ *
1200+ * @returns True if using HTTPS, false otherwise
1201+ */
1202+ public isHttps ( ) : boolean {
1203+ return this . config . https !== undefined ;
1204+ }
1205+
1206+ /**
1207+ * Set a new port for the server.
1208+ * Can only be called before the server starts listening.
1209+ * Useful for testing scenarios where you need to change the port dynamically.
1210+ *
1211+ * @param port - The new port number (0-65535)
1212+ * @throws Error if server is already running or port is invalid
1213+ */
1214+ public setPort ( port : number ) : void {
1215+ if ( this . server ) {
1216+ throw new Error ( 'Cannot change port after server has started' ) ;
1217+ }
1218+ if ( ! isPort ( port , true ) ) {
1219+ throw new Error ( `Port must be a valid number between 0 and 65535, got: ${ port } ` ) ;
1220+ }
1221+ this . config . port = port ;
1222+ }
1223+
1224+ /**
1225+ * Set a new host for the server.
1226+ * Can only be called before the server starts listening.
1227+ * Useful for testing scenarios where you need to change the host dynamically.
1228+ *
1229+ * @param host - The new host (e.g., "localhost", "0.0.0.0", "127.0.0.1", "::1", or "[::1]")
1230+ * @throws {Error } If server is already running or host is invalid
1231+ */
1232+ public setHost ( host : string ) : void {
1233+ if ( this . server ) {
1234+ throw new Error ( 'Cannot change host after server has started' ) ;
1235+ }
1236+ const normalizedHost = this . normalizeHost ( host ) ;
1237+ if ( ! isHostname ( normalizedHost ) ) {
1238+ throw new Error ( `Host must be a valid hostname or IP address, got: ${ host } ` ) ;
1239+ }
1240+ this . config . host = normalizedHost ;
1241+ }
1242+
11151243 /**
11161244 * Wait until server initialization (middleware + routes) has completed.
11171245 * Useful for integration tests that inspect app before starting.
@@ -1120,6 +1248,32 @@ export class ExpressServer {
11201248 await this . initPromise ;
11211249 }
11221250
1251+ /**
1252+ * Normalize host by stripping surrounding brackets from IPv6 addresses.
1253+ * This ensures the host value is compatible with server.listen().
1254+ * Brackets are URL syntax only and must be removed for Node.js binding.
1255+ */
1256+ private normalizeHost ( host : string ) : string {
1257+ // Strip surrounding brackets from IPv6 addresses (e.g., "[::1]" -> "::1")
1258+ if ( host . startsWith ( '[' ) && host . endsWith ( ']' ) ) {
1259+ return host . slice ( 1 , - 1 ) ;
1260+ }
1261+ return host ;
1262+ }
1263+
1264+ /**
1265+ * Format host for use in URLs.
1266+ * Wraps IPv6 addresses in brackets per RFC 3986.
1267+ */
1268+ private formatHostForUrl ( host : string ) : string {
1269+ // IPv6 addresses contain colons and need to be wrapped in brackets
1270+ // Since we normalize at input (strip brackets), we never have pre-bracketed hosts
1271+ if ( host . includes ( ':' ) ) {
1272+ return `[${ host } ]` ;
1273+ }
1274+ return host ;
1275+ }
1276+
11231277 private normalizePath ( path : string , withGlobalPrefix = false ) : string {
11241278 const sanitize = ( p : string ) : string => {
11251279 return (
0 commit comments