activeSockets = ConcurrentHashMap.newKeySet();
private volatile boolean stopped = false;
@@ -52,9 +53,14 @@ public class HttpServer {
DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
}
- public HttpServer(HttpServerConfiguration configuration) throws IOException {
+ /**
+ * Creates the server with optional server-level middlewares applied to every registered route.
+ * Middlewares are pre-fused at construction time; the first element executes outermost.
+ */
+ public HttpServer(HttpServerConfiguration configuration, Middleware... middlewares) throws IOException {
this.configuration = configuration;
- this.serverSocket = new ServerSocket(configuration.getPort());
+ this.serverSocket = new ServerSocket(configuration.getPort());
+ this.globalRouter = new GlobalRouter(middlewares);
}
/** Returns a future that completes once the accept loop is running and the server is ready. */
@@ -119,21 +125,18 @@ public class HttpServer {
return this;
}
- public HttpServer register(RequestHandler handler) {
- globalRouter.register(handler);
- return this;
- }
+ public HttpServer register(RequestHandler handler, Middleware... m) { globalRouter.register(handler, m); return this; }
- public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { globalRouter.get(path, h); return this; }
- public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { globalRouter.post(path, h); return this; }
- public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { globalRouter.put(path, h); return this; }
- public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { globalRouter.delete(path, h); return this; }
- public HttpServer patch(String path, SimpleHandler.FunctionalHandler h) { globalRouter.patch(path, h); return this; }
- public HttpServer options(String path, SimpleHandler.FunctionalHandler h) { globalRouter.options(path, h); return this; }
- public HttpServer head(String path, SimpleHandler.FunctionalHandler h) { globalRouter.head(path, h); return this; }
- public HttpServer trace(String path, SimpleHandler.FunctionalHandler h) { globalRouter.trace(path, h); return this; }
- public HttpServer connect(String path, SimpleHandler.FunctionalHandler h) { globalRouter.connect(path, h); return this; }
- public HttpServer purge(String path, SimpleHandler.FunctionalHandler h) { globalRouter.purge(path, h); return this; }
+ public HttpServer get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.get (path, h, m); return this; }
+ public HttpServer post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.post (path, h, m); return this; }
+ public HttpServer put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.put (path, h, m); return this; }
+ public HttpServer delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.delete (path, h, m); return this; }
+ public HttpServer patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.patch (path, h, m); return this; }
+ public HttpServer options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.options(path, h, m); return this; }
+ public HttpServer head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.head (path, h, m); return this; }
+ public HttpServer trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.trace (path, h, m); return this; }
+ public HttpServer connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.connect(path, h, m); return this; }
+ public HttpServer purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.purge (path, h, m); return this; }
private void process(Socket socket) {
diff --git a/flash/src/main/java/dev/relism/models/QueryParams.java b/flash/src/main/java/dev/relism/models/QueryParams.java
index 05fece8..4e28996 100644
--- a/flash/src/main/java/dev/relism/models/QueryParams.java
+++ b/flash/src/main/java/dev/relism/models/QueryParams.java
@@ -8,7 +8,10 @@ import java.util.List;
/**
* Lazy query parameter access ({@code ?key=value&...}). Backed by a zero-copy {@link ByteView}
- * over the raw query string bytes, no parsing at construction, values decoded on demand.
+ * over the raw query string bytes, no parsing at construction, values percent-decoded on demand.
+ *
+ * Percent-decoding ({@code %XX} → byte, {@code +} → space) follows the
+ * {@code application/x-www-form-urlencoded} convention used by all browsers.
*/
public class QueryParams {
public static final QueryParams EMPTY = new QueryParams(null);
@@ -22,10 +25,7 @@ public class QueryParams {
public String get(String name) {
long r = findFirst(name);
if (r < 0) return null;
- int s = (int) (r >> 32), l = (int) (r & 0xFFFFFFFFL);
- byte[] bytes = new byte[l];
- for (int i = 0; i < l; i++) bytes[i] = raw.byteAt(s + i);
- return new String(bytes, StandardCharsets.UTF_8);
+ return decode((int) (r >> 32), (int) (r & 0xFFFFFFFFL));
}
ByteView view(String name) {
@@ -51,11 +51,8 @@ public class QueryParams {
int valStart = i;
while (i < len && raw.byteAt(i) != '&') i++;
if (keyMatches(keyStart, keyLen, name)) {
- int valLen = i - valStart;
- byte[] bytes = new byte[valLen];
- for (int j = 0; j < valLen; j++) bytes[j] = raw.byteAt(valStart + j);
if (result == null) result = new ArrayList<>();
- result.add(new String(bytes, StandardCharsets.UTF_8));
+ result.add(decode(valStart, i - valStart));
}
}
if (i < len && raw.byteAt(i) == '&') i++;
@@ -63,6 +60,8 @@ public class QueryParams {
return result != null ? result : List.of();
}
+ // ── Internals ─────────────────────────────────────────────────────────────
+
/** Returns (valStart << 32) | valLen, or -1 if not found. */
private long findFirst(String name) {
if (raw == null) return -1L;
@@ -87,4 +86,39 @@ public class QueryParams {
for (int i = 0; i < len; i++) if (raw.byteAt(start + i) != (byte) name.charAt(i)) return false;
return true;
}
+
+ /**
+ * Percent-decodes a value slice from {@code raw} into a UTF-8 String.
+ * {@code %XX} triplets are decoded to their byte values; {@code +} decodes as space.
+ * Invalid {@code %} sequences are passed through as-is.
+ */
+ private String decode(int start, int length) {
+ byte[] out = new byte[length]; // upper bound — decoded is never longer
+ int w = 0;
+ for (int i = 0; i < length; i++) {
+ byte b = raw.byteAt(start + i);
+ if (b == '+') {
+ out[w++] = ' ';
+ } else if (b == '%' && i + 2 < length) {
+ int hi = hexVal(raw.byteAt(start + i + 1));
+ int lo = hexVal(raw.byteAt(start + i + 2));
+ if (hi >= 0 && lo >= 0) {
+ out[w++] = (byte) ((hi << 4) | lo);
+ i += 2;
+ } else {
+ out[w++] = b; // not a valid escape — keep literal %
+ }
+ } else {
+ out[w++] = b;
+ }
+ }
+ return new String(out, 0, w, StandardCharsets.UTF_8);
+ }
+
+ private static int hexVal(byte b) {
+ if (b >= '0' && b <= '9') return b - '0';
+ if (b >= 'a' && b <= 'f') return b - 'a' + 10;
+ if (b >= 'A' && b <= 'F') return b - 'A' + 10;
+ return -1;
+ }
}
diff --git a/flash/src/main/java/dev/relism/models/Response.java b/flash/src/main/java/dev/relism/models/Response.java
index d74a320..dc1c620 100644
--- a/flash/src/main/java/dev/relism/models/Response.java
+++ b/flash/src/main/java/dev/relism/models/Response.java
@@ -112,12 +112,6 @@ public class Response {
public boolean isStreaming() { return stream != null; }
- /**
- * Pre-encoded status bytes (e.g. {@code "200 OK"}), or {@code null} if the status was set
- * via {@link #status(int)} — in which case {@link HttpStatus#bytesForCode} is used as fallback.
- */
- public byte[] getStatusBytes() { return statusBytes; }
-
// -------------------------------------------------------------------------
// Internal setters used by HttpServer for handler return values
// -------------------------------------------------------------------------
diff --git a/flash/src/main/java/dev/relism/routing/AbstractRouter.java b/flash/src/main/java/dev/relism/routing/AbstractRouter.java
index cc632af..dd75354 100644
--- a/flash/src/main/java/dev/relism/routing/AbstractRouter.java
+++ b/flash/src/main/java/dev/relism/routing/AbstractRouter.java
@@ -12,9 +12,16 @@ import lombok.Getter;
import java.nio.charset.StandardCharsets;
/**
- * Base router. Each router has a namespace prefix (default {@code "/"});
+ * Base router. Each router has a namespace prefix (default {@code "/"}).
* Error handlers are scoped to this router.
- * The namespace is set automatically by {@link GlobalRouter#mount}.
+ *
+ *
Router-level {@link Middleware middlewares} are passed at construction time and
+ * pre-fused into a single wrapper applied to every handler registered on this router.
+ * Handler-level middlewares are passed to the {@link #register} / {@link #get} family of
+ * methods and wrapped inside the router-level chain.
+ *
+ *
All middleware composition happens once at boot time; the hot-path sees only a plain
+ * {@link RequestHandler} call with zero allocation and zero lookup overhead.
*/
public abstract class AbstractRouter {
@@ -24,6 +31,12 @@ public abstract class AbstractRouter {
@Getter(AccessLevel.PACKAGE)
protected byte[] namespaceBytes = new byte[]{ '/' };
+ /**
+ * Pre-fused router-level middleware, or {@code null} when none were registered.
+ * A single null-check in {@link #compile} is the only cost when no router middleware exists.
+ */
+ private final Middleware routerMiddleware;
+
protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> {
res.setStatusCode(404);
res.setContentType(ContentType.TEXT_HTML);
@@ -36,6 +49,20 @@ public abstract class AbstractRouter {
return ErrorPages.renderException(req, ex);
};
+ /**
+ * Constructs a router with optional router-level middlewares.
+ * Middlewares are pre-fused at construction time; the first element executes outermost.
+ *
+ * @param middlewares zero or more middlewares applied to every handler on this router
+ */
+ protected AbstractRouter(Middleware... middlewares) {
+ this.routerMiddleware = middlewares.length == 0 ? null
+ : middlewares.length == 1 ? middlewares[0]
+ : Middleware.of(middlewares);
+ }
+
+ // ── Internal wiring ───────────────────────────────────────────────────────
+
SimpleHandler getNotFoundHandler() { return notFoundHandler; }
ExceptionHandler getExceptionHandler() { return exceptionHandler; }
void setNamespace(String namespace) {
@@ -43,6 +70,30 @@ public abstract class AbstractRouter {
this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8);
}
+ /**
+ * Compiles a handler with its middleware chain. Called once per handler at registration.
+ *
+ *
Application order (innermost → outermost):
+ *
+ * - handler-level middlewares (passed at the call site)
+ * - router-level middleware (set in the constructor)
+ *
+ */
+ private RequestHandler compile(RequestHandler handler, Middleware[] handlerMiddlewares) {
+ RequestHandler compiled = handler;
+ // handler-level: wrap from innermost outward; box FunctionalHandler → SimpleHandler once per layer
+ for (int i = handlerMiddlewares.length - 1; i >= 0; i--)
+ compiled = new SimpleHandler(handlerMiddlewares[i].wrap(compiled));
+ // router-level: outermost wrapper, executes first
+ if (routerMiddleware != null)
+ compiled = new SimpleHandler(routerMiddleware.wrap(compiled));
+ return compiled;
+ }
+
+ private static final Middleware[] NO_MIDDLEWARES = new Middleware[0];
+
+ // ── Error handler configuration ───────────────────────────────────────────
+
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
this.notFoundHandler = new SimpleHandler(handler);
return this;
@@ -53,36 +104,54 @@ public abstract class AbstractRouter {
return this;
}
+ // ── Lambda handler registration ───────────────────────────────────────────
+
/**
- * Registers a route relative to this router's namespace. Return a {@link Response} to replace
- * it entirely, any other non-null value to set it as the body, or {@code null} to leave it unchanged.
+ * Registers a lambda handler with optional handler-level middlewares.
+ * Router-level middlewares are applied automatically on top.
*/
- public AbstractRouter register(HttpMethod method, String path, SimpleHandler.FunctionalHandler handler) {
- return addRoute(method, PathUtils.sanitize(path), new SimpleHandler(handler));
+ public AbstractRouter register(HttpMethod method, String path,
+ SimpleHandler.FunctionalHandler handler,
+ Middleware... middlewares) {
+ return addRoute(method, PathUtils.sanitize(path),
+ compile(new SimpleHandler(handler), middlewares));
}
- public AbstractRouter get(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.GET, path, h); }
- public AbstractRouter post(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.POST, path, h); }
- public AbstractRouter put(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PUT, path, h); }
- public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.DELETE, path, h); }
- public AbstractRouter patch(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PATCH, path, h); }
- public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.OPTIONS, path, h); }
- public AbstractRouter head(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.HEAD, path, h); }
- public AbstractRouter trace(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.TRACE, path, h); }
- public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.CONNECT, path, h); }
- public AbstractRouter purge(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PURGE, path, h); }
+ public AbstractRouter get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.GET, path, h, m); }
+ public AbstractRouter post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.POST, path, h, m); }
+ public AbstractRouter put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PUT, path, h, m); }
+ public AbstractRouter delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.DELETE, path, h, m); }
+ public AbstractRouter patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PATCH, path, h, m); }
+ public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.OPTIONS, path, h, m); }
+ public AbstractRouter head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.HEAD, path, h, m); }
+ public AbstractRouter trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.TRACE, path, h, m); }
+ public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.CONNECT, path, h, m); }
+ public AbstractRouter purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PURGE, path, h, m); }
- /** Registers a class-based handler; the class must be annotated with {@link Route @Route}. */
- public AbstractRouter register(RequestHandler handler) {
+ // ── Class-based handler registration ─────────────────────────────────────
+
+ /**
+ * Registers a class-based handler with optional handler-level middlewares.
+ * The class must carry a {@link Route @Route} annotation declaring method and path.
+ * Router-level middlewares are applied automatically on top.
+ */
+ public AbstractRouter register(RequestHandler handler, Middleware... middlewares) {
Route annotation = handler.getClass().getAnnotation(Route.class);
- if (annotation != null) {
- addRoute(HttpMethod.valueOf(annotation.method()), annotation.path(), handler);
- }
+ if (annotation != null)
+ addRoute(annotation.method(), annotation.path(),
+ compile(handler, middlewares));
return this;
}
+ // ── Routing ───────────────────────────────────────────────────────────────
+
public abstract RequestHandler route(Request request);
+ /**
+ * Stores a pre-compiled handler in the underlying routing structure.
+ * The handler passed here is already fully wrapped — implementations must not
+ * apply any additional middleware logic.
+ */
protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler);
protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) {
diff --git a/flash/src/main/java/dev/relism/routing/GlobalRouter.java b/flash/src/main/java/dev/relism/routing/GlobalRouter.java
index 5cd3d65..9a009cd 100644
--- a/flash/src/main/java/dev/relism/routing/GlobalRouter.java
+++ b/flash/src/main/java/dev/relism/routing/GlobalRouter.java
@@ -20,7 +20,14 @@ import java.util.Map;
public class GlobalRouter extends AbstractRouter {
private final Map subRoutersMap = new HashMap<>();
private final List sortedSubRouters = new ArrayList<>();
- private final AbstractRouter internalRouter = new FastPathRouterImpl();
+ private final AbstractRouter internalRouter;
+
+ public GlobalRouter(Middleware... middlewares) {
+ super(middlewares);
+ this.internalRouter = new FastPathRouterImpl();
+ }
+
+ public GlobalRouter() { this(new Middleware[0]); }
public void mount(String namespace, AbstractRouter router) {
String sanitized = PathUtils.sanitize(namespace);
diff --git a/flash/src/main/java/dev/relism/routing/Route.java b/flash/src/main/java/dev/relism/routing/Route.java
index ba34b82..efdfd1e 100644
--- a/flash/src/main/java/dev/relism/routing/Route.java
+++ b/flash/src/main/java/dev/relism/routing/Route.java
@@ -1,5 +1,7 @@
package dev.relism.routing;
+import dev.relism.http.HttpMethod;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -17,6 +19,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Route {
- String method() default "GET";
+ HttpMethod method() default HttpMethod.GET;
String path();
}
diff --git a/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
index 98089c8..bb9464a 100644
--- a/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
+++ b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
@@ -9,6 +9,7 @@ import dev.relism.http.HttpMethod;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.routing.AbstractRouter;
+import dev.relism.routing.Middleware;
import dev.relism.routing.PathUtils;
/**
@@ -22,6 +23,9 @@ public class FastPathRouterImpl extends AbstractRouter {
private volatile FastPathRouter router;
private String[] cachedParamNames;
+ public FastPathRouterImpl(Middleware... middlewares) { super(middlewares); }
+ public FastPathRouterImpl() { super(); }
+
private static final class FastPathRouterContext {
private static final ThreadLocal> RESULT_HOLDER =
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));