fix(stack-files): record download metric when response closes after a full read (#1282)

The file-download handler recorded its in-process download metric off the
source stream's end/close events. When the response consumer closed right
after receiving the whole file, those source events could be dropped and the
metric was never recorded, so the per-node download counter undercounted
successful reads under load.

Record the success directly on response close once the file has been fully
read, instead of waiting on source events that may never arrive. The abort
path (partial read) is unchanged and the recorder stays idempotent, so a
later source end/close cannot double-count. Adds a deterministic regression
test for the response-close-after-full-read path.
This commit is contained in:
Anso
2026-06-02 09:49:08 -04:00
committed by GitHub
parent dd2c2b22ec
commit 5af0c043d5
2 changed files with 65 additions and 11 deletions
@@ -502,6 +502,35 @@ describe('GET /api/stacks/:stackName/files/download', () => {
}
}
class ResponseCloseAfterFullReadStream extends Readable {
public bytesRead = 0;
public destroyCalls = 0;
public emittedResponseClose = false;
constructor(private readonly payload: Buffer) {
super();
}
_read(): void {}
override destroy(error?: Error): this {
this.destroyCalls += 1;
return super.destroy(error);
}
override pipe<T extends NodeJS.WritableStream>(destination: T): T {
destination.write(this.payload);
this.bytesRead = this.payload.length;
// The response closes after the whole body was received, and the source
// stream never emits its own end/close because the consumer is already
// gone. Mirrors the real fs.ReadStream path under load that previously
// left the download metric unrecorded.
this.emittedResponseClose = destination.emit('close');
destination.end();
return destination;
}
}
class ErrorThenCloseStream extends Readable {
_read(): void {}
@@ -681,6 +710,24 @@ describe('GET /api/stacks/:stackName/files/download', () => {
await expectDownloadMetricCounts(1, 1, 0);
});
it('records a success when the response closes after a full read and the source emits no end/close', async () => {
await resetFileExplorerMetrics();
const payload = Buffer.from('services: {}\n');
const stream = new ResponseCloseAfterFullReadStream(payload);
await mockDownloadStream(stream, payload.length);
const res = await request(app)
.get(`/api/stacks/${STACK}/files/download`)
.query({ path: 'compose.yaml' })
.set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.text).toBe(payload.toString('utf-8'));
expect(stream.emittedResponseClose).toBe(true);
expect(stream.destroyCalls).toBe(0);
await expectDownloadMetricCounts(1, 1, 0);
});
it('records stream error followed by close exactly once as a failed download', async () => {
await resetFileExplorerMetrics();
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);