Skip to content

Commit 90ee39a

Browse files
committed
Fix multipart infinite loop when uploading empty file with zero copy disable, close AsyncHttpClient#1485
Motivation: We always add transferred into position, even though transferred can be -1 because file is empty or FileChannel was closed. We then get stuck into an infinite loop. Modifications: * Only move position when transferred is > 0. * Finish part when we hit transferred < 0 Result: Fixed infinite loop
1 parent 6633bdd commit 90ee39a

File tree

4 files changed

+66
-78
lines changed

4 files changed

+66
-78
lines changed

client/src/main/java/org/asynchttpclient/request/body/Body.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ enum BodyState {
5252
* Reads the next chunk of bytes from the body.
5353
*
5454
* @param target The buffer to store the chunk in, must not be {@code null}.
55-
* @return The non-negative number of bytes actually read or {@code -1} if the body has been read completely.
55+
* @return The state.
5656
* @throws IOException If the chunk could not be read.
5757
*/
5858
BodyState transferTo(ByteBuf target) throws IOException;

client/src/main/java/org/asynchttpclient/request/body/multipart/part/FileMultipartPart.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,16 @@ protected long getContentLength() {
4949

5050
@Override
5151
protected long transferContentTo(ByteBuf target) throws IOException {
52+
// can return -1 if file is empty or FileChannel was closed
5253
int transferred = target.writeBytes(channel, target.writableBytes());
53-
position += transferred;
54-
if (position == length) {
54+
if (transferred > 0) {
55+
position += transferred;
56+
}
57+
if (position == length || transferred < 0) {
5558
state = MultipartState.POST_CONTENT;
56-
channel.close();
59+
if (channel.isOpen()) {
60+
channel.close();
61+
}
5762
}
5863
return transferred;
5964
}
@@ -63,10 +68,14 @@ protected long transferContentTo(WritableByteChannel target) throws IOException
6368
// WARN: don't use channel.position(), it's always 0 here
6469
// from FileChannel javadoc: "This method does not modify this channel's position."
6570
long transferred = channel.transferTo(position, BodyChunkedInput.DEFAULT_CHUNK_SIZE, target);
66-
position += transferred;
67-
if (position == length) {
71+
if (transferred > 0) {
72+
position += transferred;
73+
}
74+
if (position == length || transferred < 0) {
6875
state = MultipartState.POST_CONTENT;
69-
channel.close();
76+
if (channel.isOpen()) {
77+
channel.close();
78+
}
7079
} else {
7180
slowTarget = true;
7281
}

client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java

Lines changed: 50 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,7 @@
1717
import static org.asynchttpclient.test.TestUtils.*;
1818
import static org.testng.Assert.*;
1919

20-
import java.io.ByteArrayOutputStream;
21-
import java.io.File;
22-
import java.io.FileInputStream;
23-
import java.io.FileNotFoundException;
24-
import java.io.FileOutputStream;
25-
import java.io.IOException;
26-
import java.io.InputStream;
27-
import java.io.OutputStream;
28-
import java.io.Writer;
20+
import java.io.*;
2921
import java.util.ArrayList;
3022
import java.util.Arrays;
3123
import java.util.List;
@@ -47,7 +39,6 @@
4739
import org.asynchttpclient.AbstractBasicTest;
4840
import org.asynchttpclient.AsyncHttpClient;
4941
import org.asynchttpclient.Request;
50-
import org.asynchttpclient.RequestBuilder;
5142
import org.asynchttpclient.Response;
5243
import org.eclipse.jetty.server.Server;
5344
import org.eclipse.jetty.server.ServerConnector;
@@ -62,56 +53,37 @@
6253
* @author dominict
6354
*/
6455
public class MultipartUploadTest extends AbstractBasicTest {
65-
public static byte GZIPTEXT[] = new byte[] { 31, -117, 8, 8, 11, 43, 79, 75, 0, 3, 104, 101, 108, 108, 111, 46, 116, 120, 116, 0, -53, 72, -51, -55, -55, -25, 2, 0, 32, 48,
66-
58, 54, 6, 0, 0, 0 };
56+
public static byte GZIPTEXT[] = new byte[] { 31, -117, 8, 8, 11, 43, 79, 75, 0, 3, 104, 101, 108, 108, 111, 46, 116,
57+
120, 116, 0, -53, 72, -51, -55, -55, -25, 2, 0, 32, 48, 58, 54, 6, 0, 0, 0 };
6758

6859
@BeforeClass
6960
public void setUp() throws Exception {
7061
server = new Server();
7162
ServerConnector connector = addHttpConnector(server);
7263
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
73-
context.addServlet(new ServletHolder(new MockMultipartUploadServlet()), "/upload/*");
64+
context.addServlet(new ServletHolder(new MockMultipartUploadServlet()), "/upload");
7465
server.setHandler(context);
7566
server.start();
7667
port1 = connector.getLocalPort();
7768
}
7869

7970
/**
8071
* Tests that the streaming of a file works.
81-
* @throws IOException
72+
*
73+
* @throws IOException
8274
*/
8375
@Test(groups = "standalone")
84-
public void testSendingSmallFilesAndByteArray() throws IOException {
76+
public void testSendingSmallFilesAndByteArray() throws Exception {
8577
String expectedContents = "filecontent: hello";
8678
String expectedContents2 = "gzipcontent: hello";
8779
String expectedContents3 = "filecontent: hello2";
8880
String testResource1 = "textfile.txt";
8981
String testResource2 = "gzip.txt.gz";
9082
String testResource3 = "textfile2.txt";
9183

92-
File testResource1File = null;
93-
try {
94-
testResource1File = getClasspathFile(testResource1);
95-
} catch (FileNotFoundException e) {
96-
// TODO Auto-generated catch block
97-
fail("unable to find " + testResource1);
98-
}
99-
100-
File testResource2File = null;
101-
try {
102-
testResource2File = getClasspathFile(testResource2);
103-
} catch (FileNotFoundException e) {
104-
// TODO Auto-generated catch block
105-
fail("unable to find " + testResource2);
106-
}
107-
108-
File testResource3File = null;
109-
try {
110-
testResource3File = getClasspathFile(testResource3);
111-
} catch (FileNotFoundException e) {
112-
// TODO Auto-generated catch block
113-
fail("unable to find " + testResource3);
114-
}
84+
File testResource1File = getClasspathFile(testResource1);
85+
File testResource2File = getClasspathFile(testResource2);
86+
File testResource3File = getClasspathFile(testResource3);
11587

11688
List<File> testFiles = new ArrayList<>();
11789
testFiles.add(testResource1File);
@@ -137,48 +109,52 @@ public void testSendingSmallFilesAndByteArray() throws IOException {
137109
testFiles.add(tmpFile);
138110
expected.add(expectedContents);
139111
gzipped.add(false);
140-
141-
} catch (FileNotFoundException e1) {
142-
// TODO Auto-generated catch block
143-
e1.printStackTrace();
144-
} catch (IOException e1) {
145-
// TODO Auto-generated catch block
146-
e1.printStackTrace();
147112
}
148113

149114
if (!tmpFileCreated) {
150115
fail("Unable to test ByteArrayMultiPart, as unable to write to filesystem the tmp test content");
151116
}
152117

153-
try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) {
154-
155-
RequestBuilder builder = post("http://localhost" + ":" + port1 + "/upload/bob");
156-
builder.addBodyPart(new FilePart("file1", testResource1File, "text/plain", UTF_8));
157-
builder.addBodyPart(new FilePart("file2", testResource2File, "application/x-gzip", null));
158-
builder.addBodyPart(new StringPart("Name", "Dominic"));
159-
builder.addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8));
160-
builder.addBodyPart(new StringPart("Age", "3"));
161-
builder.addBodyPart(new StringPart("Height", "shrimplike"));
162-
builder.addBodyPart(new StringPart("Hair", "ridiculous"));
163-
164-
builder.addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt"));
165-
166-
Request r = builder.build();
118+
try (AsyncHttpClient c = asyncHttpClient(config())) {
119+
Request r = post("http://localhost" + ":" + port1 + "/upload")
120+
.addBodyPart(new FilePart("file1", testResource1File, "text/plain", UTF_8))
121+
.addBodyPart(new FilePart("file2", testResource2File, "application/x-gzip", null))
122+
.addBodyPart(new StringPart("Name", "Dominic"))
123+
.addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8))
124+
.addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike"))
125+
.addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4",
126+
expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt"))
127+
.build();
167128

168129
Response res = c.executeRequest(r).get();
169130

170131
assertEquals(res.getStatusCode(), 200);
171132

172133
testSentFile(expected, testFiles, res, gzipped);
134+
}
135+
}
173136

174-
} catch (Exception e) {
175-
e.printStackTrace();
176-
fail("Download Exception");
177-
} finally {
178-
FileUtils.deleteQuietly(tmpFile);
137+
private void sendEmptyFile0(boolean disableZeroCopy) throws Exception {
138+
File file = getClasspathFile("empty.txt");
139+
try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) {
140+
Request r = post("http://localhost" + ":" + port1 + "/upload/bob")
141+
.addBodyPart(new FilePart("file", file, "text/plain", UTF_8)).build();
142+
143+
Response res = c.executeRequest(r).get();
144+
assertEquals(res.getStatusCode(), 200);
179145
}
180146
}
181147

148+
@Test(groups = "standalone")
149+
public void sendEmptyFile() throws Exception {
150+
sendEmptyFile0(true);
151+
}
152+
153+
@Test(groups = "standalone")
154+
public void sendEmptyFileZeroCopy() throws Exception {
155+
sendEmptyFile0(false);
156+
}
157+
182158
/**
183159
* Test that the files were sent, based on the response from the servlet
184160
*
@@ -187,7 +163,8 @@ public void testSendingSmallFilesAndByteArray() throws IOException {
187163
* @param r
188164
* @param deflate
189165
*/
190-
private void testSentFile(List<String> expectedContents, List<File> sourceFiles, Response r, List<Boolean> deflate) {
166+
private void testSentFile(List<String> expectedContents, List<File> sourceFiles, Response r,
167+
List<Boolean> deflate) {
191168
String content = r.getResponseBody();
192169
assertNotNull("===>" + content);
193170
logger.debug(content);
@@ -248,7 +225,6 @@ private void testSentFile(List<String> expectedContents, List<File> sourceFiles,
248225
assertEquals(bytes, sourceBytes);
249226
}
250227

251-
252228
if (!deflate.get(i)) {
253229
String helloString = new String(bytes);
254230
assertEquals(helloString, expectedContents.get(i));
@@ -265,9 +241,9 @@ private void testSentFile(List<String> expectedContents, List<File> sourceFiles,
265241
} finally {
266242
deflater.close();
267243
}
268-
244+
269245
String helloString = new String(baos3.toByteArray());
270-
246+
271247
assertEquals(expectedContents.get(i), helloString);
272248
}
273249
}
@@ -326,7 +302,8 @@ public int getStringsProcessed() {
326302
}
327303

328304
@Override
329-
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
305+
public void service(HttpServletRequest request, HttpServletResponse response)
306+
throws ServletException, IOException {
330307
// Check that we have a file upload request
331308
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
332309
if (isMultipart) {
@@ -342,12 +319,14 @@ public void service(HttpServletRequest request, HttpServletResponse response) th
342319
try (InputStream stream = item.openStream()) {
343320

344321
if (item.isFormField()) {
345-
LOGGER.debug("Form field " + name + " with value " + Streams.asString(stream) + " detected.");
322+
LOGGER.debug("Form field " + name + " with value " + Streams.asString(stream)
323+
+ " detected.");
346324
incrementStringsProcessed();
347325
} else {
348326
LOGGER.debug("File field " + name + " with file name " + item.getName() + " detected.");
349327
// Process the input stream
350-
File tmpFile = File.createTempFile(UUID.randomUUID().toString() + "_MockUploadServlet", ".tmp");
328+
File tmpFile = File.createTempFile(UUID.randomUUID().toString() + "_MockUploadServlet",
329+
".tmp");
351330
tmpFile.deleteOnExit();
352331
try (OutputStream os = new FileOutputStream(tmpFile)) {
353332
byte[] buffer = new byte[4096];

client/src/test/resources/empty.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)