Skip to content

Commit 9308bc1

Browse files
authored
fix: make tcp closing error throwing consistent (#2729)
In the first close attempt we wrap the `closePromise` with a try/catch and call `.abort` if it throws, and we don't propagate the error. In the second invocation we return the `closePromise` which can reject making the code non-deterministic.
1 parent 82bd42b commit 9308bc1

File tree

2 files changed

+28
-21
lines changed

2 files changed

+28
-21
lines changed

packages/transport-tcp/src/listener.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ export class TCPListener extends TypedEventEmitter<ListenerEvents> implements Li
167167

168168
private onSocket (socket: net.Socket): void {
169169
if (this.status.code !== TCPListenerStatusCode.ACTIVE) {
170+
socket.destroy()
170171
throw new NotStartedError('Server is not listening yet')
171172
}
173+
172174
// Avoid uncaught errors caused by unstable connections
173175
socket.on('error', err => {
174176
this.log('socket error', err)

packages/transport-tcp/src/socket-to-conn.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -127,47 +127,48 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
127127
timeline: { open: Date.now() },
128128

129129
async close (options: AbortOptions = {}) {
130-
if (socket.destroyed) {
131-
log('The %s socket is destroyed', lOptsStr)
130+
if (socket.closed) {
131+
log('The %s socket is already closed', lOptsStr)
132132
return
133133
}
134134

135-
if (closePromise != null) {
136-
log('The %s socket is closed or closing', lOptsStr)
137-
return closePromise
138-
}
139-
140-
if (options.signal == null) {
141-
const signal = AbortSignal.timeout(closeTimeout)
142-
143-
options = {
144-
...options,
145-
signal
146-
}
135+
if (socket.destroyed) {
136+
log('The %s socket is already destroyed', lOptsStr)
137+
return
147138
}
148139

149140
const abortSignalListener = (): void => {
150141
socket.destroy(new AbortError('Destroying socket after timeout'))
151142
}
152143

153-
options.signal?.addEventListener('abort', abortSignalListener)
154-
155144
try {
145+
if (closePromise != null) {
146+
log('The %s socket is already closing', lOptsStr)
147+
await closePromise
148+
return
149+
}
150+
151+
if (options.signal == null) {
152+
const signal = AbortSignal.timeout(closeTimeout)
153+
154+
options = {
155+
...options,
156+
signal
157+
}
158+
}
159+
160+
options.signal?.addEventListener('abort', abortSignalListener)
161+
156162
log('%s closing socket', lOptsStr)
157163
closePromise = new Promise<void>((resolve, reject) => {
158164
socket.once('close', () => {
159165
// socket completely closed
160166
log('%s socket closed', lOptsStr)
161-
162167
resolve()
163168
})
164169
socket.once('error', (err: Error) => {
165170
log('%s socket error', lOptsStr, err)
166171

167-
// error closing socket
168-
if (maConn.timeline.close == null) {
169-
maConn.timeline.close = Date.now()
170-
}
171172
if (!socket.destroyed) {
172173
reject(err)
173174
}
@@ -210,6 +211,10 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
210211
socket.destroy(err)
211212
}
212213

214+
// closing a socket is always asynchronous (must wait for "close" event)
215+
// but the tests expect this to be a synchronous operation so we have to
216+
// set the close time here. the tests should be refactored to reflect
217+
// reality.
213218
if (maConn.timeline.close == null) {
214219
maConn.timeline.close = Date.now()
215220
}

0 commit comments

Comments
 (0)