All middleware implement the Middleware functional interface:
@FunctionalInterface
public interface Middleware {
HttpResponse run(HttpRequest request, MiddlewareChain chain);
}They are registered via server.use(middleware) and executed in registration order.
File: miniJWS-core/src/main/java/.../middleware/AccessLogMiddleware.java
Apache Common Log Format logging with asynchronous I/O.
Request Thread Worker Thread (daemon)
│ │
├── formatLogLine() │
├── queue.offer(line) ──────────►├── queue.take() (blocks)
│ ├── writer.println(line)
│ ├── writer.flush()
│ └── loop
└── return response
| Constructor | Output Target |
|---|---|
AccessLogMiddleware() |
System.out (wrapped in PrintWriter) |
AccessLogMiddleware(String filePath) |
File (append, UTF-8) |
AccessLogMiddleware(PrintWriter writer) |
Custom writer |
127.0.0.1 - - [13/Jun/2026:14:30:00 +0000] "GET /hello HTTP/1.1" 200 13 (2ms)
Fields: remote IP (respects X-Forwarded-For), timestamp, request line, status code, body size, elapsed ms.
BlockingQueue<String>(capacity 16_384) decouples request handling from log I/Oqueue.offer()never blocks (returns false if full, line is dropped)- Worker is a daemon thread (doesn't prevent JVM exit)
- Shutdown hook drains remaining entries with
flushRemaining()
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
worker.interrupt();
worker.join(2_000); // wait up to 2s for flush
flushRemaining(writer);
}));File: miniJWS-core/src/main/java/.../middleware/CorsMiddleware.java
CORS (Cross-Origin Resource Sharing) implementation.
| Method | Default | Description |
|---|---|---|
allowOrigin(String) |
* |
Allowed origin |
allowMethods(String...) |
GET, POST, PUT, DELETE, OPTIONS, PATCH | Allowed methods |
allowHeaders(String...) |
Content-Type, Authorization | Allowed headers |
allowCredentials(boolean) |
false |
Whether to send credentials |
maxAge(int) |
-1 |
Preflight cache duration |
- No Origin header → skip (pass through to next middleware)
- OPTIONS request → return 204 with CORS headers (preflight)
- Normal request → call
chain.next(), add CORS headers to response
The CORS spec forbids Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true. When allowCredentials(true) is called with allowOrigin("*"):
allowCredentials(true)throwsIllegalStateExceptionimmediately (fail-fast)- Workaround: use
allowOrigin("https://specific.domain")with credentials, or callallowCredentials(true)beforeallowOrigin("*")(order matters becauseallowCredentials()validates on call — you can set credentials first, then origin to*to skip validation)
Wait — actually looking at the code:
public CorsMiddleware allowCredentials(boolean allow) {
if (allow && "*".equals(allowOrigin)) {
throw new IllegalStateException(...);
}
this.allowCredentials = allow;
return this;
}The order matters: allowCredentials(true) throws if *. But allowCredentials(false) → then allowOrigin("*") works. And then allowCredentials(true) would throw. So the safe sequence is allowCredentials(true) first, then allowOrigin("specific-domain").
When adding CORS headers to an existing response, the middleware rebuilds the response (copies all headers, status, method, body) to preserve immutability.
File: miniJWS-core/src/main/java/.../middleware/GzipMiddleware.java
Response compression for clients that accept gzip encoding.
- Check
Accept-Encodingheader — if nogzip, pass through - Call
chain.next(request)to get the response - Skip if: body empty, already
Content-Encoding, or body < 256 bytes - Compress with
GZIPOutputStream - Skip if compressed ≥ original size
- Build new response with
Content-Encoding: gzip
new GzipMiddleware(); // default level 6
new GzipMiddleware(9); // max compression (slower)
new GzipMiddleware(1); // fastest, least compressionprivate final int level;
public GzipMiddleware(int level) {
this.level = Math.max(1, Math.min(9, level));
}The level is applied via an anonymous subclass:
var gz = new GZIPOutputStream(bos) {{
def.setLevel(level);
}};boolean alreadyEncoded = response.getHeaders().keySet().stream()
.anyMatch(k -> k.equalsIgnoreCase("Content-Encoding"));
if (alreadyEncoded) return response;Prevents compressing an already-compressed response.
try {
gz.write(data);
} finally {
gz.close(); // ensures trailer is written even if write fails
}The close() call writes the gzip trailer (CRC32 + size). Without it, the stream is incomplete.
File: miniJWS-core/src/main/java/.../middleware/RateLimitMiddleware.java
Per-IP rate limiting using a sliding window.
ConcurrentHashMap<String, Queue<Instant>> requests = new ConcurrentHashMap<>();
AtomicInteger totalEntries = new AtomicInteger(0);Each IP has a ConcurrentLinkedQueue<Instant> of request timestamps.
- Extract client IP (respects
X-Forwarded-For,X-Real-IP) - Get or create the timestamp queue for that IP
synchronized(queue):- Remove timestamps older than the window
- If queue size ≥
maxRequests→ return 429 - Otherwise, add current timestamp
- If
totalEntries > CLEANUP_THRESHOLD(10_000), triggercleanupStaleEntries()
private void cleanupStaleEntries(Instant cutoff) {
for (var it = requests.entrySet().iterator(); it.hasNext();) {
var entry = it.next();
Queue<Instant> q = entry.getValue();
synchronized (q) {
while (!q.isEmpty() && q.peek().isBefore(cutoff)) {
q.poll();
}
if (q.isEmpty()) {
it.remove(); // remove IP from map
removed++;
}
}
}
totalEntries.addAndGet(-removed);
}Without this cleanup, inactive IPs accumulate in the ConcurrentHashMap forever, causing a memory leak. The threshold-based cleanup (> 10_000 entries) ensures bounded memory usage.
return new HttpResponse.Builder()
.setStatusCode(429)
.setContentType(ContentType.TEXT)
.addHeader("Retry-After", String.valueOf(window.toSeconds()))
.setBody("429 - Too Many Requests")
.build();