Gumdrop provides a comprehensive HTTP implementation supporting HTTP/3, HTTP/2, and HTTP/1.x. The HTTP layer serves as the foundation for higher-level services including the servlet container and file server.
Gumdrop supports HTTP/3, HTTP/2, HTTP/1.1, and HTTP/1.0, with automatic version detection and negotiation. Originally built to support HTTP/1.1 as a native protocol, Gumdrop was first adapted to use the SPDY protocol developed by Google, and then, as that was deprecated and consensus established on the newer HTTP/2 protocol, Gumdrop embraced the efficiency and extensibility of the latter. With the standardisation of HTTP/3 over QUIC, Gumdrop now supports the full spectrum of modern HTTP.
HTTP/3 (RFC 9114) runs over QUIC instead of TCP. QUIC provides built-in
TLS 1.3 encryption, multiplexed streams without head-of-line blocking,
and faster connection establishment (often 0-RTT). HTTP/3 is configured
via a separate HTTP3Listener that listens on a UDP port.
See HTTP/3 and QUIC below for details.
Complete HTTP/2 implementation (RFC 7540) with multiplexing and header compression. HTTP/2 can be negotiated via ALPN during TLS handshake or via cleartext upgrade (h2c). See HTTP/2 Support below for details.
Full HTTP/1.1 support including:
Basic HTTP support for legacy clients. Connections are closed after each request/response unless explicitly kept alive.
HTTP/3 is the latest major version of the Hypertext Transfer Protocol. Unlike HTTP/1.1 and HTTP/2, which run over TCP, HTTP/3 uses QUIC (RFC 9000) as its transport layer. QUIC provides:
Gumdrop’s HTTP/3 support is built on the
quiche library via JNI.
The QUIC layer handles connection management, stream multiplexing, and
congestion control natively, while the HTTP/3 framing (HEADERS, DATA,
QPACK) is processed by Gumdrop’s HTTP3ServerHandler
and H3Stream classes.
From the application’s perspective, HTTP/3 is transparent. The same
HTTPRequestHandlerFactory and HTTPRequestHandler
interfaces are used regardless of whether the request arrived over HTTP/3,
HTTP/2, or HTTP/1.1. The only visible difference is the HTTP version
reported by HTTPResponseState.getVersion().
HTTP/3 is configured by adding an HTTP3Listener to an
HTTP service. Because QUIC uses UDP, the HTTP/3 listener is independent
of the TCP-based HTTPListener and can coexist with it:
<service id="web" class="org.bluezoo.gumdrop.servlet.ServletService">
<!-- HTTP/1.1 and HTTP/2 over TCP -->
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="keystore-file">keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
<!-- HTTP/3 over QUIC -->
<listener class="org.bluezoo.gumdrop.http.h3.HTTP3Listener">
<property name="port">443</property>
<property name="cert-file">/etc/gumdrop/cert.pem</property>
<property name="key-file">/etc/gumdrop/key.pem</property>
</listener>
</service>
Note that the TCP and QUIC listeners can share the same port number (443 is conventional) because they use different transport protocols (TCP vs UDP).
QUIC uses BoringSSL (via the quiche native library) rather than the JDK’s JSSE, so HTTP/3 listeners require PEM-encoded certificates and keys rather than a Java keystore:
cert-file – path to the PEM certificate chainkey-file – path to the PEM private key
The TCP-based HTTPListener continues to use the standard Java
keystore properties (keystore-file, keystore-pass,
keystore-format). If you have a PKCS#12 keystore, you can
extract PEM files with OpenSSL:
openssl pkcs12 -in keystore.p12 -nokeys -out cert.pem openssl pkcs12 -in keystore.p12 -nocerts -nodes -out key.pem
When an HTTPService has both TCP and QUIC listeners, it
automatically computes an Alt-Svc response header and injects it
into HTTP/1.1 and HTTP/2 responses served by the TCP listener. This tells
clients that HTTP/3 is available:
alt-svc: h3=":443"
Clients that support HTTP/3 (all major browsers, curl, etc.) will attempt to use QUIC on subsequent requests. If the QUIC connection fails or is not supported by the network, clients fall back to HTTP/2 or HTTP/1.1 transparently.
The server validates incoming requests per RFC 9114:
:method, :scheme, or
:path pseudo-headers are rejected with a 400 response.
CONNECT requests are exempted from the :scheme/:path
requirement per section 4.3.1.Priority
header is passed through to handlers for scheduling decisions. The
client emits a Priority header with the
u= urgency parameter.On the client side:
:status pseudo-header are treated as
malformed and the stream is failed.ok()/error();
the client awaits the final response.port – UDP listening port (default: 443)cert-file – path to PEM certificate chain (required)key-file – path to PEM private key (required)quic-max-idle-timeout – QUIC max idle timeout in ms (RFC 9000 section 18)quic-max-data – connection-level flow control limit in bytesquic-max-stream-data-bidi-local – per-stream flow control for locally-initiated bidi streams (bytes)quic-max-stream-data-bidi-remote – per-stream flow control for remotely-initiated bidi streams (bytes)quic-max-stream-data-uni – per-stream flow control for unidirectional streams (bytes)quic-max-streams-bidi – max concurrent bidirectional streamsquic-max-streams-uni – max concurrent unidirectional streamsGumdrop's HTTP/2 implementation provides all mandatory features of RFC 7540 plus several optional features.
All HTTP/2 frame types are supported:
Gumdrop includes a complete HPACK implementation (RFC 7541) for HTTP/2 header compression:
:method, :path, :status, etc.The HPACK encoder and decoder maintain separate dynamic tables for each direction of communication, with configurable maximum table sizes.
For security-sensitive applications, HTTP/2 frame padding can be enabled to obscure message sizes. Padding adds random-length padding to DATA, HEADERS, and PUSH_PROMISE frames (RFC 7540 Section 6.1).
<service class="org.bluezoo.gumdrop.http.HTTPService">
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="frame-padding">64</property>
</listener>
</service>
Gumdrop provides an event-driven, non-blocking API for building HTTP servers. The API abstracts away HTTP version details (HTTP/3, HTTP/2, and HTTP/1.1), allowing handlers to focus on application logic while the framework handles framing, header compression, flow control, and connection management.
The server handler API consists of three main interfaces:
Request handlers receive events in a well-defined order:
headers() // request headers (:method, :path, etc.) startRequestBody() // if request has a body requestBodyContent() // one or more body chunks endRequestBody() // body complete headers() // trailer headers (if any) requestComplete() // stream closed from client
For requests without a body (GET, HEAD, etc.):
headers() // request headers with END_STREAM requestComplete()
The HTTPResponseState interface provides methods for building
responses:
headers() // response headers (includes :status) startResponseBody() // if response has a body responseBodyContent() // one or more body chunks endResponseBody() // body complete headers() // trailer headers (optional) complete() // finish the response
public class HelloHandler extends DefaultHTTPRequestHandler {
@Override
public void headers(HTTPResponseState state, Headers headers) {
if ("GET".equals(headers.getMethod())) {
Headers response = new Headers();
response.status(HTTPStatus.OK);
response.add("content-type", "text/plain");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(
ByteBuffer.wrap("Hello, World!".getBytes()));
state.endResponseBody();
state.complete();
}
}
}
The factory receives initial headers before the handler is created, enabling routing decisions based on path, method, or other criteria:
public class MyFactory implements HTTPRequestHandlerFactory {
public HTTPRequestHandler createHandler(HTTPResponseState state,
Headers headers) {
String path = headers.getPath();
if (path.startsWith("/api/")) {
return new ApiHandler();
} else if (path.startsWith("/static/")) {
return new StaticFileHandler(documentRoot);
} else {
return new DefaultHandler();
}
}
}
Set the handler factory on the service:
HTTPService service = new HTTPService(); service.setHandlerFactory(new MyFactory()); HTTPListener listener = new HTTPListener(); listener.setPort(8080); service.addListener(listener); service.start();
If a Realm is configured on the service, authentication is
performed automatically before the factory is called. The authenticated
principal is available via HTTPResponseState.getPrincipal().
Alternatively, the factory can perform authentication manually:
public HTTPRequestHandler createHandler(HTTPResponseState state,
Headers headers) {
String auth = headers.getValue("authorization");
if (!isValidAuth(auth)) {
Headers response = new Headers();
response.status(HTTPStatus.UNAUTHORIZED);
response.add("www-authenticate", "Bearer");
state.headers(response);
state.complete();
return null; // no handler needed
}
return new MyHandler();
}
For HTTP/2 connections, handlers can initiate server push to proactively send resources to the client:
@Override
public void headers(HTTPResponseState state, Headers headers) {
// Push a stylesheet that we know the client will need
Headers pushHeaders = new Headers();
pushHeaders.add(":method", "GET");
pushHeaders.add(":path", "/styles/main.css");
pushHeaders.add(":scheme", state.getScheme());
pushHeaders.add(":authority", headers.getValue(":authority"));
if (state.pushPromise(pushHeaders)) {
// Push was accepted; the pushed stream will be processed
// through the factory like any other request
}
// Continue with the normal response...
}
Server push returns false for HTTP/1.x connections or if
the client has disabled push via settings.
The HTTPResponseState provides access to endpoint details:
getEndpoint() - the underlying endpoint (remote/local addresses)getSecurityInfo() - TLS session details (null if not secure)getVersion() - HTTP version (HTTP_3, HTTP_2, HTTP_1_1, HTTP_1_0)getScheme() - URL scheme ("http" or "https")getPrincipal() - authenticated user (if Realm configured)For server-sent events or other streaming scenarios:
public class StreamHandler extends DefaultHTTPRequestHandler {
@Override
public void headers(HTTPResponseState state, Headers headers) {
Headers response = new Headers();
response.status(HTTPStatus.OK);
response.add("content-type", "text/event-stream");
state.headers(response);
state.startResponseBody();
// Keep reference to state for async sends
eventEmitter.subscribe(event -> {
state.responseBodyContent(
ByteBuffer.wrap(event.toSSE().getBytes()));
});
}
}
When a handler produces response data faster than the network can deliver it,
or receives request body data faster than it can process it, backpressure
prevents unbounded buffering. HTTPResponseState provides three
methods that work uniformly across HTTP/1.1, HTTP/2, and HTTP/3:
onWritable(callback) – registers a one-shot
callback invoked when the transport is ready for more response data.
Use this to send body content in chunks without overwhelming the
connection.pauseRequestBody() – stops delivery of
requestBodyContent() events. The transport propagates
backpressure to the client per-stream (HTTP/2 and HTTP/3) or per-connection
(HTTP/1.1).resumeRequestBody() – resumes delivery after a
pause. Any data that arrived while paused is delivered promptly.
// Streaming a large file with write-side backpressure
state.startResponseBody();
sendNextChunk(state, channel);
void sendNextChunk(HTTPResponseState state, ReadableByteChannel src) {
ByteBuffer buf = ByteBuffer.allocate(8192);
int n = src.read(buf);
if (n > 0) {
buf.flip();
state.responseBodyContent(buf);
state.onWritable(() -> sendNextChunk(state, src));
} else {
state.endResponseBody();
state.complete();
}
}
// Throttling request body consumption with read-side backpressure
public void requestBodyContent(HTTPResponseState state, ByteBuffer data) {
if (!processor.ready()) {
state.pauseRequestBody();
processor.onReady(() -> state.resumeRequestBody());
return;
}
processor.consume(data);
}
For HTTP/2 and HTTP/3, backpressure is per-stream: pausing one stream does not affect other streams on the same connection. For HTTP/1.1, it operates at the connection level via TCP flow control.
Use cancel() to abort a stream on error:
@Override
public void requestBodyContent(HTTPResponseState state, ByteBuffer data) {
if (data.remaining() > MAX_BODY_SIZE) {
Headers response = new Headers();
response.status(HTTPStatus.REQUEST_ENTITY_TOO_LARGE);
state.headers(response);
state.complete();
state.cancel(); // abort the stream
}
}
For non-TLS connections, HTTP/2 can be negotiated via the HTTP/1.1 Upgrade mechanism. The client sends:
GET / HTTP/1.1 Host: example.com Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
If the server supports HTTP/2, it responds with 101 Switching Protocols and both sides switch to HTTP/2 framing:
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: h2c
The HTTP2-Settings header contains base64url-encoded SETTINGS
frame payload, allowing the client to communicate initial settings before
the upgrade completes.
Gumdrop provides several mechanisms for managing HTTP/1.1 persistent connections efficiently (RFC 9112 section 9):
The idle-timeout-ms property configures how long a persistent
connection can remain idle before the server closes it (RFC 9112 section 9.8).
The timer resets each time data is received. A value of 0 disables the timeout.
The max-requests-per-connection property limits how many requests
are served on a single persistent connection. When the limit is reached, the
server adds a Connection: close header to the response, signalling
the client to close the connection after the current exchange (RFC 9112
section 9.6). A value of 0 means unlimited.
When a client sends an Expect: 100-continue header, the server
sends an interim 100 Continue response before the client transmits
the request body. This avoids wasting bandwidth on large uploads the server
would ultimately reject (RFC 9110 section 10.1.1).
The server handles OPTIONS * requests directly, returning a
200 OK with an Allow header listing the supported
methods (RFC 9110 section 9.3.7). The TRACE method is supported
but disabled by default for security; set trace-method-enabled to
true to enable it (RFC 9110 section 9.3.8).
HTTP services (ServletService, WebDAVService, etc.) support the following service-level properties:
add-security-headers – when true (default), adds
X-Frame-Options: SAMEORIGIN and
X-Content-Type-Options: nosniff to responses unless the
application has already set them. Set to false to disable.
Example:
<service id="web" class="org.bluezoo.gumdrop.servlet.ServletService"> <property name="add-security-headers" value="false"/> ... </service>
HTTPListener is the TCP listener for HTTP/1.1 and HTTP/2. It
inherits standard listener properties and adds HTTP-specific options:
port – listening port (default: 80, or 443 for secure)secure – enable implicit TLSkeystore-file – path to keystore for TLSkeystore-pass – keystore passwordkeystore-format – keystore format (default: PKCS12)need-client-auth – require client certificatesframe-padding – HTTP/2 frame padding (0–255 bytes)max-concurrent-streams – HTTP/2 maximum concurrent streams
per connection (default: 100)idle-timeout-ms – close connections that receive no data for
this many milliseconds; 0 to disable (default: 0). See
Connection Management.max-requests-per-connection – send Connection:
close after this many requests on a persistent connection; 0 for unlimited
(default: 0)trace-method-enabled – enable the HTTP TRACE method
(default: false, disabled for security)ping-interval-ms – interval in milliseconds for server-initiated
HTTP/2 PING keep-alive frames; 0 to disable (default: 0,
RFC 9113 section 6.7)authentication-provider – HTTP authentication configuration
HTTP3Listener is the QUIC listener for HTTP/3. Because QUIC
uses BoringSSL rather than the JDK TLS stack, certificate configuration
uses PEM files instead of a keystore:
port – UDP listening port (default: 443)cert-file – path to PEM certificate chain (required)key-file – path to PEM private key (required)quic-max-idle-timeout – QUIC idle timeout in ms (RFC 9000 section 18)quic-max-data – connection-level flow control limit (bytes)quic-max-stream-data-bidi-local – stream flow control, local bidi (bytes)quic-max-stream-data-bidi-remote – stream flow control, remote bidi (bytes)quic-max-stream-data-uni – stream flow control, unidirectional (bytes)quic-max-streams-bidi – max concurrent bidi streamsquic-max-streams-uni – max concurrent uni streams
A typical production configuration exposes both TCP (HTTP/1.1, HTTP/2) and
QUIC (HTTP/3) on the same port number. The TCP listener handles traditional
connections while the QUIC listener handles HTTP/3 connections on UDP. The
service automatically sets the Alt-Svc header on TCP responses
so that clients can discover and migrate to HTTP/3:
<service id="web" class="org.bluezoo.gumdrop.servlet.ServletService">
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="keystore-file">keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
<listener class="org.bluezoo.gumdrop.http.h3.HTTP3Listener">
<property name="port">443</property>
<property name="cert-file">cert.pem</property>
<property name="key-file">key.pem</property>
</listener>
</service>
Gumdrop supports HTTP authentication methods:
Authentication is configured via an HTTPAuthenticationProvider
which can be shared across multiple services or customised per service. It is
integrated into the centralised org.bluezoo.gumdrop.auth.Realm-based
facility for managing authentication and authorisation across all services
running in the framework.
Gumdrop includes a non-blocking HTTP client that shares the same event-driven architecture as the server. The client supports HTTP/3, HTTP/2, and HTTP/1.1 with automatic protocol negotiation.
When an HTTP client connection is initiated by a Gumdrop server connection, it can be assigned to the same Gumdrop worker SelectorLoop as is used by used by the server connection. This provides several advantages:
This makes the HTTP client particularly well-suited for server-side use cases such as proxying, service-to-service communication, and telemetry export where the client operates within the context of an existing Gumdrop server.
The existing OpenTelemetry implementation within Gumdrop uses this exact mechanism for efficiency, and server processes that need to call REST endpoints, for instance, can easily benefit from high performance asynchronous non-blocking connections to them via this feature.
SETTINGS_MAX_CONCURRENT_STREAMS; excess requests are queued and
dispatched as capacity becomes available (RFC 9113 section 5.1.2)setIdleTimeoutMs(); on expiry, sends GOAWAY with NO_ERROR
(RFC 9113 section 9.1)Connection: close in responses and stops reusing the
connection (RFC 9112 section 9.6)
setConnectionPool(ClientEndpointPool) enables endpoint reuse with
SelectorLoop affinity. Pool targets are keyed by (host, port, secure, loop).
When a pooled connection is available, the client skips the connect/TLS
handshake and reuses the existing endpoint.
setTrustManager(X509TrustManager) configures a custom trust manager
for outbound TLS. Use PinnedCertTrustManager for SHA-256
fingerprint pinning.
setAltSvcEnabled(true) enables parsing of Alt-Svc
response headers. The client can automatically upgrade to HTTP/3 when the server
advertises QUIC availability.
HTTPClient client = new HTTPClient("example.com", 443);
client.setSecure(true);
client.connect(new HTTPClientHandler() {
public void onConnected(Endpoint endpoint) {
HTTPRequest request = client.get("/api/data");
request.header("Accept", "application/json");
request.send(new DefaultHTTPResponseHandler() {
public void ok(HTTPResponse response) {
System.out.println("Status: " + response.getStatus());
}
public void responseBodyContent(ByteBuffer data) {
// Process response body
}
public void close() {
System.out.println("Response complete");
client.close();
}
public void failed(Exception ex) {
ex.printStackTrace();
}
});
}
public void onSecurityEstablished(SecurityInfo info) { }
public void onError(Exception cause) { cause.printStackTrace(); }
public void onDisconnected() { }
});
To use HTTP/3, enable it on the client before connecting. The client uses QUIC with ALPN “h3” instead of TCP:
HTTPClient client = new HTTPClient("example.com", 443);
client.setH3Enabled(true);
client.connect(new HTTPClientHandler() {
public void onConnected(Endpoint endpoint) {
HTTPRequest request = client.get("/api/data");
request.header("Accept", "application/json");
request.send(responseHandler);
}
public void onSecurityEstablished(SecurityInfo info) { }
public void onError(Exception cause) { cause.printStackTrace(); }
public void onDisconnected() { }
});
The request and response APIs are identical regardless of the underlying protocol version. The only difference is how the client is configured before connecting.
All handler callbacks are invoked on the client connection’s assigned worker thread. Long-running operations within handlers should be offloaded to avoid blocking the SelectorLoop.
← Back to Main Page | File Server & WebDAV | WebSocket Service & Client | SMTP Server & Client | IMAP Server | POP3 Server | DNS Server | Telemetry
Gumdrop HTTP Server