@@ -22,6 +22,24 @@ pub fn bind() -> Result<CallbackListener> {
2222 Ok ( CallbackListener { listener } )
2323}
2424
25+ /// The line an operator needs when the preferred port was taken.
26+ ///
27+ /// `None` when the listener got port 1455. Otherwise a sentence naming the
28+ /// conflict, because the failure it predicts is otherwise unreadable: if
29+ /// `OpenAI` pins the redirect URI to 1455, the authorization server rejects the
30+ /// sign-in with a generic `invalid redirect_uri` and nothing on screen connects
31+ /// that to a port another process is holding.
32+ #[ must_use]
33+ pub fn fallback_notice ( port : u16 ) -> Option < String > {
34+ ( port != PREFERRED_PORT ) . then ( || {
35+ format ! (
36+ "warning: port {PREFERRED_PORT} was busy, so sign-in is listening on port {port}. \
37+ If the browser reports an invalid redirect URI, close whatever holds \
38+ port {PREFERRED_PORT} (a running codex, or another llm-stream --login) and try again."
39+ )
40+ } )
41+ }
42+
2543impl CallbackListener {
2644 pub fn port ( & self ) -> Result < u16 > {
2745 Ok ( self . listener . local_addr ( ) ?. port ( ) )
@@ -104,6 +122,23 @@ mod tests {
104122 use super :: * ;
105123 use std:: io:: Write ;
106124
125+ #[ test]
126+ fn the_preferred_port_needs_no_notice ( ) {
127+ assert_eq ! ( fallback_notice( PREFERRED_PORT ) , None ) ;
128+ }
129+
130+ #[ test]
131+ fn a_fallback_port_names_both_ports_and_the_symptom ( ) {
132+ let Some ( notice) = fallback_notice ( 54321 ) else {
133+ panic ! ( "a fallback port must produce a notice" ) ;
134+ } ;
135+ assert ! ( notice. contains( "1455" ) , "got: {notice}" ) ;
136+ assert ! ( notice. contains( "54321" ) , "got: {notice}" ) ;
137+ // Without this the operator sees an opaque browser error and has no
138+ // reason to suspect a port conflict.
139+ assert ! ( notice. contains( "invalid redirect URI" ) , "got: {notice}" ) ;
140+ }
141+
107142 #[ test]
108143 fn extracts_the_query_from_a_request_line ( ) {
109144 assert_eq ! (
0 commit comments