Skip to content

Commit 911fe8f

Browse files
fix: handle errors from file#createReadStream (#615)
* fix: handle errors from file#createReadStream * add tests * fix * once-ify complete handler * lint
1 parent a1a8137 commit 911fe8f

2 files changed

Lines changed: 193 additions & 3 deletions

File tree

handwritten/storage/src/file.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,8 @@ class File extends ServiceObject<File> {
12121212
return;
12131213
}
12141214

1215+
rawResponseStream.on('error', onComplete);
1216+
12151217
const headers = rawResponseStream.toJSON().headers;
12161218
const isCompressed = headers['content-encoding'] === 'gzip';
12171219
const shouldRunValidation = !rangeRequest && (crc32c || md5);
@@ -1235,21 +1237,24 @@ class File extends ServiceObject<File> {
12351237
rawResponseStream.pipe(pumpify.obj(throughStreams));
12361238
}
12371239

1238-
rawResponseStream.on('end', onComplete).pipe(throughStream, {
1239-
end: false
1240-
});
1240+
rawResponseStream.on('error', onComplete)
1241+
.on('end', onComplete)
1242+
.pipe(throughStream, {end: false});
12411243
};
12421244

12431245
// This is hooked to the `complete` event from the request stream. This is
12441246
// our chance to validate the data and let the user know if anything went
12451247
// wrong.
1248+
let onCompleteCalled = false;
12461249
const onComplete = (err: Error|null) => {
12471250
if (err) {
1251+
onCompleteCalled = true;
12481252
throughStream.destroy(err);
12491253
return;
12501254
}
12511255

12521256
if (rangeRequest) {
1257+
onCompleteCalled = true;
12531258
throughStream.end();
12541259
return;
12551260
}
@@ -1260,6 +1265,12 @@ class File extends ServiceObject<File> {
12601265
return;
12611266
}
12621267

1268+
if (onCompleteCalled) {
1269+
return;
1270+
}
1271+
1272+
onCompleteCalled = true;
1273+
12631274
const hashes = {
12641275
crc32c: this.metadata.crc32c,
12651276
md5: this.metadata.md5Hash,

handwritten/storage/test/file.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ const fakePromisify = {
6161
const fsCached = extend(true, {}, fs);
6262
const fakeFs = extend(true, {}, fsCached);
6363

64+
const zlibCached = extend(true, {}, zlib);
65+
let createGunzipOverride: Function|null;
66+
const fakeZlib = extend(true, {}, zlib, {
67+
createGunzip() {
68+
return (createGunzipOverride || zlibCached.createGunzip)
69+
.apply(null, arguments);
70+
},
71+
});
72+
6473
let hashStreamValidationOverride: Function|null;
6574
const hashStreamValidation = require('hash-stream-validation');
6675
function fakeHashStreamValidation() {
@@ -148,6 +157,7 @@ describe('File', () => {
148157
'hash-stream-validation': fakeHashStreamValidation,
149158
os: fakeOs,
150159
'xdg-basedir': fakeXdgBasedir,
160+
zlib: fakeZlib,
151161
}).File;
152162
});
153163

@@ -178,6 +188,7 @@ describe('File', () => {
178188
directoryFile = new File(BUCKET, 'directory/file.jpg');
179189
directoryFile.request = util.noop;
180190

191+
createGunzipOverride = null;
181192
handleRespOverride = null;
182193
hashStreamValidationOverride = null;
183194
makeWritableStreamOverride = null;
@@ -957,6 +968,83 @@ describe('File', () => {
957968
})
958969
.resume();
959970
});
971+
972+
it('should emit errors from the request stream', done => {
973+
const error = new Error('Error.');
974+
const rawResponseStream = through();
975+
// tslint:disable-next-line:no-any
976+
(rawResponseStream as any).toJSON = () => {
977+
return {headers: {}};
978+
};
979+
const requestStream = through();
980+
981+
handleRespOverride =
982+
(err: Error, res: {}, body: {}, callback: Function) => {
983+
callback(null, null, rawResponseStream);
984+
setImmediate(() => {
985+
rawResponseStream.emit('error', error);
986+
});
987+
};
988+
989+
file.requestStream = () => {
990+
setImmediate(() => {
991+
requestStream.emit('response', rawResponseStream);
992+
});
993+
return requestStream;
994+
};
995+
996+
file.createReadStream()
997+
.on('error',
998+
(err: Error) => {
999+
assert.strictEqual(err, error);
1000+
done();
1001+
})
1002+
.resume();
1003+
});
1004+
1005+
it('should not handle both error and end events', done => {
1006+
const error = new Error('Error.');
1007+
const rawResponseStream = through();
1008+
// tslint:disable-next-line:no-any
1009+
(rawResponseStream as any).toJSON = () => {
1010+
return {headers: {}};
1011+
};
1012+
const requestStream = through();
1013+
1014+
handleRespOverride =
1015+
(err: Error, res: {}, body: {}, callback: Function) => {
1016+
callback(null, null, rawResponseStream);
1017+
setImmediate(() => {
1018+
rawResponseStream.emit('error', error);
1019+
});
1020+
};
1021+
1022+
file.requestStream = () => {
1023+
setImmediate(() => {
1024+
requestStream.emit('response', rawResponseStream);
1025+
});
1026+
return requestStream;
1027+
};
1028+
1029+
file.getMetadata = (options: object, callback: Function) => {
1030+
callback();
1031+
};
1032+
1033+
let errorReceived = false;
1034+
file.createReadStream({validation: false})
1035+
.on('error',
1036+
(err: Error) => {
1037+
errorReceived = true;
1038+
assert.strictEqual(err, error);
1039+
rawResponseStream.emit('end');
1040+
setImmediate(done);
1041+
})
1042+
.on('end',
1043+
() => {
1044+
done(new Error('Should not have been called.'));
1045+
})
1046+
.resume();
1047+
});
9601048
});
9611049
});
9621050

@@ -995,6 +1083,50 @@ describe('File', () => {
9951083
})
9961084
.resume();
9971085
});
1086+
1087+
it('should emit errors from the gunzip stream', done => {
1088+
const error = new Error('Error.');
1089+
const createGunzipStream = through();
1090+
createGunzipOverride = () => {
1091+
setImmediate(() => {
1092+
createGunzipStream.emit('error', error);
1093+
});
1094+
return createGunzipStream;
1095+
};
1096+
file.createReadStream()
1097+
.on('error',
1098+
(err: Error) => {
1099+
assert.strictEqual(err, error);
1100+
done();
1101+
})
1102+
.resume();
1103+
});
1104+
1105+
it('should not handle both error and end events', done => {
1106+
const error = new Error('Error.');
1107+
const createGunzipStream = through();
1108+
createGunzipOverride = () => {
1109+
setImmediate(() => {
1110+
createGunzipStream.emit('error', error);
1111+
});
1112+
return createGunzipStream;
1113+
};
1114+
file.getMetadata = (options: object, callback: Function) => {
1115+
callback();
1116+
};
1117+
file.createReadStream({validation: false})
1118+
.on('error',
1119+
(err: Error) => {
1120+
assert.strictEqual(err, error);
1121+
createGunzipStream.emit('end');
1122+
setImmediate(done);
1123+
})
1124+
.on('end',
1125+
() => {
1126+
done(new Error('Should not have been called.'));
1127+
})
1128+
.resume();
1129+
});
9981130
});
9991131

10001132
describe('validation', () => {
@@ -1022,6 +1154,53 @@ describe('File', () => {
10221154
};
10231155
});
10241156

1157+
it('should emit errors from the validation stream', done => {
1158+
const error = new Error('Error.');
1159+
1160+
hashStreamValidationOverride = () => {
1161+
setImmediate(() => {
1162+
fakeValidationStream.emit('error', error);
1163+
});
1164+
return fakeValidationStream;
1165+
};
1166+
1167+
file.requestStream = getFakeSuccessfulRequest(data);
1168+
1169+
file.createReadStream()
1170+
.on('error',
1171+
(err: Error) => {
1172+
assert.strictEqual(err, error);
1173+
done();
1174+
})
1175+
.resume();
1176+
});
1177+
1178+
it('should not handle both error and end events', done => {
1179+
const error = new Error('Error.');
1180+
1181+
hashStreamValidationOverride = () => {
1182+
setImmediate(() => {
1183+
fakeValidationStream.emit('error', error);
1184+
});
1185+
return fakeValidationStream;
1186+
};
1187+
1188+
file.requestStream = getFakeSuccessfulRequest(data);
1189+
1190+
file.createReadStream()
1191+
.on('error',
1192+
(err: Error) => {
1193+
assert.strictEqual(err, error);
1194+
fakeValidationStream.emit('end');
1195+
setImmediate(done);
1196+
})
1197+
.on('end',
1198+
() => {
1199+
done(new Error('Should not have been called.'));
1200+
})
1201+
.resume();
1202+
});
1203+
10251204
it('should pass the userProject to getMetadata', done => {
10261205
const fakeOptions = {
10271206
userProject: 'grapce-spaceship-123',

0 commit comments

Comments
 (0)