Skip to content

Commit

Permalink
Manual 100 continue handling
Browse files Browse the repository at this point in the history
  • Loading branch information
purplefox committed Jul 2, 2015
1 parent 9c8eed0 commit 57a65be
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 22 deletions.
5 changes: 5 additions & 0 deletions src/main/asciidoc/cheatsheet/HttpServerOptions.adoc
Expand Up @@ -135,4 +135,9 @@ Set the websocket subprotocols supported by the server.+++
|`Boolean` |`Boolean`
|+++ |+++
Set whether client auth is required+++ Set whether client auth is required+++

|[[handle100ContinueAutomatically]]`handle100ContinueAutomatically`
|`Boolean`
|+++
Set whether 100 Continue should be handled automatically+++
|=== |===
11 changes: 11 additions & 0 deletions src/main/asciidoc/java/http.adoc
Expand Up @@ -1047,6 +1047,17 @@ request.continueHandler(v -> {
}); });
---- ----


On the server side a Vert.x http server can be configured to automatically send back 100 Continue interim responses
when it receives an `Expect: 100-Continue` header.
This is done by setting the option `link:../../apidocs/io/vertx/core/http/HttpServerOptions.html#setHandle100ContinueAutomatically-boolean-[setHandle100ContinueAutomatically]`.

If you'd prefer to decide whether to send back continue responses manually, then this property should be set to
`false` (the default), then you can inspect the headers and call `link:../../apidocs/io/vertx/core/http/HttpServerResponse.html#writeContinue--[writeContinue]`
if you wish the client to continue sending the body or you can reject the request by sending back a failure status code
if you don't want it to send the body. For example:



=== Enabling compression on the client === Enabling compression on the client


The http client comes with support for HTTP Compression out of the box. The http client comes with support for HTTP Compression out of the box.
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/examples/HTTPExamples.java
Expand Up @@ -523,6 +523,26 @@ public void example50(HttpClient client) {
}); });
} }


public void example50_1(HttpServer httpServer) {

httpServer.requestHandler(request -> {
if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) {
// Now decide if you want to accept the request

boolean accept = true;
if (accept) {
request.response().writeContinue();
request.bodyHandler(body -> {
// Do something with body
});
} else {
// Reject with a failure code
request.response().setStatusCode(405).end();
}
}
});
}

public void example51(HttpServer server) { public void example51(HttpServer server) {


server.websocketHandler(websocket -> { server.websocketHandler(websocket -> {
Expand Down
41 changes: 32 additions & 9 deletions src/main/java/io/vertx/core/http/HttpServerOptions.java
Expand Up @@ -19,12 +19,7 @@
import io.vertx.codegen.annotations.DataObject; import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.net.JksOptions; import io.vertx.core.net.*;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.PfxOptions;
import io.vertx.core.net.TCPSSLOptions;


/** /**
* Represents options used by an {@link io.vertx.core.http.HttpServer} instance * Represents options used by an {@link io.vertx.core.http.HttpServer} instance
Expand All @@ -49,9 +44,15 @@ public class HttpServerOptions extends NetServerOptions {
*/ */
public static final int DEFAULT_MAX_WEBSOCKET_FRAME_SIZE = 65536; public static final int DEFAULT_MAX_WEBSOCKET_FRAME_SIZE = 65536;


/**
* Default value of whether 100-Continue should be handled automatically
*/
public static final boolean DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY = false;

private boolean compressionSupported; private boolean compressionSupported;
private int maxWebsocketFrameSize; private int maxWebsocketFrameSize;
private String websocketSubProtocols; private String websocketSubProtocols;
private boolean handle100ContinueAutomatically;


/** /**
* Default constructor * Default constructor
Expand All @@ -61,6 +62,7 @@ public HttpServerOptions() {
setPort(DEFAULT_PORT); // We override the default for port setPort(DEFAULT_PORT); // We override the default for port
compressionSupported = DEFAULT_COMPRESSION_SUPPORTED; compressionSupported = DEFAULT_COMPRESSION_SUPPORTED;
maxWebsocketFrameSize = DEFAULT_MAX_WEBSOCKET_FRAME_SIZE; maxWebsocketFrameSize = DEFAULT_MAX_WEBSOCKET_FRAME_SIZE;
handle100ContinueAutomatically = DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY;
} }


/** /**
Expand All @@ -73,6 +75,7 @@ public HttpServerOptions(HttpServerOptions other) {
this.compressionSupported = other.isCompressionSupported(); this.compressionSupported = other.isCompressionSupported();
this.maxWebsocketFrameSize = other.getMaxWebsocketFrameSize(); this.maxWebsocketFrameSize = other.getMaxWebsocketFrameSize();
this.websocketSubProtocols = other.getWebsocketSubProtocols(); this.websocketSubProtocols = other.getWebsocketSubProtocols();
this.handle100ContinueAutomatically = other.handle100ContinueAutomatically;
} }


/** /**
Expand All @@ -85,6 +88,8 @@ public HttpServerOptions(JsonObject json) {
this.compressionSupported = json.getBoolean("compressionSupported", DEFAULT_COMPRESSION_SUPPORTED); this.compressionSupported = json.getBoolean("compressionSupported", DEFAULT_COMPRESSION_SUPPORTED);
this.maxWebsocketFrameSize = json.getInteger("maxWebsocketFrameSize", DEFAULT_MAX_WEBSOCKET_FRAME_SIZE); this.maxWebsocketFrameSize = json.getInteger("maxWebsocketFrameSize", DEFAULT_MAX_WEBSOCKET_FRAME_SIZE);
this.websocketSubProtocols = json.getString("websocketSubProtocols", null); this.websocketSubProtocols = json.getString("websocketSubProtocols", null);
this.handle100ContinueAutomatically = json.getBoolean("handle100ContinueAutomatically",
DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY);
setPort(json.getInteger("port", DEFAULT_PORT)); setPort(json.getInteger("port", DEFAULT_PORT));
} }


Expand Down Expand Up @@ -273,19 +278,36 @@ public HttpServerOptions setClientAuthRequired(boolean clientAuthRequired) {
return this; return this;
} }


/**
* @return whether 100 Continue should be handled automatically
*/
public boolean isHandle100ContinueAutomatically() {
return handle100ContinueAutomatically;
}

/**
* Set whether 100 Continue should be handled automatically
* @param handle100ContinueAutomatically true if it should be handled automatically
* @return a reference to this, so the API can be used fluently
*/
public HttpServerOptions setHandle100ContinueAutomatically(boolean handle100ContinueAutomatically) {
this.handle100ContinueAutomatically = handle100ContinueAutomatically;
return this;
}

@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (!(o instanceof HttpServerOptions)) return false; if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false; if (!super.equals(o)) return false;


HttpServerOptions that = (HttpServerOptions) o; HttpServerOptions that = (HttpServerOptions) o;


if (compressionSupported != that.compressionSupported) return false; if (compressionSupported != that.compressionSupported) return false;
if (maxWebsocketFrameSize != that.maxWebsocketFrameSize) return false; if (maxWebsocketFrameSize != that.maxWebsocketFrameSize) return false;
if (websocketSubProtocols != null ? !websocketSubProtocols.equals(that.websocketSubProtocols) : that.websocketSubProtocols != null) return false; if (handle100ContinueAutomatically != that.handle100ContinueAutomatically) return false;
return !(websocketSubProtocols != null ? !websocketSubProtocols.equals(that.websocketSubProtocols) : that.websocketSubProtocols != null);


return true;
} }


@Override @Override
Expand All @@ -294,6 +316,7 @@ public int hashCode() {
result = 31 * result + (compressionSupported ? 1 : 0); result = 31 * result + (compressionSupported ? 1 : 0);
result = 31 * result + maxWebsocketFrameSize; result = 31 * result + maxWebsocketFrameSize;
result = 31 * result + (websocketSubProtocols != null ? websocketSubProtocols.hashCode() : 0); result = 31 * result + (websocketSubProtocols != null ? websocketSubProtocols.hashCode() : 0);
result = 31 * result + (handle100ContinueAutomatically ? 1 : 0);
return result; return result;
} }
} }
8 changes: 8 additions & 0 deletions src/main/java/io/vertx/core/http/HttpServerResponse.java
Expand Up @@ -212,6 +212,14 @@ public interface HttpServerResponse extends WriteStream<Buffer> {
@Fluent @Fluent
HttpServerResponse write(String chunk); HttpServerResponse write(String chunk);


/**
* Used to write an interim 100 Continue response to signify that the client should send the rest of the request.
* Must only be used if the request contains an "Expect:100-Continue" header
* @return a reference to this, so the API can be used fluently
*/
@Fluent
HttpServerResponse writeContinue();

/** /**
* Same as {@link #end(Buffer)} but writes a String in UTF-8 encoding before ending the response. * Same as {@link #end(Buffer)} but writes a String in UTF-8 encoding before ending the response.
* *
Expand Down
Expand Up @@ -279,6 +279,12 @@ public HttpServerResponseImpl write(String chunk) {
return write(Buffer.buffer(chunk).getByteBuf(), null); return write(Buffer.buffer(chunk).getByteBuf(), null);
} }


@Override
public HttpServerResponse writeContinue() {
conn.write100Continue();
return this;
}

@Override @Override
public void end(String chunk) { public void end(String chunk) {
end(Buffer.buffer(chunk)); end(Buffer.buffer(chunk));
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/io/vertx/core/http/impl/ServerConnection.java
Expand Up @@ -69,15 +69,12 @@ class ServerConnection extends ConnectionBase {
private static final Logger log = LoggerFactory.getLogger(ServerConnection.class); private static final Logger log = LoggerFactory.getLogger(ServerConnection.class);


private static final int CHANNEL_PAUSE_QUEUE_SIZE = 5; private static final int CHANNEL_PAUSE_QUEUE_SIZE = 5;
public static final String HANDLE_100_CONTINUE_PROP_NAME = "vertx.handle100Continue";
private static final boolean HANDLE_100_CONTINUE = Boolean.getBoolean(HANDLE_100_CONTINUE_PROP_NAME);


private final Queue<Object> pending = new ArrayDeque<>(8); private final Queue<Object> pending = new ArrayDeque<>(8);
private final String serverOrigin; private final String serverOrigin;
private final HttpServerImpl server; private final HttpServerImpl server;
private WebSocketServerHandshaker handshaker; private WebSocketServerHandshaker handshaker;
private final HttpServerMetrics metrics; private final HttpServerMetrics metrics;

private Object requestMetric; private Object requestMetric;
private Handler<HttpServerRequest> requestHandler; private Handler<HttpServerRequest> requestHandler;
private Handler<ServerWebSocket> wsHandler; private Handler<ServerWebSocket> wsHandler;
Expand Down Expand Up @@ -320,6 +317,10 @@ synchronized void handleWebsocketConnect(ServerWebSocketImpl ws) {
} }
} }


void write100Continue() {
channel.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}

synchronized private void handleWsFrame(WebSocketFrameInternal frame) { synchronized private void handleWsFrame(WebSocketFrameInternal frame) {
if (ws != null) { if (ws != null) {
ws.handleFrame(frame); ws.handleFrame(frame);
Expand Down Expand Up @@ -380,12 +381,11 @@ private void processMessage(Object msg) {
channel.pipeline().fireExceptionCaught(result.cause()); channel.pipeline().fireExceptionCaught(result.cause());
return; return;
} }
if (HANDLE_100_CONTINUE) { if (server.options().isHandle100ContinueAutomatically()) {
if (HttpHeaders.is100ContinueExpected(request)) { if (HttpHeaders.is100ContinueExpected(request)) {
channel.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); write100Continue();
} }
} }

HttpServerResponseImpl resp = new HttpServerResponseImpl(vertx, this, request); HttpServerResponseImpl resp = new HttpServerResponseImpl(vertx, this, request);
HttpServerRequestImpl req = new HttpServerRequestImpl(this, request, resp); HttpServerRequestImpl req = new HttpServerRequestImpl(this, request, resp);
handleRequest(req, resp); handleRequest(req, resp);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/vertx/core/http/package-info.java
Expand Up @@ -831,6 +831,17 @@
* {@link examples.HTTPExamples#example50} * {@link examples.HTTPExamples#example50}
* ---- * ----
* *
* On the server side a Vert.x http server can be configured to automatically send back 100 Continue interim responses
* when it receives an `Expect: 100-Continue` header.
* This is done by setting the option {@link io.vertx.core.http.HttpServerOptions#setHandle100ContinueAutomatically(boolean)}.
*
* If you'd prefer to decide whether to send back continue responses manually, then this property should be set to
* `false` (the default), then you can inspect the headers and call {@link io.vertx.core.http.HttpServerResponse#writeContinue()}
* if you wish the client to continue sending the body or you can reject the request by sending back a failure status code
* if you don't want it to send the body. For example:
*
*
*
* === Enabling compression on the client * === Enabling compression on the client
* *
* The http client comes with support for HTTP Compression out of the box. * The http client comes with support for HTTP Compression out of the box.
Expand Down
60 changes: 53 additions & 7 deletions src/test/java/io/vertx/test/core/HttpTest.java
Expand Up @@ -68,7 +68,8 @@ public class HttpTest extends HttpTestBase {
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
testDir = testFolder.newFolder(); testDir = testFolder.newFolder();
server = vertx.createHttpServer(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST)); server = vertx.createHttpServer(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST)
.setHandle100ContinueAutomatically(true));
client = vertx.createHttpClient(new HttpClientOptions()); client = vertx.createHttpClient(new HttpClientOptions());
} }


Expand Down Expand Up @@ -291,6 +292,10 @@ public void testServerOptions() {
assertTrue(options.getEnabledCipherSuites().contains("foo")); assertTrue(options.getEnabledCipherSuites().contains("foo"));
assertTrue(options.getEnabledCipherSuites().contains("bar")); assertTrue(options.getEnabledCipherSuites().contains("bar"));


assertFalse(options.isHandle100ContinueAutomatically());
assertEquals(options, options.setHandle100ContinueAutomatically(true));
assertTrue(options.isHandle100ContinueAutomatically());

testComplete(); testComplete();
} }


Expand Down Expand Up @@ -526,6 +531,7 @@ public void testCopyServerOptions() {
boolean compressionSupported = rand.nextBoolean(); boolean compressionSupported = rand.nextBoolean();
int maxWebsocketFrameSize = TestUtils.randomPositiveInt(); int maxWebsocketFrameSize = TestUtils.randomPositiveInt();
String wsSubProtocol = TestUtils.randomAlphaString(10); String wsSubProtocol = TestUtils.randomAlphaString(10);
boolean is100ContinueHandledAutomatically = rand.nextBoolean();
options.setSendBufferSize(sendBufferSize); options.setSendBufferSize(sendBufferSize);
options.setReceiveBufferSize(receiverBufferSize); options.setReceiveBufferSize(receiverBufferSize);
options.setReuseAddress(reuseAddress); options.setReuseAddress(reuseAddress);
Expand All @@ -547,6 +553,7 @@ public void testCopyServerOptions() {
options.setCompressionSupported(compressionSupported); options.setCompressionSupported(compressionSupported);
options.setMaxWebsocketFrameSize(maxWebsocketFrameSize); options.setMaxWebsocketFrameSize(maxWebsocketFrameSize);
options.setWebsocketSubProtocol(wsSubProtocol); options.setWebsocketSubProtocol(wsSubProtocol);
options.setHandle100ContinueAutomatically(is100ContinueHandledAutomatically);
HttpServerOptions copy = new HttpServerOptions(options); HttpServerOptions copy = new HttpServerOptions(options);
assertEquals(sendBufferSize, copy.getSendBufferSize()); assertEquals(sendBufferSize, copy.getSendBufferSize());
assertEquals(receiverBufferSize, copy.getReceiveBufferSize()); assertEquals(receiverBufferSize, copy.getReceiveBufferSize());
Expand All @@ -572,8 +579,9 @@ public void testCopyServerOptions() {
assertEquals(host, copy.getHost()); assertEquals(host, copy.getHost());
assertEquals(acceptBacklog, copy.getAcceptBacklog()); assertEquals(acceptBacklog, copy.getAcceptBacklog());
assertEquals(compressionSupported, copy.isCompressionSupported()); assertEquals(compressionSupported, copy.isCompressionSupported());
assertEquals(maxWebsocketFrameSize, options.getMaxWebsocketFrameSize()); assertEquals(maxWebsocketFrameSize, copy.getMaxWebsocketFrameSize());
assertEquals(wsSubProtocol, options.getWebsocketSubProtocols()); assertEquals(wsSubProtocol, copy.getWebsocketSubProtocols());
assertEquals(is100ContinueHandledAutomatically, copy.isHandle100ContinueAutomatically());
} }


@Test @Test
Expand All @@ -594,6 +602,7 @@ public void testDefaultServerOptionsJson() {
assertEquals(def.getSoLinger(), json.getSoLinger()); assertEquals(def.getSoLinger(), json.getSoLinger());
assertEquals(def.isUsePooledBuffers(), json.isUsePooledBuffers()); assertEquals(def.isUsePooledBuffers(), json.isUsePooledBuffers());
assertEquals(def.isSsl(), json.isSsl()); assertEquals(def.isSsl(), json.isSsl());
assertEquals(def.isHandle100ContinueAutomatically(), json.isHandle100ContinueAutomatically());
} }


@Test @Test
Expand Down Expand Up @@ -627,6 +636,7 @@ public void testServerOptionsJson() {
boolean compressionSupported = rand.nextBoolean(); boolean compressionSupported = rand.nextBoolean();
int maxWebsocketFrameSize = TestUtils.randomPositiveInt(); int maxWebsocketFrameSize = TestUtils.randomPositiveInt();
String wsSubProtocol = TestUtils.randomAlphaString(10); String wsSubProtocol = TestUtils.randomAlphaString(10);
boolean is100ContinueHandledAutomatically = rand.nextBoolean();


JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.put("sendBufferSize", sendBufferSize) json.put("sendBufferSize", sendBufferSize)
Expand All @@ -648,7 +658,8 @@ public void testServerOptionsJson() {
.put("acceptBacklog", acceptBacklog) .put("acceptBacklog", acceptBacklog)
.put("compressionSupported", compressionSupported) .put("compressionSupported", compressionSupported)
.put("maxWebsocketFrameSize", maxWebsocketFrameSize) .put("maxWebsocketFrameSize", maxWebsocketFrameSize)
.put("websocketSubProtocols", wsSubProtocol); .put("websocketSubProtocols", wsSubProtocol)
.put("handle100ContinueAutomatically", is100ContinueHandledAutomatically);


HttpServerOptions options = new HttpServerOptions(json); HttpServerOptions options = new HttpServerOptions(json);
assertEquals(sendBufferSize, options.getSendBufferSize()); assertEquals(sendBufferSize, options.getSendBufferSize());
Expand Down Expand Up @@ -677,6 +688,7 @@ public void testServerOptionsJson() {
assertEquals(compressionSupported, options.isCompressionSupported()); assertEquals(compressionSupported, options.isCompressionSupported());
assertEquals(maxWebsocketFrameSize, options.getMaxWebsocketFrameSize()); assertEquals(maxWebsocketFrameSize, options.getMaxWebsocketFrameSize());
assertEquals(wsSubProtocol, options.getWebsocketSubProtocols()); assertEquals(wsSubProtocol, options.getWebsocketSubProtocols());
assertEquals(is100ContinueHandledAutomatically, options.isHandle100ContinueAutomatically());


// Test other keystore/truststore types // Test other keystore/truststore types
json.put("pfxKeyCertOptions", new JsonObject().put("password", ksPassword)) json.put("pfxKeyCertOptions", new JsonObject().put("password", ksPassword))
Expand Down Expand Up @@ -2268,7 +2280,7 @@ public void testSendFileDirectoryWithHandler() throws Exception {
} }


@Test @Test
public void test100ContinueDefault() throws Exception { public void test100ContinueHandledAutomatically() throws Exception {
Buffer toSend = TestUtils.randomBuffer(1000); Buffer toSend = TestUtils.randomBuffer(1000);


server.requestHandler(req -> { server.requestHandler(req -> {
Expand All @@ -2295,10 +2307,15 @@ public void test100ContinueDefault() throws Exception {
} }


@Test @Test
public void test100ContinueHandled() throws Exception { public void test100ContinueHandledManually() throws Exception {

server.close();
server = vertx.createHttpServer(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST));

Buffer toSend = TestUtils.randomBuffer(1000); Buffer toSend = TestUtils.randomBuffer(1000);
server.requestHandler(req -> { server.requestHandler(req -> {
req.response().headers().set("HTTP/1.1", "100 Continue"); assertEquals("100-continue", req.getHeader("expect"));
req.response().writeContinue();
req.bodyHandler(data -> { req.bodyHandler(data -> {
assertEquals(toSend, data); assertEquals(toSend, data);
req.response().end(); req.response().end();
Expand All @@ -2321,6 +2338,35 @@ public void test100ContinueHandled() throws Exception {
await(); await();
} }


@Test
public void test100ContinueRejectedManually() throws Exception {

server.close();
server = vertx.createHttpServer(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST));

server.requestHandler(req -> {
req.response().setStatusCode(405).end();
req.bodyHandler(data -> {
fail("body should not be received");
});
});

server.listen(onSuccess(s -> {
HttpClientRequest req = client.request(HttpMethod.PUT, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {
assertEquals(405, resp.statusCode());
testComplete();
});
req.headers().set("Expect", "100-continue");
req.setChunked(true);
req.continueHandler(v -> {
fail("should not be called");
});
req.sendHead();
}));

await();
}

@Test @Test
public void testClientDrainHandler() { public void testClientDrainHandler() {
pausingServer(s -> { pausingServer(s -> {
Expand Down

0 comments on commit 57a65be

Please sign in to comment.