@@ -491,6 +491,8 @@ fn prepare_server_config_with_drivers(
491491 ) ;
492492
493493 validate_listener_ports ( bind, service_bind, health_bind, metrics_bind) ?;
494+ config. service_public_port = file_gateway. and_then ( |g| g. service_public_port ) ;
495+ validate_service_public_port ( config. service_public_port ) ?;
494496 if let Some ( addr) = service_bind {
495497 config = config. with_service_bind_address ( addr) ;
496498 }
@@ -1020,6 +1022,7 @@ fn validate_preflight_semantics(
10201022 "service_port" ,
10211023 || gateway. service_bind_address ,
10221024 ) ;
1025+ validate_service_public_port ( gateway. service_public_port ) ?;
10231026 let health_bind = resolve_aux_listener (
10241027 args. bind_address ,
10251028 args. health_port ,
@@ -1065,6 +1068,15 @@ fn validate_listener_ports(
10651068 Ok ( ( ) )
10661069}
10671070
1071+ fn validate_service_public_port ( port : Option < u16 > ) -> Result < ( ) > {
1072+ if port == Some ( 0 ) {
1073+ return Err ( miette:: miette!(
1074+ "service_public_port requires a nonzero port"
1075+ ) ) ;
1076+ }
1077+ Ok ( ( ) )
1078+ }
1079+
10681080fn resolve_config_path ( args : & RunArgs ) -> Result < Option < PathBuf > > {
10691081 if let Some ( path) = args. config . clone ( ) {
10701082 return Ok ( Some ( path) ) ;
@@ -2373,6 +2385,10 @@ mod tests {
23732385 "service-listener-zero" ,
23742386 "[openshell]\n version = 2\n [openshell.gateway]\n service_bind_address = '0.0.0.0:0'\n disable_tls = true\n " ,
23752387 ) ,
2388+ (
2389+ "service-public-port-zero" ,
2390+ "[openshell]\n version = 2\n [openshell.gateway]\n service_public_port = 0\n disable_tls = true\n " ,
2391+ ) ,
23762392 (
23772393 "driver-selector" ,
23782394 "[openshell]\n version = 2\n [openshell.gateway]\n compute_driver = 'secret-driver-marker'\n disable_tls = true\n " ,
@@ -3170,6 +3186,35 @@ service_bind_address = '0.0.0.0:8082'
31703186 ) ;
31713187 }
31723188
3189+ #[ test]
3190+ fn service_public_port_is_loaded_without_rebinding_the_listener ( ) {
3191+ let _lock = ENV_LOCK
3192+ . lock ( )
3193+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
3194+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
3195+ let path = dir. path ( ) . join ( "gateway.toml" ) ;
3196+ std:: fs:: write (
3197+ & path,
3198+ "[openshell]\n version = 2\n [openshell.gateway]\n service_bind_address = '0.0.0.0:8082'\n service_public_port = 32082\n disable_tls = true\n " ,
3199+ )
3200+ . unwrap ( ) ;
3201+ let _config = EnvVarGuard :: set ( "OPENSHELL_GATEWAY_CONFIG" , path. to_str ( ) . unwrap ( ) ) ;
3202+ let _service_port = EnvVarGuard :: remove ( "OPENSHELL_SERVICE_PORT" ) ;
3203+ let _legacy = EnvVarGuard :: remove ( "OPENSHELL_DRIVERS" ) ;
3204+ let registry = test_registry ( "shared" , false ) ;
3205+ let ( mut args, matches) = parse_with_args ( & [
3206+ "openshell-gateway" ,
3207+ "--db-url" ,
3208+ "sqlite::memory:" ,
3209+ "--compute-driver" ,
3210+ "shared" ,
3211+ ] ) ;
3212+ let prepared =
3213+ super :: prepare_server_config_with_drivers ( & mut args, & matches, & registry) . unwrap ( ) ;
3214+ assert_eq ! ( prepared. config. service_address( ) . port( ) , 8082 ) ;
3215+ assert_eq ! ( prepared. config. advertised_service_port( ) , Some ( 32082 ) ) ;
3216+ }
3217+
31733218 #[ test]
31743219 fn service_listener_rejects_zero_and_collisions_with_all_other_ports ( ) {
31753220 let primary = "127.0.0.1:8080" . parse ( ) . unwrap ( ) ;
0 commit comments