Skip to content

Commit 8dc41ab

Browse files
committed
fix: fixes mem leak; bumps test cov
1 parent ffd497a commit 8dc41ab

File tree

2 files changed

+430
-4
lines changed

2 files changed

+430
-4
lines changed

__tests__/application/respond.test.js

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const statuses = require('statuses')
66
const assert = require('node:assert/strict')
77
const Koa = require('../..')
88
const fs = require('fs')
9+
const http = require('http')
910

1011
describe('app.respond', () => {
1112
describe('when ctx.respond === false', () => {
@@ -916,4 +917,378 @@ describe('app.respond', () => {
916917
assert.equal(res.headers['content-length'], '0')
917918
})
918919
})
920+
921+
describe('when setupStreamAbortHandling is called', () => {
922+
it('should handle ReadableStream bodies', async () => {
923+
const app = new Koa()
924+
925+
app.use((ctx) => {
926+
const readable = new ReadableStream({
927+
start (controller) {
928+
controller.enqueue(new TextEncoder().encode('test data'))
929+
controller.close()
930+
}
931+
})
932+
ctx.body = readable
933+
})
934+
935+
await request(app.callback())
936+
.get('/')
937+
.expect(200)
938+
.expect(Buffer.from('test data'))
939+
})
940+
941+
it('should handle Response objects with streams', async () => {
942+
const app = new Koa()
943+
944+
app.use((ctx) => {
945+
const readable = new ReadableStream({
946+
start (controller) {
947+
controller.enqueue(new TextEncoder().encode('response data'))
948+
controller.close()
949+
}
950+
})
951+
ctx.body = new Response(readable)
952+
})
953+
954+
await request(app.callback())
955+
.get('/')
956+
.expect(200)
957+
.expect(Buffer.from('response data'))
958+
})
959+
960+
it('should handle Response objects without streams', async () => {
961+
const app = new Koa()
962+
963+
app.use((ctx) => {
964+
ctx.body = new Response(null)
965+
})
966+
967+
await request(app.callback())
968+
.get('/')
969+
.expect(200)
970+
.expect(Buffer.from([]))
971+
})
972+
973+
it('should handle Response objects with non-ReadableStream body', async () => {
974+
const app = new Koa()
975+
976+
app.use((ctx) => {
977+
const response = new Response('text body')
978+
Object.defineProperty(response, 'body', {
979+
value: 'not a stream',
980+
writable: false
981+
})
982+
ctx.body = response
983+
})
984+
985+
await request(app.callback())
986+
.get('/')
987+
.expect(200)
988+
})
989+
990+
it('should handle ReadableStream destruction', async () => {
991+
const app = new Koa()
992+
let destroyed = false
993+
let cancelPromiseResolve
994+
const cancelPromise = new Promise((resolve) => {
995+
cancelPromiseResolve = resolve
996+
})
997+
998+
app.use((ctx) => {
999+
const readable = new ReadableStream({
1000+
start (controller) {
1001+
controller.enqueue(new TextEncoder().encode('destroy test'))
1002+
setTimeout(() => {
1003+
if (!destroyed) {
1004+
controller.enqueue(new TextEncoder().encode(' more data'))
1005+
}
1006+
}, 10)
1007+
},
1008+
cancel () {
1009+
destroyed = true
1010+
cancelPromiseResolve()
1011+
}
1012+
})
1013+
ctx.body = readable
1014+
})
1015+
1016+
const server = app.listen()
1017+
return new Promise((resolve, reject) => {
1018+
const timeout = setTimeout(() => {
1019+
server.close()
1020+
reject(new Error('Test timed out - cancel was not called'))
1021+
}, 1000)
1022+
1023+
const req = http.request({
1024+
port: server.address().port,
1025+
path: '/'
1026+
})
1027+
1028+
req.on('response', (res) => {
1029+
res.on('data', (chunk) => {
1030+
setImmediate(() => req.destroy())
1031+
})
1032+
1033+
res.on('error', () => {
1034+
})
1035+
})
1036+
1037+
req.on('error', () => {
1038+
})
1039+
1040+
cancelPromise.then(() => {
1041+
clearTimeout(timeout)
1042+
server.close()
1043+
assert.strictEqual(destroyed, true, 'ReadableStream should be destroyed')
1044+
server.on('close', resolve)
1045+
})
1046+
1047+
req.end()
1048+
})
1049+
})
1050+
1051+
it('should handle locked ReadableStream', async () => {
1052+
const app = new Koa()
1053+
let cleanupCalled = false
1054+
1055+
app.use((ctx) => {
1056+
const readable = new ReadableStream({
1057+
start (controller) {
1058+
controller.enqueue(new TextEncoder().encode('locked stream'))
1059+
controller.close()
1060+
},
1061+
cancel () {
1062+
cleanupCalled = true
1063+
}
1064+
})
1065+
1066+
// Lock the stream by getting a reader
1067+
const reader = readable.getReader()
1068+
reader.releaseLock()
1069+
1070+
ctx.body = readable
1071+
})
1072+
1073+
const server = app.listen()
1074+
const req = http.request({
1075+
port: server.address().port,
1076+
path: '/'
1077+
})
1078+
1079+
req.on('response', (res) => {
1080+
req.destroy()
1081+
setTimeout(() => {
1082+
server.close()
1083+
}, 50)
1084+
})
1085+
1086+
req.end()
1087+
1088+
return new Promise((resolve) => {
1089+
server.on('close', () => {
1090+
assert.strictEqual(cleanupCalled, false, 'Cancel should not be called for locked stream')
1091+
resolve()
1092+
})
1093+
})
1094+
})
1095+
1096+
it('should handle ReadableStream without cancel method', async () => {
1097+
const app = new Koa()
1098+
1099+
app.use((ctx) => {
1100+
const readable = new ReadableStream({
1101+
start (controller) {
1102+
controller.enqueue(new TextEncoder().encode('no cancel'))
1103+
controller.close()
1104+
}
1105+
})
1106+
1107+
// Remove the cancel method
1108+
delete readable.cancel
1109+
1110+
ctx.body = readable
1111+
})
1112+
1113+
const server = app.listen()
1114+
const req = http.request({
1115+
port: server.address().port,
1116+
path: '/'
1117+
})
1118+
1119+
req.on('response', (res) => {
1120+
req.destroy()
1121+
setTimeout(() => {
1122+
server.close()
1123+
}, 50)
1124+
})
1125+
1126+
req.end()
1127+
1128+
return new Promise((resolve) => {
1129+
server.on('close', resolve)
1130+
})
1131+
})
1132+
1133+
it('should handle Response with locked ReadableStream body', async () => {
1134+
const app = new Koa()
1135+
let cleanupCalled = false
1136+
1137+
app.use((ctx) => {
1138+
const readable = new ReadableStream({
1139+
start (controller) {
1140+
controller.enqueue(new TextEncoder().encode('response locked'))
1141+
controller.close()
1142+
},
1143+
cancel () {
1144+
cleanupCalled = true
1145+
}
1146+
})
1147+
1148+
// Lock the stream
1149+
const reader = readable.getReader()
1150+
reader.releaseLock()
1151+
1152+
ctx.body = new Response(readable)
1153+
})
1154+
1155+
const server = app.listen()
1156+
const req = http.request({
1157+
port: server.address().port,
1158+
path: '/'
1159+
})
1160+
1161+
req.on('response', (res) => {
1162+
req.destroy()
1163+
setTimeout(() => {
1164+
server.close()
1165+
}, 50)
1166+
})
1167+
1168+
req.end()
1169+
1170+
return new Promise((resolve) => {
1171+
server.on('close', () => {
1172+
assert.strictEqual(cleanupCalled, false, 'Cancel should not be called for locked Response body')
1173+
resolve()
1174+
})
1175+
})
1176+
})
1177+
1178+
it('should handle Response with body without cancel method', async () => {
1179+
const app = new Koa()
1180+
1181+
app.use((ctx) => {
1182+
const readable = new ReadableStream({
1183+
start (controller) {
1184+
controller.enqueue(new TextEncoder().encode('no cancel method'))
1185+
controller.close()
1186+
}
1187+
})
1188+
1189+
// Remove the cancel method
1190+
delete readable.cancel
1191+
1192+
ctx.body = new Response(readable)
1193+
})
1194+
1195+
const server = app.listen()
1196+
const req = http.request({
1197+
port: server.address().port,
1198+
path: '/'
1199+
})
1200+
1201+
req.on('response', (res) => {
1202+
req.destroy()
1203+
setTimeout(() => {
1204+
server.close()
1205+
}, 50)
1206+
})
1207+
1208+
req.end()
1209+
1210+
return new Promise((resolve) => {
1211+
server.on('close', resolve)
1212+
})
1213+
})
1214+
1215+
it('should handle non-stream original parameter', async () => {
1216+
const app = new Koa()
1217+
1218+
app.use((ctx) => {
1219+
const readable = new ReadableStream({
1220+
start (controller) {
1221+
controller.enqueue(new TextEncoder().encode('non-stream original'))
1222+
controller.close()
1223+
}
1224+
})
1225+
1226+
// We'll test this indirectly by ensuring the stream works normally
1227+
ctx.body = readable
1228+
})
1229+
1230+
const server = app.listen()
1231+
const req = http.request({
1232+
port: server.address().port,
1233+
path: '/'
1234+
})
1235+
1236+
req.on('response', (res) => {
1237+
req.destroy()
1238+
setTimeout(() => {
1239+
server.close()
1240+
}, 50)
1241+
})
1242+
1243+
req.end()
1244+
1245+
return new Promise((resolve) => {
1246+
server.on('close', resolve)
1247+
})
1248+
})
1249+
1250+
it('should exercise setupStreamAbortHandling code paths', () => {
1251+
const app = new Koa()
1252+
let setupCalled = false
1253+
1254+
app.use((ctx) => {
1255+
const readable = new ReadableStream({
1256+
start (controller) {
1257+
setupCalled = true
1258+
controller.enqueue(new TextEncoder().encode('data'))
1259+
controller.close()
1260+
}
1261+
})
1262+
ctx.body = readable
1263+
})
1264+
1265+
return request(app.callback())
1266+
.get('/')
1267+
.expect(200)
1268+
.then(() => {
1269+
assert.strictEqual(setupCalled, true, 'Stream setup should be called')
1270+
})
1271+
})
1272+
})
1273+
1274+
describe('when accessing static default property', () => {
1275+
it('should return Application constructor', () => {
1276+
assert.strictEqual(Koa.default, Koa)
1277+
})
1278+
})
1279+
1280+
describe('when using test helpers', () => {
1281+
it('should exercise test helper stream functions', () => {
1282+
const { Readable } = require('../../test-helpers/stream')
1283+
const stream = new Readable()
1284+
1285+
stream.pipe()
1286+
stream.read()
1287+
stream.destroy()
1288+
1289+
assert.strictEqual(stream.readable, true)
1290+
assert.strictEqual(stream.readableObjectMode, false)
1291+
assert.strictEqual(stream.destroyed, false)
1292+
})
1293+
})
9191294
})

0 commit comments

Comments
 (0)