Skip to content

Commit 3d281cb

Browse files
authored
Merge pull request #5459 from eclipse-vertx/client-form-upload-attunements
Client form upload attunements
2 parents 6b58c32 + d28e8b1 commit 3d281cb

File tree

3 files changed

+202
-26
lines changed

3 files changed

+202
-26
lines changed

vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -272,30 +272,33 @@ public synchronized HttpClientRequest redirectHandler(@Nullable Function<HttpCli
272272
public Future<HttpClientResponse> send(ClientForm body) {
273273
ClientMultipartFormImpl impl = (ClientMultipartFormImpl) body;
274274
String contentType = headers != null ? headers.get(HttpHeaders.CONTENT_TYPE) : null;
275-
boolean multipart;
276-
if (contentType == null) {
277-
multipart = impl.isMultipart();
278-
contentType = multipart ? HttpHeaders.MULTIPART_FORM_DATA.toString() : HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString();
279-
putHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
280-
} else {
281-
if (contentType.equalsIgnoreCase(HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
282-
if (impl.isMultipart()) {
283-
throw new UnsupportedOperationException("handle me");
284-
}
285-
multipart = false;
286-
} else if (contentType.equalsIgnoreCase(HttpHeaders.MULTIPART_FORM_DATA.toString())) {
287-
multipart = true;
288-
} else {
289-
throw new UnsupportedOperationException("handle me");
290-
}
291-
}
292275
boolean multipartMixed = impl.mixed();
293276
HttpPostRequestEncoder.EncoderMode encoderMode = multipartMixed ? HttpPostRequestEncoder.EncoderMode.RFC1738 : HttpPostRequestEncoder.EncoderMode.HTML5;
294277
ClientMultipartFormUpload form;
295278
try {
279+
boolean multipart;
280+
if (contentType == null) {
281+
multipart = impl.isMultipart();
282+
contentType = multipart ? HttpHeaders.MULTIPART_FORM_DATA.toString() : HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString();
283+
putHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
284+
} else {
285+
if (contentType.equalsIgnoreCase(HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
286+
if (impl.isMultipart()) {
287+
throw new IllegalStateException("Multipart form requires multipart/form-data content type instead of "
288+
+ HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED);
289+
}
290+
multipart = false;
291+
} else if (contentType.equalsIgnoreCase(HttpHeaders.MULTIPART_FORM_DATA.toString())) {
292+
multipart = true;
293+
} else {
294+
throw new IllegalStateException("Sending form requires multipart/form-data or "
295+
+ HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED + " content type instead of " + contentType);
296+
}
297+
}
296298
form = new ClientMultipartFormUpload(context, impl, multipart, encoderMode);
297299
} catch (Exception e) {
298-
return context.failedFuture(e);
300+
reset(0, e);
301+
return response();
299302
}
300303
for (Map.Entry<String, String> header : form.headers()) {
301304
if (header.getKey().equalsIgnoreCase(CONTENT_LENGTH.toString())) {

vertx-core/src/test/java/io/vertx/tests/http/fileupload/HttpClientFileUploadTest.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.vertx.tests.http.fileupload;
22

33
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
4+
import io.vertx.core.Future;
45
import io.vertx.core.MultiMap;
56
import io.vertx.core.buffer.Buffer;
67
import io.vertx.core.http.*;
@@ -355,18 +356,50 @@ public void testFileUploadWhenFileDoesNotExist() throws Exception {
355356
fail();
356357
});
357358
startServer();
359+
HttpClientRequest request = client
360+
.request(new RequestOptions(requestOptions).putHeader("bla", Arrays.asList("1", "2")).setMethod(HttpMethod.POST))
361+
.await();
362+
Future<HttpClientResponse> response = request.send(ClientMultipartForm
363+
.multipartForm()
364+
.textFileUpload("file", "nonexistentFilename", "nonexistentPathname", "text/plain"));
358365
try {
359-
client.request(new RequestOptions(requestOptions).putHeader("bla", Arrays.asList("1", "2")).setMethod(HttpMethod.POST))
360-
.compose(req -> req
361-
.send(ClientMultipartForm
362-
.multipartForm()
363-
.textFileUpload("file", "nonexistentFilename", "nonexistentPathname", "text/plain"))
366+
response
364367
.expecting(HttpResponseExpectation.SC_OK)
365-
.compose(HttpClientResponse::body))
368+
.compose(HttpClientResponse::body)
366369
.await();
367370
} catch (Exception err) {
368-
assertEquals(err.getClass(), HttpPostRequestEncoder.ErrorDataEncoderException.class);
369-
assertEquals(err.getCause().getClass(), FileNotFoundException.class);
371+
assertEquals(err.getClass(), StreamResetException.class);
372+
assertEquals(err.getCause().getClass(), HttpPostRequestEncoder.ErrorDataEncoderException.class);
373+
assertEquals(err.getCause().getCause().getClass(), FileNotFoundException.class);
374+
}
375+
assertTrue(request.response().failed());
376+
}
377+
378+
@Test
379+
public void testInvalidMultipartContentType() throws Exception {
380+
testInvalidContentType(ClientMultipartForm.multipartForm(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString());
381+
}
382+
383+
@Test
384+
public void testInvalidContentType() throws Exception {
385+
testInvalidContentType(ClientMultipartForm.multipartForm(), HttpHeaders.TEXT_HTML.toString());
386+
}
387+
388+
private void testInvalidContentType(ClientForm form, String contentType) throws Exception {
389+
server.requestHandler(req -> {
390+
fail();
391+
});
392+
startServer();
393+
try {
394+
client
395+
.request(new RequestOptions(requestOptions)
396+
.putHeader(HttpHeaders.CONTENT_TYPE, contentType)
397+
.setMethod(HttpMethod.POST))
398+
.compose(request -> request
399+
.send(form))
400+
.await();
401+
fail();
402+
} catch (Exception expected) {
370403
}
371404
}
372405
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2014 Red Hat, Inc.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v1.0
6+
* and Apache License v2.0 which accompanies this distribution.
7+
*
8+
* The Eclipse Public License is available at
9+
* http://www.eclipse.org/legal/epl-v10.html
10+
*
11+
* The Apache License v2.0 is available at
12+
* http://www.opensource.org/licenses/apache2.0.php
13+
*
14+
* You may elect to redistribute this code under either of these licenses.
15+
*/
16+
package io.vertx.tests.http.fileupload;
17+
18+
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
19+
import io.vertx.core.Vertx;
20+
import io.vertx.core.buffer.Buffer;
21+
import io.vertx.core.http.ClientForm;
22+
import io.vertx.core.http.ClientMultipartForm;
23+
import io.vertx.core.http.impl.ClientMultipartFormImpl;
24+
import io.vertx.core.http.impl.ClientMultipartFormUpload;
25+
import io.vertx.core.internal.ContextInternal;
26+
import io.vertx.core.internal.VertxInternal;
27+
import io.vertx.test.core.TestUtils;
28+
import io.vertx.test.http.HttpTestBase;
29+
import org.junit.After;
30+
import org.junit.Before;
31+
import org.junit.ClassRule;
32+
import org.junit.Test;
33+
import org.junit.rules.TemporaryFolder;
34+
import org.junit.runner.RunWith;
35+
36+
import java.io.File;
37+
import java.nio.file.Files;
38+
import java.util.ArrayList;
39+
import java.util.Collections;
40+
import java.util.List;
41+
import java.util.concurrent.atomic.AtomicInteger;
42+
43+
import static org.junit.Assume.assumeTrue;
44+
45+
public class MultipartFormUploadTest extends HttpTestBase {
46+
47+
@ClassRule
48+
public static TemporaryFolder testFolder = new TemporaryFolder();
49+
50+
private VertxInternal vertx;
51+
52+
@Before
53+
public void setUp() throws Exception {
54+
super.setUp();
55+
vertx = (VertxInternal) Vertx.vertx();
56+
}
57+
58+
@Test
59+
public void testSimpleAttribute() throws Exception {
60+
Buffer result = Buffer.buffer();
61+
ContextInternal context = vertx.getOrCreateContext();
62+
ClientMultipartFormUpload upload = new ClientMultipartFormUpload(context, (ClientMultipartFormImpl) ClientForm.form().attribute("foo", "bar"), false, HttpPostRequestEncoder.EncoderMode.RFC1738);
63+
upload.endHandler(v -> {
64+
assertEquals("foo=bar", result.toString());
65+
testComplete();
66+
});
67+
upload.handler(result::appendBuffer);
68+
upload.resume();
69+
context.runOnContext(v -> upload.pump());
70+
}
71+
72+
@Test
73+
public void testFileUploadEventLoopContext() throws Exception {
74+
testFileUpload(vertx.createEventLoopContext(), false);
75+
}
76+
77+
@Test
78+
public void testFileUploadWorkerContext() throws Exception {
79+
testFileUpload(vertx.createWorkerContext(), false);
80+
}
81+
82+
@Test
83+
public void testFileUploadVirtualThreadContext() throws Exception {
84+
assumeTrue(vertx.isVirtualThreadAvailable());
85+
testFileUpload(vertx.createVirtualThreadContext(), false);
86+
}
87+
88+
@Test
89+
public void testFileUploadPausedEventLoopContext() throws Exception {
90+
testFileUpload(vertx.createEventLoopContext(), true);
91+
}
92+
93+
@Test
94+
public void testFileUploadPausedWorkerContext() throws Exception {
95+
testFileUpload(vertx.createWorkerContext(), true);
96+
}
97+
98+
@Test
99+
public void testFileUploadPausedVirtualThreadContext() throws Exception {
100+
assumeTrue(vertx.isVirtualThreadAvailable());
101+
testFileUpload(vertx.createVirtualThreadContext(), true);
102+
}
103+
104+
private void testFileUpload(ContextInternal context, boolean paused) throws Exception {
105+
File file = testFolder.newFile();
106+
Files.write(file.toPath(), TestUtils.randomByteArray(32 * 1024));
107+
108+
String filename = file.getName();
109+
String pathname = file.getAbsolutePath();
110+
111+
context.runOnContext(v1 -> {
112+
try {
113+
ClientMultipartFormUpload upload = new ClientMultipartFormUpload(context, (ClientMultipartFormImpl) ClientMultipartForm
114+
.multipartForm()
115+
.textFileUpload("the-file", filename, "text/plain", pathname)
116+
, true, HttpPostRequestEncoder.EncoderMode.RFC1738);
117+
List<Buffer> buffers = Collections.synchronizedList(new ArrayList<>());
118+
AtomicInteger end = new AtomicInteger();
119+
upload.endHandler(v2 -> {
120+
assertEquals(0, end.getAndIncrement());
121+
assertFalse(buffers.isEmpty());
122+
testComplete();
123+
});
124+
upload.handler(buffer -> {
125+
assertEquals(0, end.get());
126+
buffers.add(buffer);
127+
});
128+
if (!paused) {
129+
upload.resume();
130+
}
131+
upload.pump();
132+
if (paused) {
133+
context.runOnContext(v3 -> upload.resume());
134+
}
135+
} catch (Exception e) {
136+
fail(e);
137+
}
138+
});
139+
}
140+
}

0 commit comments

Comments
 (0)