Skip to content

Commit 5acdb87

Browse files
committed
* Bug fixes
* Doc improvements * Removed setLevel from logger since SLF4J doesn't support that
1 parent d588ee2 commit 5acdb87

9 files changed

Lines changed: 96 additions & 30 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
**NOTE:** This project is in progress.
44

5-
The goal of this project is to build a full-featured HTTP server and client in plain Java without the use of any libraries. The general
6-
requirements and roadmap are as follows:
5+
The goal of this project is to build a full-featured HTTP server and client in plain Java without the use of any libraries. The client and server will use non-blocking NIO in order to provide the highest performance possible.
6+
7+
The general requirements and roadmap are as follows:
78

89
### Server tasks
910

@@ -18,13 +19,15 @@ requirements and roadmap are as follows:
1819
* [x] Clean up HTTPRequest
1920
* [x] Support form data
2021
* [x] Support multipart form data
22+
* [x] Support TLS
2123
* [ ] Support trailers
2224
* [ ] Support HTTP 2
2325

2426
### Client tasks
2527

2628
* [ ] Basic HTTP 1.1
2729
* [ ] Support Keep-Alive
30+
* [ ] Support TLS
2831
* [ ] Support HTTP 2
2932
* [ ] Support Expect-Continue 100
3033
* [ ] Support chunked request and response
@@ -37,6 +40,7 @@ requirements and roadmap are as follows:
3740
Creating a server is simple:
3841

3942
```java
43+
import io.fusionauth.http.server.HTTPListenerConfiguration;
4044
import io.fusionauth.http.server.HTTPServer;
4145
import io.fusionauth.http.server.HTTPHandler;
4246

@@ -46,7 +50,7 @@ public class Example {
4650
// Handler code goes here
4751
};
4852

49-
HTTPServer server = new HTTPServer().withHandler(handler).withPort(4242);
53+
HTTPServer server = new HTTPServer().withHandler(handler).withListener(new HTTPListenerConfiguration(4242));
5054
server.start();
5155
// Use server
5256
server.close();
@@ -57,6 +61,7 @@ public class Example {
5761
Since the `HTTPServer` class implements `java.io.Closeable`, you can also use a try-resource block like this:
5862

5963
```java
64+
import io.fusionauth.http.server.HTTPListenerConfiguration;
6065
import io.fusionauth.http.server.HTTPServer;
6166
import io.fusionauth.http.server.HTTPHandler;
6267

@@ -66,7 +71,7 @@ public class Example {
6671
// Handler code goes here
6772
};
6873

69-
try (HTTPServer server = new HTTPServer().withHandler(handler).withPort(4242)) {
74+
try (HTTPServer server = new HTTPServer().withHandler(handler).withListener(new HTTPListenerConfiguration(4242))) {
7075
server.start();
7176
// When this block exits, the server will be shutdown
7277
}
@@ -79,6 +84,7 @@ You can also set various options on the server using the `with` methods on the c
7984
```java
8085
import java.time.Duration;
8186

87+
import io.fusionauth.http.server.HTTPListenerConfiguration;
8288
import io.fusionauth.http.server.HTTPServer;
8389
import io.fusionauth.http.server.HTTPHandler;
8490

@@ -91,7 +97,7 @@ public class Example {
9197
HTTPServer server = new HTTPServer().withHandler(handler)
9298
.withNumberOfWorkerThreads(42)
9399
.withShutdownDuration(Duration.ofSeconds(10L))
94-
.withPort(4242);
100+
.withListener(new HTTPListenerConfiguration(4242));
95101
server.start();
96102
// Use server
97103
server.close();

java-http.iml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,6 @@
241241
</SOURCES>
242242
</library>
243243
</orderEntry>
244+
<orderEntry type="library" name="Groovy 2.4" level="application" />
244245
</component>
245246
</module>

src/main/java/io/fusionauth/http/log/Logger.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,6 @@ public interface Logger {
8080
*/
8181
boolean isDebuggable();
8282

83-
/**
84-
* Sets the level for this logger.
85-
*
86-
* @param level The level.
87-
*/
88-
void setLevel(Level level);
89-
9083
/**
9184
* Logs a trace message with values.
9285
*

src/main/java/io/fusionauth/http/log/SystemOutLogger.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ public boolean isDebuggable() {
8686
return level.ordinal() <= Level.Debug.ordinal();
8787
}
8888

89-
@Override
90-
public void setLevel(Level level) {
91-
this.level = level;
92-
}
93-
9489
@Override
9590
public void trace(String message) {
9691
if (level.ordinal() == Level.Trace.ordinal()) {

src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ public RequestState processPreambleBytes(ByteBuffer buffer) {
112112

113113
int size = Math.max(buffer.remaining(), bufferSize);
114114
if (contentLength != null) {
115-
logger.info("Handling body using Content-Length header");
115+
logger.debug("Handling body using Content-Length header");
116116
bodyProcessor = new ContentLengthBodyProcessor(size, contentLength);
117117
} else {
118-
logger.info("Handling body using Chunked data");
118+
logger.debug("Handling body using Chunked data");
119119
bodyProcessor = new ChunkedBodyProcessor(size);
120120
configuration.getInstrumenter().chunkedRequest();
121121
}

src/main/java/io/fusionauth/http/server/HTTPServerThread.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ public HTTPServerThread(HTTPServerConfiguration configuration, HTTPListenerConfi
8181

8282
@Override
8383
public void close() {
84+
// Close all the client connections as cleanly as possible
85+
var keys = selector.keys();
86+
for (SelectionKey key : keys) {
87+
cancelAndCloseKey(key);
88+
}
89+
8490
try {
8591
selector.close();
8692
} catch (Throwable t) {
@@ -172,14 +178,26 @@ private void accept(SelectionKey key) throws GeneralSecurityException, IOExcepti
172178
client.configureBlocking(false);
173179
client.register(key.selector(), tlsProcessor.initialKeyOps(), tlsProcessor);
174180

181+
if (logger.isDebuggable()) {
182+
try {
183+
logger.debug("Accepted connection from client [{}]", client.getRemoteAddress().toString());
184+
} catch (IOException e) {
185+
/// Ignore because we are just debugging
186+
}
187+
}
188+
175189
if (instrumenter != null) {
176190
instrumenter.acceptedConnection();
177191
}
178192
}
179193

180194
private void cancelAndCloseKey(SelectionKey key) {
181195
if (key != null) {
182-
try (var ignore = key.channel()) {
196+
try (var client = key.channel()) {
197+
if (logger.isDebuggable() && client instanceof SocketChannel socketChannel) {
198+
logger.debug("Closing connection to client [{}]", socketChannel.getRemoteAddress().toString());
199+
}
200+
183201
key.cancel();
184202
} catch (Throwable t) {
185203
logger.error("An exception was thrown while trying to cancel a SelectionKey and close a channel with a client due to an exception being thrown for that specific client. Enable debug logging to see the error", t);
@@ -197,10 +215,12 @@ private void cleanup() {
197215
.filter(key -> ((HTTPProcessor) key.attachment()).lastUsed() < now - clientTimeout.toMillis())
198216
.forEach(key -> {
199217
var client = (SocketChannel) key.channel();
200-
try {
201-
logger.debug("Closing client connection [{}] due to inactivity", client.getRemoteAddress().toString());
202-
} catch (IOException e) {
203-
// Ignore because we are just debugging
218+
if (logger.isDebuggable()) {
219+
try {
220+
logger.debug("Closing client connection [{}] due to inactivity", client.getRemoteAddress().toString());
221+
} catch (IOException e) {
222+
// Ignore because we are just debugging
223+
}
204224
}
205225

206226
try {

src/test/java/io/fusionauth/http/ChunkedTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import com.inversoft.rest.TextResponseHandler;
3636
import io.fusionauth.http.HTTPValues.Headers;
3737
import io.fusionauth.http.log.Level;
38-
import io.fusionauth.http.log.SystemOutLoggerFactory;
38+
import io.fusionauth.http.log.SystemOutLogger;
3939
import io.fusionauth.http.server.CountingInstrumenter;
4040
import io.fusionauth.http.server.HTTPHandler;
4141
import io.fusionauth.http.server.HTTPServer;
@@ -57,7 +57,7 @@ public class ChunkedTest extends BaseTest {
5757
static {
5858
System.setProperty("sun.net.http.retryPost", "false");
5959
System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
60-
SystemOutLoggerFactory.FACTORY.getLogger(ChunkedTest.class).setLevel(Level.Info);
60+
SystemOutLogger.level = Level.Info;
6161
}
6262

6363
@Test(dataProvider = "schemes")

src/test/java/io/fusionauth/http/CoreTest.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
import java.util.List;
3030
import java.util.Locale;
3131

32+
import com.inversoft.rest.RESTClient;
33+
import com.inversoft.rest.TextResponseHandler;
3234
import io.fusionauth.http.HTTPValues.Connections;
3335
import io.fusionauth.http.HTTPValues.Headers;
3436
import io.fusionauth.http.log.Level;
35-
import io.fusionauth.http.log.SystemOutLoggerFactory;
37+
import io.fusionauth.http.log.SystemOutLogger;
3638
import io.fusionauth.http.server.CountingInstrumenter;
3739
import io.fusionauth.http.server.HTTPHandler;
3840
import io.fusionauth.http.server.HTTPListenerConfiguration;
@@ -55,7 +57,7 @@ public class CoreTest extends BaseTest {
5557
static {
5658
System.setProperty("sun.net.http.retryPost", "false");
5759
System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
58-
SystemOutLoggerFactory.FACTORY.getLogger(CoreTest.class).setLevel(Level.Info);
60+
SystemOutLogger.level = Level.Info;
5961
}
6062

6163

@@ -252,6 +254,55 @@ public void performanceNoKeepAlive(String scheme) throws Exception {
252254
assertEquals(instrumenter.getConnections(), iterations);
253255
}
254256

257+
/**
258+
* This test uses Restify in order to leverage the URLConnection implementation of the JDK. That implementation is not smart enough to
259+
* realize that a socket in the connection pool that was using Keep-Alives with the server is potentially dead. Since we are shutting down
260+
* the server and doing another request, this ensures that the server itself is sending a socket close signal back to the URLConnection
261+
* and removing the socket form the connection pool.
262+
*/
263+
@Test(dataProvider = "schemes")
264+
public void restifyMultipleServers(String scheme) {
265+
HTTPHandler handler = (req, res) -> {
266+
res.setHeader(Headers.ContentType, "text/plain");
267+
res.setHeader("Content-Length", "16");
268+
res.setStatus(200);
269+
270+
try {
271+
OutputStream outputStream = res.getOutputStream();
272+
outputStream.write(ExpectedResponse.getBytes());
273+
outputStream.close();
274+
} catch (IOException e) {
275+
throw new RuntimeException(e);
276+
}
277+
};
278+
279+
try (HTTPServer ignore = makeServer(scheme, handler).start()) {
280+
URI uri = makeURI(scheme, "");
281+
var response = new RESTClient<>(String.class, String.class).url(uri.toString())
282+
.connectTimeout(600_000)
283+
.readTimeout(600_000)
284+
.get()
285+
.successResponseHandler(new TextResponseHandler())
286+
.errorResponseHandler(new TextResponseHandler())
287+
.go();
288+
assertEquals(response.status, 200);
289+
assertEquals(response.successResponse, ExpectedResponse);
290+
}
291+
292+
try (HTTPServer ignore = makeServer(scheme, handler).start()) {
293+
URI uri = makeURI(scheme, "");
294+
var response = new RESTClient<>(String.class, String.class).url(uri.toString())
295+
.connectTimeout(600_000)
296+
.readTimeout(600_000)
297+
.get()
298+
.successResponseHandler(new TextResponseHandler())
299+
.errorResponseHandler(new TextResponseHandler())
300+
.go();
301+
assertEquals(response.status, 200);
302+
assertEquals(response.successResponse, ExpectedResponse);
303+
}
304+
}
305+
255306
@Test(dataProvider = "schemes")
256307
public void simpleGet(String scheme) throws Exception {
257308
HTTPHandler handler = (req, res) -> {

src/test/java/io/fusionauth/http/ExpectTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import io.fusionauth.http.HTTPValues.Headers;
2929
import io.fusionauth.http.log.Level;
30-
import io.fusionauth.http.log.SystemOutLoggerFactory;
30+
import io.fusionauth.http.log.SystemOutLogger;
3131
import io.fusionauth.http.server.CountingInstrumenter;
3232
import io.fusionauth.http.server.ExpectValidator;
3333
import io.fusionauth.http.server.HTTPHandler;
@@ -50,7 +50,7 @@ public class ExpectTest extends BaseTest {
5050
static {
5151
System.setProperty("sun.net.http.retryPost", "false");
5252
System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
53-
SystemOutLoggerFactory.FACTORY.getLogger(ExpectTest.class).setLevel(Level.Info);
53+
SystemOutLogger.level = Level.Info;
5454
}
5555

5656
@Test(dataProvider = "schemes")

0 commit comments

Comments
 (0)