Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
43e9767
update some logging, add comments
robotdan May 13, 2025
55a2aba
working
robotdan May 13, 2025
5a2c059
working
robotdan May 13, 2025
981f84e
working
robotdan May 13, 2025
22dc35c
working
robotdan May 14, 2025
f128fbc
Add logging, and tests
robotdan May 15, 2025
dfdc104
Tests
robotdan May 15, 2025
8c8c3ec
Tests
robotdan May 15, 2025
6836604
Tests
robotdan May 15, 2025
c709bbb
Tests
robotdan May 15, 2025
309e8c3
Working
robotdan May 15, 2025
32746a9
Tests
robotdan May 16, 2025
8356101
Tests
robotdan May 16, 2025
af92fe8
Tests
robotdan May 16, 2025
8ae5cf1
copyright
robotdan May 16, 2025
2f7c476
Working
robotdan May 24, 2025
2376537
Working
robotdan May 24, 2025
b23099d
Working
robotdan May 28, 2025
d425944
Working
robotdan May 28, 2025
78e4c2e
Working
robotdan May 28, 2025
daaa59c
Working
robotdan May 28, 2025
2e0aa12
Working
robotdan May 28, 2025
409b40f
Working
robotdan May 28, 2025
55f14b3
Working
robotdan May 28, 2025
3d322ac
Working
robotdan May 28, 2025
9dc897f
Working
robotdan May 28, 2025
f7148a0
Working
robotdan May 30, 2025
a468f6b
Working
robotdan May 30, 2025
713ccb9
Working
robotdan May 30, 2025
91db717
Tests
robotdan May 30, 2025
e659c0e
Working
robotdan May 30, 2025
a0691f0
Working
robotdan May 30, 2025
797ed9c
Get Tomcat setup
robotdan May 30, 2025
598729b
README
robotdan May 30, 2025
31a8e6f
review edits
robotdan May 30, 2025
993eb99
build updates
robotdan May 30, 2025
cd8c7b7
Working
robotdan Jun 3, 2025
3c45a5c
Working
robotdan Jun 3, 2025
130b48c
Working
robotdan Jun 3, 2025
8a22c0c
Working
robotdan Jun 3, 2025
d69682c
Working
robotdan Jun 4, 2025
251c938
Working
robotdan Jun 4, 2025
d2950be
Working
robotdan Jun 4, 2025
5d312dd
Working
robotdan Jun 4, 2025
a56fce4
Working
robotdan Jun 4, 2025
2ec79ed
Working
robotdan Jun 4, 2025
4f0adc2
Working
robotdan Jun 5, 2025
6a73fa4
Add option to keep additional attributes on cookies
robotdan Jun 5, 2025
cff1c68
Working
robotdan Jun 6, 2025
8519136
Working
robotdan Jun 7, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Working
  • Loading branch information
robotdan committed Jun 5, 2025
commit 4f0adc20b0f5d4ca20620fc980fd1d6dedcafe15
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For more information about Project Loom and virtual threads, here is a good arti
- Very fast
- Easy to make a simple web server like you can in Node.js
- No dependencies
- To not boil the ocean. This is a purpose built HTTP server that probably won't do everything.
- To not boil the ocean. This is a purpose built HTTP server that probably won't do everything.

## Installation

Expand Down Expand Up @@ -187,7 +187,7 @@ Load test last performed May 30, 2025. Using the [fusionauth-load-test](https://

### Running load tests

Start the HTTP server to test.
Start the HTTP server to test.

#### java-http

Expand All @@ -201,6 +201,7 @@ sb clean start
#### Apache Tomcat

Start the HTTP server. Run the following commands from the `java-http` repo.

```bash
cd load-tests/tomcat
sb clean start
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# LinuxConfig - Ultimate Web Server Benchmark

- https://linuxconfig.org/ultimate-web-server-benchmark-apache-nginx-litespeed-openlitespeed-caddy-lighttpd-compared

I asked about the `-k` parameter for the `ab` usages to ensure I understand how HTTP Keep Alive was being used in the RPS measurements.
Expand All @@ -10,10 +11,9 @@ I asked about the `-k` parameter for the `ab` usages to ensure I understand how

> Static file handling is a fundamental task for any web server. This test measures how efficiently each server serves a simple HTML page under concurrent requests. A web server optimized for static content should deliver high requests per second (RPS) with minimal latency and resource usage. This test is crucial for scenarios where websites serve mostly cached, pre-generated pages, such as blogs, documentation sites, and content delivery networks (CDNs).


### Command
The test was conducted using Apache Benchmark (`ab`) with the following command:

The test was conducted using Apache Benchmark (`ab`) with the following command:

```sh
ab -n 100000 -c 100 http://localhost:8080/
Expand All @@ -23,10 +23,10 @@ ab -n 100000 -c 100 http://localhost:8080/
- -c 50 → Number of concurrent users (50)
- http://localhost:8080/ → URL of the static test page


### Results

Test run on:

- MacBook Pro 16-inch 2021, Apple M1 Max, 64 GB

| Server | Requests per second (RPS) | Latency (ms) | Normalized Performance (%) |
Expand All @@ -53,6 +53,7 @@ For fun, here is the same test using the `-k` parameter. In practice this may be
> Serving large files efficiently is critical for websites that deliver downloads, streaming media, or large assets such as high-resolution images or software packages. This test evaluates how well each web server handles the transfer of a 10MB file under concurrent requests. A well-optimized server should maintain high transfer rates while keeping CPU and memory usage minimal.

### Command

The test was conducted using Apache Benchmark (`ab`) with the following command:

```sh
Expand All @@ -66,6 +67,7 @@ ab -n 500 -c 10 http://localhost:8080/file?size=10485760
### Results

Test run on:

- MacBook Pro 16-inch 2021, Apple M1 Max, 64 GB

| Server | Requests per second (RPS) | Latency (ms) | Transfer Rate (MB/sec) | Normalized Performance (%) |
Expand All @@ -77,7 +79,6 @@ Note to calculate the Transfer Rate in MB/sec, I am taking the `ab` result of `K

Same test with `-k` which assumes we are using an HTTP proxy with Keep Alive.


| Server | Requests per second (RPS) | Latency (ms) | Transfer Rate (MB/sec) | Normalized Performance (%) |
|---------------|---------------------------|--------------|------------------------|----------------------------|
| java-http | 682 | 14.647 | 6,632 | |
Expand All @@ -92,6 +93,7 @@ It is unexpected that Apache Tomcat would perform worse with `-k` enabled. It is
> Web servers must efficiently handle high traffic volumes, especially during peak loads. This test measures how well each server performs when faced with 1,000 simultaneous users making requests to a simple HTML page. A well-optimized server should maintain a high request rate with minimal latency and avoid excessive CPU and memory consumption. This test is crucial for sites experiencing traffic spikes, such as e-commerce platforms, news websites, and online services.

### Command

The test was conducted using Apache Benchmark (`ab`) with the following command:

```sh
Expand Down
2 changes: 0 additions & 2 deletions load-tests/self/build.savant
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import java.nio.file.Paths

/*
* Copyright (c) 2022-2025, FusionAuth, All Rights Reserved
*
Expand Down
2 changes: 1 addition & 1 deletion load-tests/tomcat/src/main/tomcat/conf/server.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<Host name="localhost" autoDeploy="false" unpackWARs="false">
<Context path="" docBase="${catalina.base}/../web">
<Manager pathname=""/>
<Resources allowLinking="true" />
<Resources allowLinking="true"/>
</Context>
</Host>
</Engine>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,4 @@
</build>
</profile>
</profiles>
</project>
</project>
16 changes: 8 additions & 8 deletions src/main/java/io/fusionauth/http/Cookie.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2022, FusionAuth, All Rights Reserved
* Copyright (c) 2021-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -281,13 +281,13 @@ public boolean equals(Object o) {
return false;
}
return httpOnly == cookie.httpOnly &&
secure == cookie.secure &&
Objects.equals(domain, cookie.domain) &&
Objects.equals(expires, cookie.expires) &&
Objects.equals(maxAge, cookie.maxAge) &&
Objects.equals(name, cookie.name) &&
Objects.equals(path, cookie.path) &&
Objects.equals(value, cookie.value);
secure == cookie.secure &&
Objects.equals(domain, cookie.domain) &&
Objects.equals(expires, cookie.expires) &&
Objects.equals(maxAge, cookie.maxAge) &&
Objects.equals(name, cookie.name) &&
Objects.equals(path, cookie.path) &&
Objects.equals(value, cookie.value);
}

public String getDomain() {
Expand Down
26 changes: 13 additions & 13 deletions src/main/java/io/fusionauth/http/HTTPMethod.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2022, FusionAuth, All Rights Reserved
* Copyright (c) 2021-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,6 +48,18 @@ public class HTTPMethod {

private final String name;

static {
StandardMethods.put(CONNECT.name(), CONNECT);
StandardMethods.put(DELETE.name(), DELETE);
StandardMethods.put(GET.name(), GET);
StandardMethods.put(HEAD.name(), HEAD);
StandardMethods.put(OPTIONS.name(), OPTIONS);
StandardMethods.put(PATCH.name(), PATCH);
StandardMethods.put(POST.name(), POST);
StandardMethods.put(PUT.name(), PUT);
StandardMethods.put(TRACE.name(), TRACE);
}

private HTTPMethod(String name) {
Objects.requireNonNull(name);
this.name = name.toUpperCase(Locale.ROOT);
Expand Down Expand Up @@ -95,16 +107,4 @@ public String name() {
public String toString() {
return name;
}

static {
StandardMethods.put(CONNECT.name(), CONNECT);
StandardMethods.put(DELETE.name(), DELETE);
StandardMethods.put(GET.name(), GET);
StandardMethods.put(HEAD.name(), HEAD);
StandardMethods.put(OPTIONS.name(), OPTIONS);
StandardMethods.put(PATCH.name(), PATCH);
StandardMethods.put(POST.name(), POST);
StandardMethods.put(PUT.name(), PUT);
StandardMethods.put(TRACE.name(), TRACE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.http;
package io.fusionauth.http.io;

/**
* Exception that is thrown if a Chunked request or response is invalid.
Expand Down
28 changes: 4 additions & 24 deletions src/main/java/io/fusionauth/http/io/ChunkedInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import java.io.IOException;
import java.io.InputStream;

import io.fusionauth.http.ChunkException;
import io.fusionauth.http.ParseException;
import io.fusionauth.http.util.HTTPTools;
import static io.fusionauth.http.util.HTTPTools.makeParseException;
Expand All @@ -33,7 +32,7 @@ public class ChunkedInputStream extends InputStream {

private final byte[] buffer;

private final InputStream delegate;
private final PushbackInputStream delegate;

private final StringBuilder headerSizeHex = new StringBuilder();

Expand All @@ -47,7 +46,7 @@ public class ChunkedInputStream extends InputStream {

private ChunkedBodyState state = ChunkedBodyState.ChunkSize;

public ChunkedInputStream(InputStream delegate, int bufferSize) {
public ChunkedInputStream(PushbackInputStream delegate, int bufferSize) {
this.delegate = delegate;
this.buffer = new byte[bufferSize];
}
Expand Down Expand Up @@ -75,17 +74,7 @@ private int processChunk(byte[] destination, int offset, int length) throws IOEx
// We need to push back any remaining bytes to the InputStream since we may have read more bytes than we needed.
int leftOver = bufferLength - bufferIndex;
if (leftOver > 0) {
// TODO : Daniel : Review : This doesn't seem like a good idea. It will fail silently, but this is required.
// Discuss with Brian.
// .
// Options:
// - Leave as is
// - Throw a OverReadException that is caught by the HTTPInputStream which has a typed ref to PushbackInputStream and handled.
// - Keep a typed ref for PushbackInputStream, but this messes up the 'commit' path in HTTPInputStream that swaps the pointer.
// So we could re-work how we handle a chunked request body - instead of swapping out pointers we do something else?
if (delegate instanceof PushbackInputStream pis) {
pis.push(buffer, bufferIndex, leftOver);
}
delegate.push(buffer, bufferIndex, leftOver);
}

return -1;
Expand Down Expand Up @@ -114,16 +103,7 @@ private int processChunk(byte[] destination, int offset, int length) throws IOEx
if (nextState == ChunkedBodyState.Complete) {
state = nextState;
bufferIndex++;
// We need to push back any remaining bytes to the InputStream since we may have read more bytes than we needed.
int leftOver = bufferLength - bufferIndex;
if (leftOver > 0) {
// TODO : Daniel : Review : This doesn't seem like a good idea. It will fail silently, but this is required.
// Discuss with Brian.
if (delegate instanceof PushbackInputStream pis) {
pis.push(buffer, bufferIndex, leftOver);
}
}
return -1;
continue;
}

// Record the size hex digit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public FastByteArrayOutputStream(int size, int growthRate) {
this.growthRate = growthRate;
}

/**
* @return The byte array (directly without copying).
*/
public byte[] bytes() {
return buffer;
}

@Override
public void close() {
}
Expand All @@ -60,13 +67,6 @@ public int size() {
return count;
}

/**
* @return The byte array (directly without copying).
*/
public byte[] bytes() {
return buffer;
}

@Override
public void write(int b) {
if (count + 1 >= buffer.length) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/fusionauth/http/io/MultipartStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
import io.fusionauth.http.HTTPValues.DispositionParameters;
import io.fusionauth.http.HTTPValues.Headers;
import io.fusionauth.http.ParseException;
import io.fusionauth.http.util.RequestPreambleState;
import io.fusionauth.http.util.HTTPTools;
import io.fusionauth.http.util.HTTPTools.HeaderValue;
import io.fusionauth.http.util.RequestPreambleState;

/**
* Handles the multipart body encoding and file uploads.
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/io/fusionauth/http/io/PushbackInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@
public class PushbackInputStream extends InputStream {
private final byte[] b1 = new byte[1];

private final InputStream delegate;

private byte[] buffer;

private int bufferEndPosition;

private int bufferPosition;

private InputStream delegate;
public PushbackInputStream(InputStream delegate) {
this.delegate = delegate;
}

public int getAvailableBufferedBytesRemaining() {
return buffer != null ? (bufferEndPosition - bufferPosition) : 0;
Expand Down Expand Up @@ -75,6 +79,8 @@ public int read(byte[] b, int off, int len) throws IOException {
// - So I think we have to return, and allow the caller to decide if they want to read more bytes based upon
// the contents of the bytes we return.
// TODO : Daniel : Review the above statement.
// ...
// TODO : Put back the code... we want to continue reading past the buffer here.
return read;
}

Expand All @@ -90,8 +96,4 @@ public int read() throws IOException {

return b1[0] & 0xFF;
}

public void setDelegate(InputStream delegate) {
this.delegate = delegate;
}
}
8 changes: 4 additions & 4 deletions src/main/java/io/fusionauth/http/log/FileLoggerFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public class FileLoggerFactory implements LoggerFactory {

private static FileLogger logger;

public static void setLogger(FileLogger logger) {
FileLoggerFactory.logger = logger;
}

@Override
public Logger getLogger(Class<?> klass) {
return logger;
}

public static void setLogger(FileLogger logger) {
FileLoggerFactory.logger = logger;
}
}
Loading