@@ -5,7 +5,8 @@ import { PassThrough } from 'node:stream';
5
5
import { KubeConfig } from './config.js' ;
6
6
import { Cluster , Context , User } from './config_types.js' ;
7
7
import { Watch } from './watch.js' ;
8
- import { IncomingMessage } from 'node:http' ;
8
+ import { IncomingMessage , createServer } from 'node:http' ;
9
+ import { AddressInfo } from 'node:net' ;
9
10
10
11
const server = 'https://foo.company.com' ;
11
12
@@ -167,6 +168,69 @@ describe('Watch', () => {
167
168
stream . destroy ( ) ;
168
169
} ) ;
169
170
171
+ it ( 'should not call the done callback more than once on unexpected connection loss' , async ( ) => {
172
+ // Create a server that accepts the connection and flushes headers, then
173
+ // immediately destroys the connection (causing a "Premature close"
174
+ // error).
175
+ //
176
+ // This reproduces a bug where AbortController.abort() inside
177
+ // doneCallOnce could cause done() to be invoked twice.
178
+
179
+ const mockServer = createServer ( ( req , res ) => {
180
+ res . writeHead ( 200 , {
181
+ 'Content-Type' : 'application/json' ,
182
+ 'Transfer-Encoding' : 'chunked' ,
183
+ } ) ;
184
+
185
+ res . flushHeaders ( ) ;
186
+ res . destroy ( ) ; // Prematurely close the connection
187
+ } ) ;
188
+
189
+ const mockServerPort = await new Promise < number > ( ( resolve ) => {
190
+ mockServer . listen ( 0 , ( ) => {
191
+ resolve ( ( mockServer . address ( ) as AddressInfo ) . port ) ;
192
+ } ) ;
193
+ } ) ;
194
+
195
+ const kc = new KubeConfig ( ) ;
196
+
197
+ kc . loadFromClusterAndUser (
198
+ {
199
+ name : 'cluster' ,
200
+ server : `http://localhost:${ mockServerPort } ` ,
201
+ skipTLSVerify : true ,
202
+ } ,
203
+ {
204
+ name : 'user' ,
205
+ } ,
206
+ ) ;
207
+
208
+ const watch = new Watch ( kc ) ;
209
+
210
+ let doneCalled = 0 ;
211
+ let doneResolve : ( ) => void ;
212
+
213
+ const donePromise = new Promise < void > ( ( resolve ) => {
214
+ doneResolve = resolve ;
215
+ } ) ;
216
+
217
+ await watch . watch (
218
+ '/some/path/to/object' ,
219
+ { } ,
220
+ ( ) => { } ,
221
+ ( ) => {
222
+ doneCalled += 1 ;
223
+ doneResolve ( ) ;
224
+ } ,
225
+ ) ;
226
+
227
+ await donePromise ;
228
+
229
+ mockServer . close ( ) ;
230
+
231
+ strictEqual ( doneCalled , 1 ) ;
232
+ } ) ;
233
+
170
234
it ( 'should call setKeepAlive on the socket to extend the default of 5 mins' , async ( ) => {
171
235
const kc = new KubeConfig ( ) ;
172
236
0 commit comments