fix: partial zip requests could be left hanging
Massimo Melina committed
Sep 13, 2025 at 00:23 UTC
83b890c6587f2c712c370f365f657202dc0a1c7c
3 files changed
+14
-10
src/QuickZipStream.ts
+8
-5
@@ -57,6 +57,10 @@ export class QuickZipStream extends Readable {
57
this.push(null) // EOF
58
}
59
60
+ continuePiping() {
61
+ setImmediate(() => this.push('')) // stimulate the pipe. In some situations this needs to be done at next tick; not sure when, but maybe it's when we are inside a _read call that writes nothing
62
+ }
63
+
64
applyRange(start: number, end: number) {
65
if (end < start)
66
return this.closeStream()
@@ -153,12 +157,11 @@ export class QuickZipStream extends Readable {
157
this.skip -= size
158
this.dataWritten += size
159
this.entries.push(entry)
156
- setTimeout(() => this.push('')) // this "signal" works only after _read() is done
157
- return
160
+ return this.continuePiping()
161
}
162
if (!getData) {
163
this.entries.push(entry)
161
- return
164
+ return this.continuePiping()
165
}
166
const data = getData()
167
data.on('error', (err) => {
@@ -166,7 +169,7 @@ export class QuickZipStream extends Readable {
169
console.error('zipping:', String(err))
170
data.destroy(err)
171
this.workingFile = undefined
169
- this.push('') // continue piping
172
+ this.continuePiping()
173
})
174
data.on('end', ()=>{
175
entry.crc = crc
@@ -174,7 +177,7 @@ export class QuickZipStream extends Readable {
177
crcCache[sourcePath] = { ts, crc }
178
this.entries.push(entry)
179
this.workingFile = undefined
177
- this.push('') // continue piping
180
+ this.continuePiping()
181
})
182
this.workingFile = data
183
data.on('data', chunk => {
src/zip.ts
+1
-1
@@ -53,7 +53,7 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
53
|| filterComment && !filterComment(await getCommentFor(source) || ''))
54
return
55
try {
56
- if (el.isFolder)
56
+ if (nodeIsFolder(el))
57
return { path: name + '/' }
58
if (!source) return
59
const st = el.stats || await fs.stat(source)
tests/test.ts
+5
-4
@@ -117,11 +117,12 @@ describe('basics', () => {
117
test('protectFromAbove.list', reqList('/protectFromAbove/child/', { inList:['alfa.txt'] }))
118
test('inheritNegativeMask', reqList('/tests/page', { outList: ['index.html'] }))
119
120
- const zipSize = 13010
121
- const zipOfs = 0x1359
120
+ const zipSize = 13178
121
+ const zipOfs = 0x32AF
122
+ const zipLength = 4
123
test('zip.head', req('/f1/?get=zip', { empty:true, length:zipSize }, { method:'HEAD' }) )
123
- test('zip.partial', req('/f1/?get=zip', { re:/^C3$/, length: 2 }, { headers: { Range: `bytes=${zipOfs}-${zipOfs+1}` } }) )
124
- test('zip.partial.resume', req('/f1/?get=zip', { re:/^C3/, length:zipSize-zipOfs }, { headers: { Range: `bytes=${zipOfs}-` } }) )
124
+ test('zip.partial', req('/f1/?get=zip', { re:/^page$/, length: zipLength }, { headers: { Range: `bytes=${zipOfs}-${zipOfs+zipLength-1}` } }) )
125
+ test('zip.partial.resume', req('/f1/?get=zip', { re:/^page/, length:zipSize-zipOfs }, { headers: { Range: `bytes=${zipOfs}-` } }) )
126
test('zip.partial.end', req('/f1/f2/?get=zip', { re:/^6/, length:10 }, { headers: { Range: 'bytes=-10' } }) )
127
test('zip.alfa is forbidden', req('/protectFromAbove/child/?get=zip&list=alfa.txt//renamed', { empty: true, length:118 }, { method:'HEAD' }))
128
test('zip.cantReadPage', req('/cantReadPage/?get=zip', { length: 4800 }, { method:'HEAD' }))