Location: src/news/news.controller.ts:129 (api)
What: The public media-serving endpoints do result.stream.pipe(res) on a raw MinIO getObject Readable (news.service.getImageStream, branding/avatars/trophies getStream/getFile/getPwaIcon all return client.getObject(...)). No .on('error') listener is attached to the source stream and no res.on('close') handler destroys it. main.ts (line 117) registers only a process.on('unhandledRejection') handler and NO process.on('uncaughtException') handler. When a Readable emits 'error' with zero 'error' listeners, Node re-throws it as an uncaughtException, which with no handler terminates the whole Node process. Separately, when the HTTP client disconnects mid-download there is nothing to destroy the source stream, so the underlying MinIO socket/fd is held until it times out.
Impact: A client requests GET /avatars/players/ (a very hot path, rendered for every player/team in the UI) or GET /news/image/. The MinIO read stream errors partway through the transfer (S3/MinIO restart, TCP reset, object deleted mid-read) -> the source Readable emits 'error' with no listener -> uncaughtException with no handler -> the api Node process exits, dropping every concurrent HTTP request and WebSocket connection on that pod. Independently, users navigating away mid-image-load repeatedly abort the response with no source-stream teardown, leaking MinIO sockets/file descriptors until the pod exhausts fds and crashes.
Suggested fix: Before piping, attach result.stream.on('error', (e) => { res.destroy(e); }) and res.on('close', () => result.stream.destroy()) (or use stream.pipeline / res.download) in each of news.controller.ts:129, branding.controller.ts:81 & :102, avatars.controller.ts:212, and trophies.controller.ts:78; also add a global process.on('uncaughtException') handler in main.ts.
Verifier evidence
news.controller.ts:129 result.stream.pipe(res); (source from getImageStream). news.service.ts:245-251 returns stream from this.s3.get(key). s3.service.ts:92-96 public async get(...): Promise<Readable> { return await this.client.getObject(bucket, filename); } (raw MinIO stream). Same pattern: branding.controller.ts:81 and :102, avatars.controller.ts:212 (getStream at avatars.service.ts:379-388), trophies.controller.ts:78. main.ts:117-119 only process.on("unhandledRejection", ...); grep of src/ for uncaughtException|useGlobalFilters|ExceptionFilter returns only that one unhandledRejection line. No .on('error') on any of these streams and no res.on('close')/res.on('aborted') handlers exist in the cited controllers.
Found by the 2026-07 multi-agent code audit; adversarially verified (CONFIRMED, P1). One of a batch of findings from that pass.
Location:
src/news/news.controller.ts:129(api)What: The public media-serving endpoints do
result.stream.pipe(res)on a raw MinIO getObject Readable (news.service.getImageStream, branding/avatars/trophies getStream/getFile/getPwaIcon all returnclient.getObject(...)). No.on('error')listener is attached to the source stream and nores.on('close')handler destroys it. main.ts (line 117) registers only aprocess.on('unhandledRejection')handler and NOprocess.on('uncaughtException')handler. When a Readable emits 'error' with zero 'error' listeners, Node re-throws it as an uncaughtException, which with no handler terminates the whole Node process. Separately, when the HTTP client disconnects mid-download there is nothing to destroy the source stream, so the underlying MinIO socket/fd is held until it times out.Impact: A client requests GET /avatars/players/ (a very hot path, rendered for every player/team in the UI) or GET /news/image/. The MinIO read stream errors partway through the transfer (S3/MinIO restart, TCP reset, object deleted mid-read) -> the source Readable emits 'error' with no listener -> uncaughtException with no handler -> the api Node process exits, dropping every concurrent HTTP request and WebSocket connection on that pod. Independently, users navigating away mid-image-load repeatedly abort the response with no source-stream teardown, leaking MinIO sockets/file descriptors until the pod exhausts fds and crashes.
Suggested fix: Before piping, attach
result.stream.on('error', (e) => { res.destroy(e); })andres.on('close', () => result.stream.destroy())(or use stream.pipeline / res.download) in each of news.controller.ts:129, branding.controller.ts:81 & :102, avatars.controller.ts:212, and trophies.controller.ts:78; also add a globalprocess.on('uncaughtException')handler in main.ts.Verifier evidence
news.controller.ts:129
result.stream.pipe(res);(source from getImageStream). news.service.ts:245-251 returnsstreamfromthis.s3.get(key). s3.service.ts:92-96public async get(...): Promise<Readable> { return await this.client.getObject(bucket, filename); }(raw MinIO stream). Same pattern: branding.controller.ts:81 and :102, avatars.controller.ts:212 (getStream at avatars.service.ts:379-388), trophies.controller.ts:78. main.ts:117-119 onlyprocess.on("unhandledRejection", ...); grep of src/ foruncaughtException|useGlobalFilters|ExceptionFilterreturns only that one unhandledRejection line. No.on('error')on any of these streams and nores.on('close')/res.on('aborted')handlers exist in the cited controllers.Found by the 2026-07 multi-agent code audit; adversarially verified (CONFIRMED, P1). One of a batch of findings from that pass.