queries(String name) { return resolveQueryParams().getAll(name); }
+
+ // ── Body ──────────────────────────────────────────────────────────────────
+
+ /**
+ * Returns the request body accessor. Use {@link RequestBody#bytes()} to materialise
+ * the full body or {@link RequestBody#stream()} for zero-copy streaming access.
+ * The two modes are mutually exclusive per request.
+ */
+ public RequestBody body() { return body; }
+
+ /** Discards unread body bytes; called by the server after each request on keep-alive connections. */
+ public void drain() { body.drain(); }
+
+ // ── Internal ─────────────────────────────────────────────────────────────
+
+ /** Internal: case-insensitive header value comparison used by the server keep-alive logic. */
+ public boolean headerEquals(String name, String value) {
+ return requestLine.getHeaders().valueEqualsIgnoreCase(name, value);
}
private QueryParams resolveQueryParams() {
diff --git a/flash/src/main/java/dev/relism/models/RequestHandler.java b/flash/src/main/java/dev/relism/models/RequestHandler.java
index f4f546e..310eaf0 100644
--- a/flash/src/main/java/dev/relism/models/RequestHandler.java
+++ b/flash/src/main/java/dev/relism/models/RequestHandler.java
@@ -13,5 +13,5 @@ public abstract class RequestHandler {
* return a {@link Response} to replace the whole response, any other non-null value
* to set it as the body, or {@code null} to leave the response as-is.
*/
- public abstract Object handle(Request request, Response response);
+ public abstract Object handle(Request request, Response response) throws Exception;
}
\ No newline at end of file
diff --git a/flash/src/main/java/dev/relism/models/Response.java b/flash/src/main/java/dev/relism/models/Response.java
index c121104..d74a320 100644
--- a/flash/src/main/java/dev/relism/models/Response.java
+++ b/flash/src/main/java/dev/relism/models/Response.java
@@ -1,45 +1,142 @@
package dev.relism.models;
import dev.relism.http.ContentType;
+import dev.relism.http.HttpStatus;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+/**
+ * HTTP response. All mutating methods return {@code this} for fluent chaining.
+ *
+ * {@code
+ * // fixed body
+ * return new Response(200, "ok", ContentType.TEXT_PLAIN);
+ *
+ * // known-length stream → Content-Length header
+ * return new Response(200, ContentType.BINARY).stream(Files.newInputStream(p), Files.size(p));
+ *
+ * // unknown-length stream → Transfer-Encoding: chunked
+ * return new Response(200, ContentType.TEXT_PLAIN).chunked(source);
+ * }
+ */
@Getter
@ToString
public class Response {
@Setter private int statusCode;
- private byte[] body;
- private byte[] contentType;
+ private byte[] statusBytes; // pre-encoded "200 OK"; null when set via status(int)
+ private byte[] body;
+ @ToString.Exclude
+ private InputStream stream;
+ private long streamLength; // meaningful only when isStreaming() && !chunked
+ private boolean chunked;
+ private byte[] contentType;
+ @Getter(lombok.AccessLevel.NONE)
+ private List headers; // pre-encoded "Name: Value\r\n" entries
+
+ // -------------------------------------------------------------------------
+ // Constructors
+ // -------------------------------------------------------------------------
+
+ public Response(int statusCode, ContentType contentType) {
+ this.statusCode = statusCode;
+ this.contentType = contentType.getBytes();
+ }
public Response(int statusCode, byte[] body, ContentType contentType) {
- this.statusCode = statusCode;
- this.body = body;
- this.contentType = contentType.getBytes();
+ this(statusCode, contentType);
+ this.body = body;
}
public Response(int statusCode, String text, ContentType contentType) {
- this.statusCode = statusCode;
- this.body = text.getBytes(StandardCharsets.UTF_8);
- this.contentType = contentType.getBytes();
+ this(statusCode, text.getBytes(StandardCharsets.UTF_8), contentType);
}
- public void setContentType(ContentType contentType) {
- this.contentType = contentType.getBytes();
- }
+ // -------------------------------------------------------------------------
+ // Fluent mutators
+ // -------------------------------------------------------------------------
- public void setContentType(String contentType) {
- this.contentType = contentType.getBytes(StandardCharsets.UTF_8);
- }
+ /** Sets the status code. The phrase is looked up from {@link HttpStatus} on the write path. */
+ public Response status(int code) { this.statusCode = code; this.statusBytes = null; return this; }
- public Response setBody(Object body) {
- if (body instanceof byte[] bytes) {
- this.body = bytes;
- } else if (body != null) {
- this.body = body.toString().getBytes(StandardCharsets.UTF_8);
- }
+ /** Sets the status from an {@link HttpStatus} constant. The pre-encoded bytes are used
+ * directly on the write path — zero lookup, zero allocation. */
+ public Response status(HttpStatus status) { this.statusCode = status.code(); this.statusBytes = status.bytes(); return this; }
+ public Response type(ContentType ct) { this.contentType = ct.getBytes(); return this; }
+ public Response type(String ct) { this.contentType = ct.getBytes(StandardCharsets.UTF_8); return this; }
+
+ public Response body(byte[] bytes) {
+ this.body = bytes;
+ this.stream = null;
return this;
}
+
+ public Response body(String text) {
+ return body(text.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /** Streaming response with known length; written with {@code Content-Length}. */
+ public Response stream(InputStream is, long length) {
+ this.stream = is;
+ this.streamLength = length;
+ this.chunked = false;
+ this.body = null;
+ return this;
+ }
+
+ /** Streaming response with unknown length; written with {@code Transfer-Encoding: chunked}. */
+ public Response chunked(InputStream is) {
+ this.stream = is;
+ this.chunked = true;
+ this.body = null;
+ return this;
+ }
+
+ /** Adds a response header. Encoded once at call time; zero-alloc on the write path. */
+ public Response header(String name, String value) {
+ if (headers == null) headers = new ArrayList<>();
+ headers.add((name + ": " + value + "\r\n").getBytes(StandardCharsets.UTF_8));
+ return this;
+ }
+
+ // -------------------------------------------------------------------------
+ // State queries
+ // -------------------------------------------------------------------------
+
+ 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
+ // -------------------------------------------------------------------------
+
+ /** Sets the body from an arbitrary handler return value. */
+ public Response setBody(Object body) {
+ if (body instanceof byte[] bytes) this.body = bytes;
+ else if (body != null) this.body = body.toString().getBytes(StandardCharsets.UTF_8);
+ return this;
+ }
+
+ public void setContentType(ContentType ct) { this.contentType = ct.getBytes(); }
+
+ /** Returns custom headers, or an empty list if none were added. */
+ public List getHeaders() { return headers != null ? headers : List.of(); }
+
+ /** Writes pre-encoded custom headers directly to {@code out}. Zero-alloc when no headers are set. */
+ public void writeHeaders(OutputStream out) throws IOException {
+ if (headers == null) return;
+ for (int i = 0, n = headers.size(); i < n; i++) out.write(headers.get(i));
+ }
}
diff --git a/flash/src/main/java/dev/relism/models/SimpleHandler.java b/flash/src/main/java/dev/relism/models/SimpleHandler.java
index 49e0103..409ff01 100644
--- a/flash/src/main/java/dev/relism/models/SimpleHandler.java
+++ b/flash/src/main/java/dev/relism/models/SimpleHandler.java
@@ -12,7 +12,7 @@ public class SimpleHandler extends RequestHandler {
private final FunctionalHandler delegate;
@Override
- public Object handle(Request request, Response response) {
+ public Object handle(Request request, Response response) throws Exception {
return delegate.handle(request, response);
}
@@ -21,6 +21,6 @@ public class SimpleHandler extends RequestHandler {
*/
@FunctionalInterface
public interface FunctionalHandler {
- Object handle(Request request, Response response);
+ Object handle(Request request, Response response) throws Exception;
}
}
diff --git a/flash/src/main/java/dev/relism/routing/AbstractRouter.java b/flash/src/main/java/dev/relism/routing/AbstractRouter.java
index 5172272..cc632af 100644
--- a/flash/src/main/java/dev/relism/routing/AbstractRouter.java
+++ b/flash/src/main/java/dev/relism/routing/AbstractRouter.java
@@ -12,8 +12,8 @@ import lombok.Getter;
import java.nio.charset.StandardCharsets;
/**
- * Base router. Each router has a namespace prefix (default {@code "/"}); routes added via
- * {@link #get}, {@link #post}, etc. are relative to it. Error handlers are scoped to this router.
+ * 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}.
*/
public abstract class AbstractRouter {
@@ -57,21 +57,20 @@ public abstract class AbstractRouter {
* 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.
*/
- public AbstractRouter get(String path, SimpleHandler.FunctionalHandler handler) {
- return addRoute(HttpMethod.GET, PathUtils.sanitize(path), new SimpleHandler(handler));
+ public AbstractRouter register(HttpMethod method, String path, SimpleHandler.FunctionalHandler handler) {
+ return addRoute(method, PathUtils.sanitize(path), new SimpleHandler(handler));
}
- public AbstractRouter post(String path, SimpleHandler.FunctionalHandler handler) {
- return addRoute(HttpMethod.POST, PathUtils.sanitize(path), new SimpleHandler(handler));
- }
-
- public AbstractRouter put(String path, SimpleHandler.FunctionalHandler handler) {
- return addRoute(HttpMethod.PUT, PathUtils.sanitize(path), new SimpleHandler(handler));
- }
-
- public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler handler) {
- return addRoute(HttpMethod.DELETE, PathUtils.sanitize(path), new SimpleHandler(handler));
- }
+ 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); }
/** Registers a class-based handler; the class must be annotated with {@link Route @Route}. */
public AbstractRouter register(RequestHandler handler) {
diff --git a/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
index 6ba6a31..47b1c4e 100644
--- a/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
+++ b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
@@ -39,7 +39,7 @@ public final class FastPathViews {
}
}
- /** Mutable composite view: method bytes + path. Reused via ThreadLocal — call reset() before use. */
+ /** Mutable composite view: method bytes + path. Reused via ThreadLocal, call reset() before use. */
public static final class MethodPathByteView implements ByteView {
private byte[] method;
private ByteView path;
diff --git a/flash/src/main/resources/assets/html/default_404.html b/flash/src/main/resources/assets/html/default_404.html
index ba88da6..443b44b 100644
--- a/flash/src/main/resources/assets/html/default_404.html
+++ b/flash/src/main/resources/assets/html/default_404.html
@@ -4,7 +4,7 @@
- 404 — Flash
+ 404