@@ -7,7 +7,7 @@ export interface CallbackServer {
77 redirectUrl : string ;
88 expectState : ( state : string ) => void ;
99 // Resolves once the authorization server redirects back with a code, or rejects
10- // if the signal aborts or the server reports an error.
10+ // if the signal aborts, the server reports an error, or close() runs .
1111 waitForCode : ( signal : AbortSignal ) => Promise < string > ;
1212 close : ( ) => void ;
1313}
@@ -19,13 +19,13 @@ interface CallbackWaiter {
1919}
2020
2121const CALLBACK_PATH = "/callback" ;
22+ const CLOSED_ERROR = "OAuth callback server closed before authorization completed." ;
2223
23- // Start an ephemeral loopback server to receive the OAuth redirect. `serverName`
24- // only names the authorization on the page the browser lands on.
25- // Binds to a
26- // random port on 127.0.0.1 so it never collides with anything and is only
27- // reachable locally.
24+ // Start a loopback server to receive the OAuth redirect. close() fail-closes
25+ // waitForCode so disposing the toolset cannot leave authorization hung.
26+ // `serverName` only names the authorization on the page the browser lands on.
2827export async function startCallbackServer ( serverName ?: string ) : Promise < CallbackServer > {
28+ let closed = false ;
2929 let expectedState : string | undefined ;
3030 let pendingResult : CallbackResult | undefined ;
3131 let waiter : CallbackWaiter | undefined ;
@@ -36,6 +36,7 @@ export async function startCallbackServer(serverName?: string): Promise<Callback
3636 } ;
3737
3838 const deliver = ( result : CallbackResult ) : void => {
39+ if ( closed ) return ;
3940 if ( waiter === undefined ) {
4041 pendingResult = result ;
4142 return ;
@@ -77,7 +78,9 @@ export async function startCallbackServer(serverName?: string): Promise<Callback
7778 } ) ;
7879
7980 await new Promise < void > ( ( resolve , reject ) => {
80- server . once ( "error" , reject ) ;
81+ server . once ( "error" , ( err ) => {
82+ reject ( new Error ( `Could not start the OAuth callback server: ${ err . message } ` ) ) ;
83+ } ) ;
8184 server . listen ( 0 , "127.0.0.1" , resolve ) ;
8285 } ) ;
8386
@@ -92,6 +95,10 @@ export async function startCallbackServer(serverName?: string): Promise<Callback
9295 } ,
9396 waitForCode : ( signal : AbortSignal ) =>
9497 new Promise < string > ( ( resolve , reject ) => {
98+ if ( closed ) {
99+ reject ( new Error ( CLOSED_ERROR ) ) ;
100+ return ;
101+ }
95102 if ( signal . aborted ) {
96103 reject ( new Error ( "aborted" ) ) ;
97104 return ;
@@ -115,6 +122,18 @@ export async function startCallbackServer(serverName?: string): Promise<Callback
115122 { once : true } ,
116123 ) ;
117124 } ) ,
118- close : ( ) => server . close ( ) ,
125+ close : ( ) => {
126+ // Rejecting the waiter here is what unblocks toolset disposal: a server
127+ // that merely stops listening would leave a pending waitForCode hung.
128+ if ( closed ) return ;
129+ closed = true ;
130+ pendingResult = undefined ;
131+ if ( waiter !== undefined ) {
132+ const activeWaiter = waiter ;
133+ clearAuthorization ( ) ;
134+ activeWaiter . reject ( new Error ( CLOSED_ERROR ) ) ;
135+ }
136+ server . close ( ) ;
137+ } ,
119138 } ;
120139}
0 commit comments