Skip to content

Commit 72341c0

Browse files
yawkatclaude
andauthored
Do not release the in-progress HttpData twice on destroy() (#66)
destroy() released the HttpData that is still being decoded unconditionally, assuming the decoder is its only owner. It is not: it is handed out by currentPartialHttpData(), and a tracking factory releases it on cleanFiles() or when the user cleans the factory directly. Check refCnt() before releasing it, like for bodyListHttpData. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cae3740 commit 72341c0

3 files changed

Lines changed: 96 additions & 9 deletions

File tree

‎multipart-vintage/src/main/java/io/netty/contrib/multipart/vintage/HttpPostMultipartRequestDecoder.java‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -868,17 +868,22 @@ private FileUpload getFileUpload() {
868868
*/
869869
@Override
870870
public void destroy() {
871-
// The partially decoded item is owned by this decoder and never handed out. It is not in
872-
// bodyListHttpData, and a memory-only factory does not track it, so release it here. Remove it
873-
// from the factory first so that cleanFiles() does not release it a second time.
871+
// The HttpData that is still being decoded was never added to bodyListHttpData, and a memory based
872+
// one is not tracked by the factory either, so release it here. Remove it from the factory first so
873+
// that cleanFiles() below does not release it a second time. It might have been released already by
874+
// the user (through currentPartialHttpData(), cleanFiles() or by cleaning the factory directly).
874875
if (currentFileUpload != null) {
875876
factory.removeHttpDataFromClean(request, currentFileUpload);
876-
currentFileUpload.release();
877+
if (currentFileUpload.refCnt() > 0) {
878+
currentFileUpload.release();
879+
}
877880
currentFileUpload = null;
878881
}
879882
if (currentAttribute != null) {
880883
factory.removeHttpDataFromClean(request, currentAttribute);
881-
currentAttribute.release();
884+
if (currentAttribute.refCnt() > 0) {
885+
currentAttribute.release();
886+
}
882887
currentAttribute = null;
883888
}
884889
// Release all data items, including those not yet pulled, only file based items

‎multipart-vintage/src/main/java/io/netty/contrib/multipart/vintage/HttpPostStandardRequestDecoder.java‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,15 @@ private void parseBodyAttributes() {
471471
*/
472472
@Override
473473
public void destroy() {
474-
// The partially decoded item is owned by this decoder and never handed out. It is not in
475-
// bodyListHttpData, and a memory-only factory does not track it, so release it here. Remove it
476-
// from the factory first so that cleanFiles() does not release it a second time.
474+
// The HttpData that is still being decoded was never added to bodyListHttpData, and a memory based
475+
// one is not tracked by the factory either, so release it here. Remove it from the factory first so
476+
// that cleanFiles() below does not release it a second time. It might have been released already by
477+
// the user (through currentPartialHttpData(), cleanFiles() or by cleaning the factory directly).
477478
if (currentAttribute != null) {
478479
factory.removeHttpDataFromClean(request, currentAttribute);
479-
currentAttribute.release();
480+
if (currentAttribute.refCnt() > 0) {
481+
currentAttribute.release();
482+
}
480483
currentAttribute = null;
481484
}
482485
// Release all data items, including those not yet pulled, only file based items

‎multipart-vintage/src/test/java/io/netty/contrib/multipart/vintage/HttpPostRequestDecoderTest.java‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.netty.handler.codec.http.multipart.Attribute;
3939
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
4040
import io.netty.handler.codec.http.multipart.FileUpload;
41+
import io.netty.handler.codec.http.multipart.HttpDataFactory;
4142
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
4243
import io.netty.handler.codec.http.multipart.MemoryAttribute;
4344
import io.netty.handler.codec.http.multipart.MemoryFileUpload;
@@ -1290,4 +1291,82 @@ public void testBuilderReuseStandardMaxFields() {
12901291
decoder.destroy();
12911292
}
12921293
}
1294+
1295+
/**
1296+
* A decoder with a field that is still being decoded.
1297+
*/
1298+
private HttpPostRequestDecoder decoderWithPartialData(HttpDataFactory factory, boolean multipart) {
1299+
String boundary = "be38b42a9ad2713f";
1300+
HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
1301+
String body;
1302+
if (multipart) {
1303+
req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary);
1304+
body = "--" + boundary + "\r\n" +
1305+
"Content-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n" +
1306+
"Content-Type: text/plain\r\n" +
1307+
"\r\n" +
1308+
"some partial content";
1309+
} else {
1310+
req.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded");
1311+
body = "field=partialvalue";
1312+
}
1313+
HttpPostRequestDecoder decoder = this.builder().dataFactory(factory).build(req);
1314+
decoder.offer(new DefaultHttpContent(Unpooled.copiedBuffer(body, CharsetUtil.UTF_8)));
1315+
assertNotNull(decoder.currentPartialHttpData());
1316+
return decoder;
1317+
}
1318+
1319+
private void testDestroyAfterCleanFiles(boolean multipart) {
1320+
// the default factory tracks the data, so cleanFiles() releases the partial data
1321+
HttpPostRequestDecoder decoder = decoderWithPartialData(new DefaultHttpDataFactory(), multipart);
1322+
InterfaceHttpData partial = decoder.currentPartialHttpData();
1323+
decoder.cleanFiles();
1324+
assertEquals(0, partial.refCnt());
1325+
decoder.destroy();
1326+
}
1327+
1328+
@Test
1329+
public void testDestroyAfterCleanFilesStandardDecoder() {
1330+
testDestroyAfterCleanFiles(false);
1331+
}
1332+
1333+
@Test
1334+
public void testDestroyAfterCleanFilesMultipartDecoder() {
1335+
testDestroyAfterCleanFiles(true);
1336+
}
1337+
1338+
private void testDestroyAfterFactoryCleaned(boolean multipart) {
1339+
DefaultHttpDataFactory factory = new DefaultHttpDataFactory();
1340+
HttpPostRequestDecoder decoder = decoderWithPartialData(factory, multipart);
1341+
InterfaceHttpData partial = decoder.currentPartialHttpData();
1342+
factory.cleanAllHttpData();
1343+
assertEquals(0, partial.refCnt());
1344+
decoder.destroy();
1345+
}
1346+
1347+
@Test
1348+
public void testDestroyAfterFactoryCleanedStandardDecoder() {
1349+
testDestroyAfterFactoryCleaned(false);
1350+
}
1351+
1352+
@Test
1353+
public void testDestroyAfterFactoryCleanedMultipartDecoder() {
1354+
testDestroyAfterFactoryCleaned(true);
1355+
}
1356+
1357+
private void testDestroyAfterPartialReleased(boolean multipart) {
1358+
HttpPostRequestDecoder decoder = decoderWithPartialData(new DefaultHttpDataFactory(false), multipart);
1359+
assertTrue(decoder.currentPartialHttpData().release());
1360+
decoder.destroy();
1361+
}
1362+
1363+
@Test
1364+
public void testDestroyAfterPartialReleasedStandardDecoder() {
1365+
testDestroyAfterPartialReleased(false);
1366+
}
1367+
1368+
@Test
1369+
public void testDestroyAfterPartialReleasedMultipartDecoder() {
1370+
testDestroyAfterPartialReleased(true);
1371+
}
12931372
}

0 commit comments

Comments
 (0)