Skip to content

Commit b9d8551

Browse files
committed
Improve HTTP/1.1 response send file implementation
Motivation: The implementation of HTTP/1.1 send file can be simplied by using two arguments FileChannel/RandomAccessFile instead of trying to abstract them. In addition we can set the response content type earlier when a file name is provided. Changes: Make the Http1xServerResponse#sendFileInternal accept both FileChannel/RandomAccessFile as those are the only arguments we can actually use. Set the response content type when a file name is available and to application/octet-stream in the generic case. Add test for sending RandomAccessFile instances.
1 parent 6b96ea7 commit b9d8551

File tree

5 files changed

+107
-148
lines changed

5 files changed

+107
-148
lines changed

vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ public interface HttpHeaders {
354354
@GenIgnore(GenIgnore.PERMITTED_TYPE)
355355
CharSequence APPLICATION_X_WWW_FORM_URLENCODED = HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED;
356356

357+
/**
358+
* application/application/octet-stream header value
359+
*/
360+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
361+
CharSequence APPLICATION_OCTET_STREAM = HttpHeaderValues.APPLICATION_OCTET_STREAM;
362+
357363
/**
358364
* multipart/form-data header value
359365
*/

vertx-core/src/main/java/io/vertx/core/http/HttpServerResponse.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ default Future<Void> sendFile(FileChannel channel, long offset) {
423423
@GenIgnore(GenIgnore.PERMITTED_TYPE)
424424
@Unstable
425425
default Future<Void> sendFile(RandomAccessFile file) {
426-
return sendFile(file.getChannel(), 0);
426+
return sendFile(file, 0);
427427
}
428428

429429
/**
@@ -437,7 +437,7 @@ default Future<Void> sendFile(RandomAccessFile file) {
437437
@GenIgnore(GenIgnore.PERMITTED_TYPE)
438438
@Unstable
439439
default Future<Void> sendFile(RandomAccessFile file, long offset) {
440-
return sendFile(file.getChannel(), offset, Long.MAX_VALUE);
440+
return sendFile(file, offset, Long.MAX_VALUE);
441441
}
442442

443443
/**
@@ -450,9 +450,7 @@ default Future<Void> sendFile(RandomAccessFile file, long offset) {
450450
*/
451451
@GenIgnore(GenIgnore.PERMITTED_TYPE)
452452
@Unstable
453-
default Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
454-
return sendFile(file.getChannel(), offset, length);
455-
}
453+
Future<Void> sendFile(RandomAccessFile file, long offset, long length);
456454

457455
/**
458456
* @return has the response already ended?

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

Lines changed: 39 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -449,18 +449,14 @@ public Future<Void> sendFile(String filename, long offset, long length) {
449449
} catch (Exception e) {
450450
return context.failedFuture(e);
451451
}
452-
Future<Void> result = sendFileInternal(filename, offset,
453-
length,
454-
MimeMapping::mimeTypeForFilename,
455-
(r) -> {
456-
try {
457-
return r.length();
458-
} catch (IOException e) {
459-
throw new RuntimeException(e);
460-
}
461-
},
462-
() -> raf,
463-
conn::sendFile);
452+
if (!headers.contains(HttpHeaders.CONTENT_TYPE)) {
453+
CharSequence mimeType = MimeMapping.mimeTypeForFilename(filename);
454+
if (mimeType == null) {
455+
mimeType = APPLICATION_OCTET_STREAM;
456+
}
457+
headers.set(CONTENT_TYPE, mimeType);
458+
}
459+
Future<Void> result = sendFile(raf, offset, length);
464460
if (result.failed()) {
465461
try {
466462
raf.close();
@@ -470,23 +466,35 @@ public Future<Void> sendFile(String filename, long offset, long length) {
470466
return result;
471467
}
472468

469+
@Override
470+
public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
471+
if (!headers.contains(HttpHeaders.CONTENT_TYPE)) {
472+
headers.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
473+
}
474+
long size;
475+
try {
476+
size = file.length();
477+
} catch (IOException e) {
478+
return context.failedFuture(e);
479+
}
480+
return sendFileInternal(offset, length, size, null, file);
481+
}
482+
473483
@Override
474484
public Future<Void> sendFile(FileChannel channel, long offset, long length) {
475-
return sendFileInternal("application/octet-stream", offset,
476-
length,
477-
Function.identity(),
478-
(c) -> {
479-
try {
480-
return c.size();
481-
} catch (IOException e) {
482-
throw new RuntimeException(e);
483-
}
484-
},
485-
() -> channel,
486-
conn::sendFile);
485+
if (!headers.contains(HttpHeaders.CONTENT_TYPE)) {
486+
headers.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
487+
}
488+
long size;
489+
try {
490+
size = channel.size();
491+
} catch (IOException e) {
492+
return context.failedFuture(e);
493+
}
494+
return sendFileInternal(offset, length, size, channel, null);
487495
}
488496

489-
private <F> Future<Void> sendFileInternal(String nameOrExtension, long offset, long length, Function<String, String> contentTypeMapper, Function<F, Long> lengthSupplier, Supplier<F> fileSupplier, Sender<F> sendFileSupplier) {
497+
private <F> Future<Void> sendFileInternal(long offset, long length, long size, FileChannel fileChannel, RandomAccessFile raf) {
490498
ContextInternal ctx = vertx.getOrCreateContext();
491499
if (offset < 0) {
492500
return ctx.failedFuture("offset : " + offset + " (expected: >= 0)");
@@ -500,12 +508,6 @@ private <F> Future<Void> sendFileInternal(String nameOrExtension, long offset, l
500508
throw new IllegalStateException("Head already written");
501509
}
502510

503-
long size;
504-
try {
505-
size = lengthSupplier.apply(fileSupplier.get());
506-
} catch (Exception e) {
507-
return ctx.failedFuture(e);
508-
}
509511
long actualLength = Math.min(length, size - offset);
510512
long actualOffset = Math.min(offset, size);
511513

@@ -514,19 +516,18 @@ private <F> Future<Void> sendFileInternal(String nameOrExtension, long offset, l
514516
return ctx.failedFuture("offset : " + offset + " is larger than the requested file length : " + size);
515517
}
516518

517-
if (!headers.contains(HttpHeaders.CONTENT_TYPE)) {
518-
String contentType = contentTypeMapper.apply(nameOrExtension);
519-
if (contentType != null) {
520-
headers.set(HttpHeaders.CONTENT_TYPE, contentType);
521-
}
522-
}
523519
prepareHeaders(actualLength);
524520
bytesWritten = actualLength;
525521
written = true;
526522

527523
conn.write(new VertxAssembledHttpResponse(head, version, status, headers), null);
528524

529-
ChannelFuture channelFut = sendFileSupplier.send(fileSupplier.get(), actualOffset, actualLength);
525+
ChannelFuture channelFut;
526+
if (fileChannel != null) {
527+
channelFut = conn.sendFile(fileChannel, actualOffset, actualLength);
528+
} else {
529+
channelFut = conn.sendFile(raf, actualOffset, actualLength);
530+
}
530531
channelFut.addListener(future -> {
531532

532533
// write an empty last content to let the http encoder know the response is complete
@@ -836,12 +837,4 @@ public HttpServerResponse addCookie(Cookie cookie) {
836837
return (Set) cookies().removeOrInvalidateAll(name, invalidate);
837838
}
838839
}
839-
840-
@FunctionalInterface
841-
private static interface Sender<F> {
842-
843-
ChannelFuture send(F u, Long v, Long w);
844-
845-
}
846-
847840
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.vertx.core.spi.observability.HttpResponse;
3535
import io.vertx.core.streams.ReadStream;
3636

37+
import java.io.RandomAccessFile;
3738
import java.nio.channels.FileChannel;
3839
import java.util.Map.Entry;
3940
import java.util.Set;
@@ -570,6 +571,11 @@ public Future<Void> sendFile(String filename, long offset, long length) {
570571
});
571572
}
572573

574+
@Override
575+
public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
576+
return stream.context.failedFuture("HTTP/2 does not support sending random access file for now");
577+
}
578+
573579
@Override
574580
public Future<Void> sendFile(FileChannel channel, long offset, long length) {
575581
return stream.context.failedFuture("HTTP/2 does not support sending channel for now");

vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java

Lines changed: 53 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2305,114 +2305,70 @@ private void testSendFileWithFailure(BiFunction<HttpServerResponse, File, Future
23052305

23062306
@Test
23072307
public void testSendFileWithFileChannel() throws Exception {
2308-
int expected = 16 * 1024 * 1024;
2309-
File file = TestUtils.tmpFile(".dat", expected);
2310-
FileChannel channel = FileChannel.open(file.toPath());
2311-
server.requestHandler(
2312-
req -> {
2313-
req.response().sendFile(channel).onFailure(
2314-
t -> {
2315-
if (HttpTest.this instanceof Http2Test) {
2316-
req.response().end();
2317-
}
2318-
}
2319-
);
2320-
});
2321-
startServer(testAddress);
2322-
Object[] res = {0, ""};
2323-
Object[] r = client.request(requestOptions)
2324-
.compose(req -> req.send()
2325-
.compose(resp -> {
2326-
resp.handler(buff -> {
2327-
Integer length = (Integer) res[0];
2328-
length += buff.length();
2329-
res[0] = length;
2330-
});
2331-
res[1] = resp.getHeader("Content-Type");
2332-
resp.exceptionHandler(this::fail);
2333-
return resp.end();
2334-
}))
2335-
.map(v -> res).await();
2336-
if (this instanceof Http1xTest){
2337-
assertEquals((int) r[0], file.length());
2338-
assertEquals("application/octet-stream", r[1]);
2339-
}
2308+
int fileLength = 16 * 1024 * 1024;
2309+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response.sendFile(file.getChannel());
2310+
testSendFileWithFileChannel(fileLength, sender, "application/octet-stream", fileLength);
23402311
}
23412312

23422313
@Test
23432314
public void testSendFileWithFileChannelAndExtension() throws Exception {
2344-
int expected = 16 * 1024 * 1024;
2345-
File file = TestUtils.tmpFile(".dat", expected);
2346-
FileChannel channel = FileChannel.open(file.toPath());
2347-
server.requestHandler(
2348-
req -> {
2349-
req.response()
2350-
.putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4")
2351-
.sendFile(channel).onFailure(
2352-
t -> {
2353-
if (HttpTest.this instanceof Http2Test) {
2354-
req.response().end();
2355-
}
2356-
}
2357-
);
2358-
});
2359-
startServer(testAddress);
2360-
Object[] res = {0, ""};
2361-
Object[] r = client.request(requestOptions)
2362-
.compose(req -> req.send()
2363-
.compose(resp -> {
2364-
resp.handler(buff -> {
2365-
Integer length = (Integer) res[0];
2366-
length += buff.length();
2367-
res[0] = length;
2368-
});
2369-
res[1] = resp.getHeader("Content-Type");
2370-
resp.exceptionHandler(this::fail);
2371-
return resp.end();
2372-
}))
2373-
.map(v -> res).await();
2374-
if (this instanceof Http1xTest){
2375-
assertEquals((int)r[0], file.length());
2376-
assertEquals(r[1], "video/mp4");
2377-
}
2315+
int fileLength = 16 * 1024 * 1024;
2316+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response
2317+
.putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4")
2318+
.sendFile(file.getChannel());
2319+
testSendFileWithFileChannel(fileLength, sender, "video/mp4", fileLength);
23782320
}
23792321

23802322
@Test
23812323
public void testSendFileWithFileChannelRange() throws Exception {
23822324
int fileLength = 16 * 1024 * 1024;
2383-
File file = TestUtils.tmpFile(".dat", fileLength);
2384-
FileChannel channel = FileChannel.open(file.toPath());
23852325
int offset = 1024 * 4;
23862326
int expectedRange = fileLength - offset;
2387-
server.requestHandler(
2388-
req -> {
2389-
req.response().putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4");
2390-
req.response().sendFile(channel, offset, expectedRange).onFailure(
2391-
t -> {
2392-
if (HttpTest.this instanceof Http2Test) {
2393-
req.response().end();
2394-
}
2395-
}
2396-
);
2397-
});
2398-
startServer(testAddress);
2399-
Object[] res = {0, ""};
2400-
Object[] r = client.request(requestOptions)
2401-
.compose(req -> req.send()
2402-
.compose(resp -> {
2403-
resp.handler(buff -> {
2404-
Integer length = (Integer) res[0];
2405-
length += buff.length();
2406-
res[0] = length;
2407-
});
2408-
res[1] = resp.getHeader("Content-Type");
2409-
resp.exceptionHandler(this::fail);
2410-
return resp.end();
2411-
}))
2412-
.map(v -> res).await();
2413-
if (this instanceof Http1xTest) {
2414-
assertEquals(expectedRange, (int) r[0]);
2415-
assertEquals("video/mp4", r[1]);
2327+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response
2328+
.putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4")
2329+
.sendFile(file.getChannel(), offset, expectedRange);
2330+
testSendFileWithFileChannel(fileLength, sender, "video/mp4", expectedRange);
2331+
}
2332+
2333+
@Test
2334+
public void testSendFileWithRandomAccessFile() throws Exception {
2335+
int fileLength = 16 * 1024 * 1024;
2336+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response.sendFile(file);
2337+
testSendFileWithFileChannel(fileLength, sender, "application/octet-stream", fileLength);
2338+
}
2339+
2340+
@Test
2341+
public void testSendFileWithRandomAccessFileAndExtension() throws Exception {
2342+
int fileLength = 16 * 1024 * 1024;
2343+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response
2344+
.putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4")
2345+
.sendFile(file);
2346+
testSendFileWithFileChannel(fileLength, sender, "video/mp4", fileLength);
2347+
}
2348+
2349+
@Test
2350+
public void testSendFileWithRandomAccessFileRange() throws Exception {
2351+
int fileLength = 16 * 1024 * 1024;
2352+
int offset = 1024 * 4;
2353+
int expectedRange = fileLength - offset;
2354+
BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender = (file, response) -> response
2355+
.putHeader(HttpHeaders.CONTENT_TYPE, "video/mp4")
2356+
.sendFile(file, offset, expectedRange);
2357+
testSendFileWithFileChannel(fileLength, sender, "video/mp4", expectedRange);
2358+
}
2359+
2360+
private void testSendFileWithFileChannel(int flen, BiFunction<RandomAccessFile, HttpServerResponse, Future<?>> sender, String expectedContentType, long expectedLength) throws Exception {
2361+
Assume.assumeTrue(this instanceof Http1xTest);
2362+
File file = TestUtils.tmpFile(".dat", flen);
2363+
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
2364+
server.requestHandler(req -> sender.apply(raf, req.response()).onComplete(onSuccess(v -> testComplete())));
2365+
startServer(testAddress);
2366+
Buffer body = client.request(requestOptions)
2367+
.compose(req -> req.send()
2368+
.expecting(HttpResponseExpectation.contentType(expectedContentType))
2369+
.compose(HttpClientResponse::body)).await();
2370+
assertEquals(body.length(), expectedLength);
2371+
await();
24162372
}
24172373
}
24182374

0 commit comments

Comments
 (0)