Skip to content

Commit d96f0fa

Browse files
committed
Expect/Continue support: Add ServerOptions.expectContinue option
- Whenever 100-Expect and continue requests are handled by the server. This is off by default, except for Jetty which is always ON. - Fixes jooby-project#2363
1 parent b4f834f commit d96f0fa

File tree

7 files changed

+120
-6
lines changed

7 files changed

+120
-6
lines changed

docs/asciidoc/servers.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Server options are available via javadoc:ServerOptions[] class:
4949
.setSecurePort(8433)
5050
.setSsl(SslOptions.selfSigned())
5151
.setHttp2(true)
52+
.setExpectContinue(true)
5253
);
5354
}
5455
----
@@ -70,6 +71,7 @@ Server options are available via javadoc:ServerOptions[] class:
7071
securePort = 8443
7172
ssl = SslOptions.selfSigned()
7273
http2 = true
74+
expectContinue = true
7375
}
7476
}
7577
----
@@ -86,6 +88,8 @@ Server options are available via javadoc:ServerOptions[] class:
8688
- securePort: Enable HTTPS. This option is fully covered in next section.
8789
- ssl: SSL options with certificate details. This option is fully covered in next section.
8890
- http2: Enable HTTP 2.0.
91+
- expectContinue: Whenever 100-Expect and continue requests are handled by the server. This is off
92+
by default, except for Jetty which is always ON.
8993

9094
Server options are available as application configuration properties too:
9195

@@ -104,6 +108,7 @@ server.maxRequestSize = 10485760
104108
server.securePort = 8443
105109
server.ssl.type = self-signed | PKCS12 | X509
106110
server.http2 = true
111+
server.expectContinue = false
107112
----
108113

109114
=== HTTPS Support

jooby/src/main/java/io/jooby/ServerOptions.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public class ServerOptions {
105105

106106
private Boolean http2;
107107

108+
private Boolean expectContinue;
109+
108110
/**
109111
* Creates server options from config object. The configuration options must provided entries
110112
* like: <code>server.port</code>, <code>server.ioThreads</code>, etc...
@@ -148,6 +150,9 @@ public class ServerOptions {
148150
if (conf.hasPath("server.host")) {
149151
options.setHost(conf.getString("server.host"));
150152
}
153+
if (conf.hasPath("server.expectContinue")) {
154+
options.setExpectContinue(conf.getBoolean("server.expectContinue"));
155+
}
151156
// ssl
152157
SslOptions.from(conf, "server.ssl").ifPresent(options::setSsl);
153158
if (conf.hasPath("server.http2")) {
@@ -496,6 +501,28 @@ public ServerOptions setHttp2(@Nullable Boolean http2) {
496501
return this;
497502
}
498503

504+
/**
505+
* Whenever 100-Expect and continue requests are handled by the server.
506+
* This is off by default, except for Jetty which is always ON.
507+
*
508+
* @return True when enabled.
509+
*/
510+
public @Nullable Boolean isExpectContinue() {
511+
return expectContinue;
512+
}
513+
514+
/**
515+
* Set 100-Expect and continue requests are handled by the server.
516+
* This is off by default, except for Jetty which is always ON.
517+
*
518+
* @param expectContinue True or false.
519+
* @return This options.
520+
*/
521+
public ServerOptions setExpectContinue(@Nullable Boolean expectContinue) {
522+
this.expectContinue = expectContinue;
523+
return this;
524+
}
525+
499526
/**
500527
* Creates SSL context using the given resource loader. This method attempts to create a
501528
* SSLContext when:

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package io.jooby.internal.netty;
77

8+
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
9+
810
import java.nio.charset.StandardCharsets;
911
import java.time.ZoneOffset;
1012
import java.time.ZonedDateTime;
@@ -21,13 +23,17 @@
2123
import io.jooby.StatusCode;
2224
import io.jooby.WebSocketCloseStatus;
2325
import io.jooby.exception.StatusCodeException;
26+
import io.netty.buffer.Unpooled;
2427
import io.netty.channel.ChannelHandlerContext;
2528
import io.netty.channel.ChannelInboundHandlerAdapter;
29+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
30+
import io.netty.handler.codec.http.FullHttpResponse;
2631
import io.netty.handler.codec.http.HttpContent;
2732
import io.netty.handler.codec.http.HttpHeaderNames;
2833
import io.netty.handler.codec.http.HttpHeaderValues;
2934
import io.netty.handler.codec.http.HttpRequest;
3035
import io.netty.handler.codec.http.HttpUtil;
36+
import io.netty.handler.codec.http.HttpVersion;
3137
import io.netty.handler.codec.http.LastHttpContent;
3238
import io.netty.handler.codec.http.multipart.HttpDataFactory;
3339
import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;
@@ -52,6 +58,7 @@ public class NettyHandler extends ChannelInboundHandlerAdapter {
5258
private final Router router;
5359
private final int bufferSize;
5460
private final boolean defaultHeaders;
61+
private final boolean is100ContinueExpected;
5562
private NettyContext context;
5663

5764
private final HttpDataFactory factory;
@@ -62,20 +69,23 @@ public class NettyHandler extends ChannelInboundHandlerAdapter {
6269
private long chunkSize;
6370

6471
public NettyHandler(ScheduledExecutorService scheduler, Router router, long maxRequestSize,
65-
int bufferSize, HttpDataFactory factory, boolean defaultHeaders) {
72+
int bufferSize, HttpDataFactory factory, boolean defaultHeaders,
73+
boolean is100ContinueExpected) {
6674
this.scheduler = scheduler;
6775
this.router = router;
6876
this.maxRequestSize = maxRequestSize;
6977
this.factory = factory;
7078
this.bufferSize = bufferSize;
7179
this.defaultHeaders = defaultHeaders;
80+
this.is100ContinueExpected = is100ContinueExpected;
7281
}
7382

7483
@Override
7584
public void channelRead(ChannelHandlerContext ctx, Object msg) {
7685
try {
7786
if (msg instanceof HttpRequest) {
7887
HttpRequest req = (HttpRequest) msg;
88+
7989
context = new NettyContext(ctx, req, router, pathOnly(req.uri()), bufferSize);
8090

8191
if (defaultHeaders) {

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.netty.channel.ChannelPipeline;
2020
import io.netty.channel.socket.SocketChannel;
2121
import io.netty.handler.codec.http.HttpServerCodec;
22+
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
2223
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
2324
import io.netty.handler.codec.http.multipart.HttpDataFactory;
2425
import io.netty.handler.ssl.SslContext;
@@ -35,11 +36,12 @@ public class NettyPipeline extends ChannelInitializer<SocketChannel> {
3536
private final ScheduledExecutorService service;
3637
private final SslContext sslContext;
3738
private final Http2Configurer<Http2Extension, ChannelInboundHandler> http2;
39+
private final boolean is100ContinueExpected;
3840

3941
public NettyPipeline(ScheduledExecutorService service, Router router, HttpDataFactory factory,
4042
SslContext sslContext, Http2Configurer<Http2Extension, ChannelInboundHandler> http2,
41-
boolean defaultHeaders,
42-
Integer compressionLevel, int bufferSize, long maxRequestSize) {
43+
boolean defaultHeaders, Integer compressionLevel, int bufferSize, long maxRequestSize,
44+
boolean is100ContinueExpected) {
4345
this.service = service;
4446
this.router = router;
4547
this.factory = factory;
@@ -49,6 +51,7 @@ public NettyPipeline(ScheduledExecutorService service, Router router, HttpDataFa
4951
this.compressionLevel = compressionLevel;
5052
this.bufferSize = bufferSize;
5153
this.maxRequestSize = maxRequestSize;
54+
this.is100ContinueExpected = is100ContinueExpected;
5255
}
5356

5457
@Override
@@ -72,7 +75,9 @@ public void initChannel(SocketChannel ch) {
7275
p.addLast("compressor", new HttpChunkContentCompressor(compressionLevel));
7376
p.addLast("ws-compressor", new NettyWebSocketCompressor(compressionLevel));
7477
}
75-
78+
if (is100ContinueExpected) {
79+
p.addLast("expect-continue", new HttpServerExpectContinueHandler());
80+
}
7681
p.addLast("handler", createHandler());
7782
}
7883
}
@@ -104,6 +109,9 @@ private void http11(ChannelPipeline p) {
104109
p.addLast("compressor", new HttpChunkContentCompressor(compressionLevel));
105110
p.addLast("ws-compressor", new NettyWebSocketCompressor(compressionLevel));
106111
}
112+
if (is100ContinueExpected) {
113+
p.addLast("expect-continue", new HttpServerExpectContinueHandler());
114+
}
107115
p.addLast("handler", createHandler());
108116
}
109117

@@ -112,6 +120,7 @@ HttpServerCodec createServerCodec() {
112120
}
113121

114122
private NettyHandler createHandler() {
115-
return new NettyHandler(service, router, maxRequestSize, bufferSize, factory, defaultHeaders);
123+
return new NettyHandler(service, router, maxRequestSize, bufferSize, factory, defaultHeaders,
124+
is100ContinueExpected);
116125
}
117126
}

modules/jooby-netty/src/main/java/io/jooby/netty/Netty.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ private NettyPipeline newPipeline(HttpDataFactory factory, SslContext sslContext
189189
options.getDefaultHeaders(),
190190
options.getCompressionLevel(),
191191
options.getBufferSize(),
192-
options.getMaxRequestSize()
192+
options.getMaxRequestSize(),
193+
options.isExpectContinue() == Boolean.TRUE
193194
);
194195
}
195196

modules/jooby-utow/src/main/java/io/jooby/utow/Utow.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.undertow.Undertow;
3434
import io.undertow.UndertowOptions;
3535
import io.undertow.server.HttpHandler;
36+
import io.undertow.server.handlers.HttpContinueReadHandler;
3637
import io.undertow.server.handlers.encoding.ContentEncodingRepository;
3738
import io.undertow.server.handlers.encoding.DeflateEncodingProvider;
3839
import io.undertow.server.handlers.encoding.EncodingHandler;
@@ -87,6 +88,10 @@ public class Utow extends Server.Base {
8788
.addEncodingHandler("deflate", new DeflateEncodingProvider(compressionLevel), _10));
8889
}
8990

91+
if (options.isExpectContinue() == Boolean.TRUE) {
92+
handler = new HttpContinueReadHandler(handler);
93+
}
94+
9095
Undertow.Builder builder = Undertow.builder()
9196
.addHttpListener(options.getPort(), options.getHost())
9297
.setBufferSize(options.getBufferSize())
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.jooby.i2363;
2+
3+
import static okhttp3.RequestBody.create;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Path;
8+
import java.nio.file.Paths;
9+
10+
import io.jooby.ServerOptions;
11+
import io.jooby.junit.ServerTest;
12+
import io.jooby.junit.ServerTestRunner;
13+
import okhttp3.MediaType;
14+
import okhttp3.MultipartBody;
15+
16+
public class Issue2363 {
17+
18+
/**
19+
* Test for https://github.com/jooby-project/jooby/issues/2363.
20+
*
21+
* We just make sure it works as expected but I can't figure it out how to test/assert
22+
* for a 100 response using OKHttpClient.
23+
*
24+
* @param runner Test runner.
25+
*/
26+
@ServerTest
27+
public void shouldAllowExpectAndContinue(ServerTestRunner runner) {
28+
runner.define(app -> {
29+
app.setServerOptions(new ServerOptions().setExpectContinue(true));
30+
31+
app.post("/2363", ctx -> new String(ctx.file("f").bytes(), StandardCharsets.UTF_8));
32+
33+
}).ready(http -> {
34+
http.header("Expect", "100-continue")
35+
.post("/2363", new MultipartBody.Builder()
36+
.setType(MultipartBody.FORM)
37+
.addFormDataPart("f", "fileupload.js",
38+
create(userdir("src", "test", "resources", "files", "fileupload.js").toFile(),
39+
MediaType.parse("application/javascript")))
40+
.build(), rsp -> {
41+
assertEquals(200, rsp.code());
42+
assertEquals("(function () {\n"
43+
+ " console.log('ready');\n"
44+
+ "})();\n",
45+
rsp.body().string());
46+
});
47+
});
48+
}
49+
50+
private static Path userdir(String... segments) {
51+
Path path = Paths.get(System.getProperty("user.dir"));
52+
for (String segment : segments) {
53+
path = path.resolve(segment);
54+
}
55+
return path;
56+
}
57+
}

0 commit comments

Comments
 (0)