enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles

This commit is contained in:
Relism
2026-03-19 12:44:33 +01:00
parent 94d029a631
commit 96afbf665d
30 changed files with 1250 additions and 521 deletions
@@ -40,7 +40,7 @@ class HttpServerConcurrencyTest {
server = new HttpServer(config);
server.get("/ping", (req, res) -> "pong");
server.post("/echo", (req, res) -> {
byte[] body = req.getBody();
byte[] body = req.body().bytes();
return new Response(200, body, ContentType.TEXT_PLAIN);
});
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
@@ -39,14 +40,29 @@ class HttpServerTest {
server.get("/api/ping", (req, res) -> "pong");
server.post("/api/echo", (req, res) -> {
byte[] body = req.getBody();
return new Response(201, body, ContentType.TEXT_PLAIN); // echo body & change status
byte[] body = req.body().bytes();
return res.status(201).body(body); // echo body & change status
});
server.get("/api/crash", (req, res) -> {
throw new RuntimeException("Simulated Crash");
});
byte[] streamData = "streaming response body".getBytes(StandardCharsets.UTF_8);
server.get("/api/stream", (req, res) ->
res.stream(new ByteArrayInputStream(streamData), streamData.length));
server.get("/api/chunked-out", (req, res) ->
res.chunked(new ByteArrayInputStream(streamData)));
server.get("/api/custom-header", (req, res) ->
res.body("ok").header("X-Flash", "works"));
server.post("/api/chunked-in", (req, res) -> {
byte[] body = req.body().bytes();
return res.body(body);
});
server.start().get(5, TimeUnit.SECONDS);
}
@@ -57,27 +73,35 @@ class HttpServerTest {
}
}
// --- raw socket helper ---
// --- helpers ---
private static final int SOCKET_TIMEOUT_MS = 5000;
private String sendRawRequest(String rawHttp) throws Exception {
try (Socket socket = new Socket("127.0.0.1", port);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
out.write(rawHttp.getBytes(StandardCharsets.UTF_8));
out.flush();
return readOneResponse(in);
}
}
// Read until \r\n\r\n to get the full header block
ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
int b, prev3 = -1, prev2 = -1, prev1 = -1;
while ((b = in.read()) != -1) {
headerBuf.write(b);
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
prev3 = prev2; prev2 = prev1; prev1 = b;
}
String headers = headerBuf.toString(StandardCharsets.UTF_8);
private String readOneResponse(InputStream in) throws Exception {
ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
int b, prev3 = -1, prev2 = -1, prev1 = -1;
while ((b = in.read()) != -1) {
headerBuf.write(b);
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
prev3 = prev2; prev2 = prev1; prev1 = b;
}
String headers = headerBuf.toString(StandardCharsets.UTF_8);
// Parse Content-Length
String body;
if (headers.toLowerCase().contains("transfer-encoding: chunked")) {
body = readChunkedBody(in);
} else {
int contentLength = 0;
for (String line : headers.split("\r\n")) {
if (line.toLowerCase().startsWith("content-length:")) {
@@ -85,11 +109,25 @@ class HttpServerTest {
break;
}
}
// Read exactly Content-Length bytes for the body
byte[] body = in.readNBytes(contentLength);
return headers + new String(body, StandardCharsets.UTF_8);
body = new String(in.readNBytes(contentLength), StandardCharsets.UTF_8);
}
return headers + body;
}
private String readChunkedBody(InputStream in) throws Exception {
ByteArrayOutputStream result = new ByteArrayOutputStream();
while (true) {
StringBuilder sizeLine = new StringBuilder();
int b;
while ((b = in.read()) != -1 && b != '\n') {
if (b != '\r') sizeLine.append((char) b);
}
int size = Integer.parseInt(sizeLine.toString().trim(), 16);
if (size == 0) break;
result.write(in.readNBytes(size));
in.read(); in.read(); // \r\n after chunk data
}
return result.toString(StandardCharsets.UTF_8);
}
// --- E2E Tests ---
@@ -154,10 +192,84 @@ class HttpServerTest {
String req = "GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n";
String res = sendRawRequest(req);
assertTrue(res.startsWith("HTTP/1.1 404 Not Found"));
assertTrue(res.contains("No route matched this request."));
}
// --- streaming response ---
@Test
void testStreamingResponse_writesContentLengthAndBody() throws Exception {
String req = "GET /api/stream HTTP/1.1\r\nHost: localhost\r\n\r\n";
String res = sendRawRequest(req);
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
assertTrue(res.contains("Content-Length: 23")); // "streaming response body"
assertTrue(res.endsWith("streaming response body"));
}
@Test
void testChunkedResponse_writesTransferEncodingChunked() throws Exception {
String req = "GET /api/chunked-out HTTP/1.1\r\nHost: localhost\r\n\r\n";
String res = sendRawRequest(req);
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
assertTrue(res.contains("Transfer-Encoding: chunked"));
assertTrue(res.endsWith("streaming response body"));
}
// --- custom response headers ---
@Test
void testCustomHeader_appearsInResponse() throws Exception {
String req = "GET /api/custom-header HTTP/1.1\r\nHost: localhost\r\n\r\n";
String res = sendRawRequest(req);
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
assertTrue(res.contains("X-Flash: works\r\n"));
}
// --- chunked request body ---
@Test
void testChunkedRequestBody_decodedAndEchoed() throws Exception {
String req = "POST /api/chunked-in HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
String res = sendRawRequest(req);
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
assertTrue(res.endsWith("hello world"));
}
// --- keep-alive ---
@Test
void testKeepAlive_twoRequestsOnSameConnection() throws Exception {
String req = "GET /api/ping HTTP/1.1\r\nHost: localhost\r\n\r\n";
try (Socket socket = new Socket("127.0.0.1", port);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
out.write(req.getBytes(StandardCharsets.UTF_8));
out.write(req.getBytes(StandardCharsets.UTF_8));
out.flush();
String first = readOneResponse(in);
String second = readOneResponse(in);
assertTrue(first.contains("pong"));
assertTrue(second.contains("pong"));
assertTrue(first.contains("Connection: keep-alive"));
assertTrue(second.contains("Connection: keep-alive"));
}
}
}
@@ -44,8 +44,8 @@ class RequestParserTest {
@Test
void queryParam_resolvedFromPath() throws IOException {
Request r = parse(req("GET /search?q=flash&page=2 HTTP/1.1", "Host: localhost"));
assertEquals("flash", r.getQueryParam("q"));
assertEquals("2", r.getQueryParam("page"));
assertEquals("flash", r.query("q"));
assertEquals("2", r.query("page"));
}
// --- headers ---
@@ -53,15 +53,15 @@ class RequestParserTest {
@Test
void headers_parsed() throws IOException {
Request r = parse(req("GET / HTTP/1.1", "Host: example.com", "Accept: application/json"));
assertEquals("example.com", r.getHeader("Host"));
assertEquals("application/json", r.getHeader("Accept"));
assertEquals("example.com", r.header("Host"));
assertEquals("application/json", r.header("Accept"));
}
@Test
void headers_caseInsensitive() throws IOException {
Request r = parse(req("GET / HTTP/1.1", "Content-Type: text/plain"));
assertEquals("text/plain", r.getHeader("content-type"));
assertEquals("text/plain", r.getHeader("CONTENT-TYPE"));
assertEquals("text/plain", r.header("content-type"));
assertEquals("text/plain", r.header("CONTENT-TYPE"));
}
// --- body ---
@@ -72,13 +72,13 @@ class RequestParserTest {
String raw = "POST / HTTP/1.1\r\nContent-Length: " + body.length() + "\r\n\r\n" + body;
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r);
assertEquals(body, new String(r.getBody(), StandardCharsets.UTF_8));
assertEquals(body, new String(r.body().bytes(), StandardCharsets.UTF_8));
}
@Test
void body_emptyWhenNoContentLength() throws IOException {
Request r = parse(req("GET / HTTP/1.1", "Host: localhost"));
assertEquals(0, r.getBody().length);
assertTrue(r.body().isEmpty());
}
// --- edge cases / robustness ---
@@ -102,19 +102,45 @@ class RequestParserTest {
@Test
void requestLine_noProtocol_throwsIOException() {
// No space after path parser cannot find protocol boundary
// No space after path, parser cannot find protocol boundary
assertThrows(IOException.class, () -> parse(req("GET /noproto")));
}
@Test
void headers_exceedingMaxBufferSize_throwsIOException() {
// Feed more bytes than the configured cap with no \r\n\r\n must throw
// Feed more bytes than the configured cap with no \r\n\r\n : must throw
int cap = 16 * 1024;
byte[] giant = new byte[cap + 1];
Arrays.fill(giant, (byte) 'A');
assertThrows(IOException.class, () -> new RequestParser(cap).parse(new ByteArrayInputStream(giant)));
}
@Test
void chunkedTransferEncoding_bodyReadable() throws IOException {
String raw = "POST / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r);
assertEquals(-1L, r.body().contentLength()); // -1 = chunked
assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8), r.body().bytes());
}
@Test
void contentLength_parsedAsLong() throws IOException {
// 5 GB — too large to materialize, but contentLength must be a long
String raw = "POST / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Length: 5000000000\r\n" +
"\r\n";
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r);
assertEquals(5_000_000_000L, r.body().contentLength());
assertThrows(IllegalStateException.class, r.body()::bytes);
}
@Test
void contentLength_largerThanBody_readsPartial() throws IOException {
// Content-Length claims 50 but stream ends after 5 bytes
@@ -122,7 +148,7 @@ class RequestParserTest {
String raw = "POST / HTTP/1.1\r\nContent-Length: 50\r\n\r\n" + body;
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r);
assertEquals(50, r.getBody().length);
assertEquals(body, new String(r.getBody(), 0, body.length(), StandardCharsets.UTF_8));
assertEquals(50, r.body().bytes().length);
assertEquals(body, new String(r.body().bytes(), 0, body.length(), StandardCharsets.UTF_8));
}
}
@@ -12,73 +12,63 @@ class HeaderMapTest {
// --- helpers ---
private static HeaderMap parse(String raw, String... headers) {
private static HeaderMap parse(String... headers) {
StringBuilder sb = new StringBuilder();
for (String h : headers) sb.append(h).append("\r\n");
byte[] buffer = sb.toString().getBytes(StandardCharsets.UTF_8);
HeaderMap map = new HeaderMap(buffer);
int current = 0;
for (int i = 0; i < headers.length; i++) {
int colon = sb.indexOf(":", current);
int lineEnd = sb.indexOf("\r\n", current);
int valueStart = colon + 1;
while (valueStart < lineEnd && buffer[valueStart] == ' ') valueStart++;
map.add(current, colon - current, valueStart, lineEnd - valueStart);
current = lineEnd + 2;
}
HeaderMap map = new HeaderMap();
map.reset(buffer, 0, buffer.length);
return map;
}
// --- getFirst ---
// --- first ---
@Test
void getFirst_existingHeader() {
HeaderMap map = parse("", "Host: localhost", "Accept: text/plain");
assertEquals("localhost", map.getFirst("Host"));
assertEquals("text/plain", map.getFirst("Accept"));
void first_existingHeader() {
HeaderMap map = parse("Host: localhost", "Accept: text/plain");
assertEquals("localhost", map.first("Host"));
assertEquals("text/plain", map.first("Accept"));
}
@Test
void getFirst_caseInsensitive() {
HeaderMap map = parse("", "ConteNT-tYPe: application/json");
assertEquals("application/json", map.getFirst("content-type"));
assertEquals("application/json", map.getFirst("CONTENT-TYPE"));
void first_caseInsensitive() {
HeaderMap map = parse("ConteNT-tYPe: application/json");
assertEquals("application/json", map.first("content-type"));
assertEquals("application/json", map.first("CONTENT-TYPE"));
}
@Test
void getFirst_missingHeader_returnsNull() {
HeaderMap map = parse("", "Host: localhost");
assertNull(map.getFirst("Accept"));
void first_missingHeader_returnsNull() {
HeaderMap map = parse("Host: localhost");
assertNull(map.first("Accept"));
}
// --- getAll ---
// --- all ---
@Test
void getAll_multipleValuesByName() {
HeaderMap map = parse("", "Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
assertEquals(List.of("a=1", "b=2"), map.getAll("Cookie"));
void all_multipleValuesByName() {
HeaderMap map = parse("Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
assertEquals(List.of("a=1", "b=2"), map.all("Cookie"));
}
@Test
void getAll_missingHeader_returnsEmptyList() {
HeaderMap map = parse("", "Host: localhost");
assertTrue(map.getAll("Cookie").isEmpty());
void all_missingHeader_returnsEmptyList() {
HeaderMap map = parse("Host: localhost");
assertTrue(map.all("Cookie").isEmpty());
}
@Test
void getAll_returnsAllHeaders() {
HeaderMap map = parse("", "A: 1", "B: 2");
assertEquals(List.of("1", "2"), map.getAll());
void all_returnsAllHeaders() {
HeaderMap map = parse("A: 1", "B: 2");
assertEquals(List.of("1", "2"), map.all());
}
// --- getView ---
// --- view ---
@Test
void getView_returnsZeroCopyView() {
HeaderMap map = parse("", "Host: localhost");
ByteView view = map.getView("Host");
void view_returnsZeroCopyView() {
HeaderMap map = parse("Host: localhost");
ByteView view = map.view("Host");
assertNotNull(view);
assertEquals(9, view.length());
assertEquals('l', view.byteAt(0));
@@ -86,20 +76,18 @@ class HeaderMapTest {
}
@Test
void getView_missingHeader_returnsNull() {
HeaderMap map = parse("", "Host: localhost");
assertNull(map.getView("Accept"));
void view_missingHeader_returnsNull() {
HeaderMap map = parse("Host: localhost");
assertNull(map.view("Accept"));
}
// --- limit ---
// --- empty ---
@Test
void maxHeadersLimit() {
byte[] buffer = new byte[100];
HeaderMap map = new HeaderMap(buffer);
for (int i = 0; i < 32; i++) {
map.add(0, 1, 1, 1);
}
assertThrows(IllegalStateException.class, () -> map.add(0, 1, 1, 1));
void emptyMap_returnsNullAndEmptyList() {
HeaderMap map = new HeaderMap();
assertNull(map.first("Host"));
assertTrue(map.all("Host").isEmpty());
assertTrue(map.all().isEmpty());
}
}
@@ -28,7 +28,7 @@ class RequestLineTest {
ByteView path = viewOf("/api");
ByteView query = viewOf("q=1");
ByteView proto = viewOf("HTTP/1.1");
HeaderMap headers = new HeaderMap(new byte[0]);
HeaderMap headers = new HeaderMap();
RequestLine rl = new RequestLine(HttpMethod.GET, path, query, proto, headers);
@@ -25,64 +25,65 @@ class RequestTest {
// --- creation ---
@Test
void request_creationAndGetters() {
HeaderMap headers = new HeaderMap(new byte[0]);
void request_creationAndAccessors() {
HeaderMap headers = new HeaderMap();
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/path"), viewOf("q=1"), viewOf("HTTP/1.1"), headers);
byte[] body = "body".getBytes(StandardCharsets.UTF_8);
Request r = new Request(line, body);
assertEquals(line, r.getRequestLine());
assertArrayEquals(body, r.getBody());
assertEquals(HttpMethod.GET, r.method());
assertEquals("/path", r.path());
assertArrayEquals(body, r.body().bytes());
assertNotNull(r.toString());
}
// --- delegates ---
// --- headers ---
@Test
void headers_delegatesToRequestLine() {
void header_delegatesToRequestLine() {
byte[] buffer = "Host: localhost\r\n".getBytes(StandardCharsets.UTF_8);
HeaderMap headers = new HeaderMap(buffer);
headers.add(0, 4, 6, 9);
HeaderMap headers = new HeaderMap();
headers.reset(buffer, 0, buffer.length);
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), headers);
Request r = new Request(line, new byte[0]);
assertEquals("localhost", r.getHeader("Host"));
assertEquals(List.of("localhost"), r.getHeaders("Host"));
assertEquals(List.of("localhost"), r.getHeaders());
assertEquals("localhost", r.header("Host"));
assertEquals(List.of("localhost"), r.headers("Host"));
assertEquals(List.of("localhost"), r.headers());
}
// --- pathParams ---
// --- path params ---
@Test
void pathParam_lazyGet() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
void param_lazyGet() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
Request r = new Request(line, new byte[0]);
assertNull(r.getPathParam("id"));
assertNull(r.param("id"));
r.setPathParams(new PathParams(viewOf("/123"), new String[]{"id"}, new int[]{1}, new int[]{3}));
assertEquals("123", r.getPathParam("id"));
assertEquals("123", r.param("id"));
}
// --- queryParams ---
// --- query params ---
@Test
void queryParam_lazyGet_fromQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
void query_lazyGet_fromQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new HeaderMap());
Request r = new Request(line, new byte[0]);
assertEquals("1", r.getQueryParam("a"));
assertEquals("2", r.getQueryParam("b")); // First value
assertEquals(List.of("2", "3"), r.getQueryParams("b"));
assertEquals("1", r.query("a"));
assertEquals("2", r.query("b"));
assertEquals(List.of("2", "3"), r.queries("b"));
}
@Test
void queryParam_lazyGet_nullQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
void query_lazyGet_nullQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
Request r = new Request(line, new byte[0]);
assertNull(r.getQueryParam("a"));
assertTrue(r.getQueryParams("a").isEmpty());
assertNull(r.query("a"));
assertTrue(r.queries("a").isEmpty());
}
}
@@ -3,19 +3,21 @@ package dev.relism.models;
import dev.relism.http.ContentType;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class ResponseTest {
// --- creation ---
// --- construction ---
@Test
void constructor_withByteArray() {
byte[] body = "bytes".getBytes(StandardCharsets.UTF_8);
Response r = new Response(200, body, ContentType.BINARY);
assertEquals(200, r.getStatusCode());
assertArrayEquals(body, r.getBody());
assertArrayEquals("application/octet-stream".getBytes(StandardCharsets.UTF_8), r.getContentType());
@@ -25,47 +27,98 @@ class ResponseTest {
@Test
void constructor_withStringText() {
Response r = new Response(404, "Not Found Text", ContentType.TEXT_PLAIN);
assertEquals(404, r.getStatusCode());
assertArrayEquals("Not Found Text".getBytes(StandardCharsets.UTF_8), r.getBody());
assertArrayEquals(ContentType.TEXT_PLAIN.getBytes(), r.getContentType());
}
// --- setters ---
// --- fluent mutators ---
@Test
void setContentType_byEnum() {
void type_byEnum() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.setContentType(ContentType.JSON);
r.type(ContentType.JSON);
assertArrayEquals(ContentType.JSON.getBytes(), r.getContentType());
}
@Test
void setContentType_byString() {
void type_byString() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.setContentType("application/custom");
r.type("application/custom");
assertArrayEquals("application/custom".getBytes(StandardCharsets.UTF_8), r.getContentType());
}
@Test
void setBody_withByteArray() {
void body_withByteArray() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
byte[] newBody = "new".getBytes(StandardCharsets.UTF_8);
r.setBody(newBody);
r.body(newBody);
assertArrayEquals(newBody, r.getBody());
}
@Test
void setBody_withObjectConvertedToString() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.setBody(12345); // auto boxes to Integer, toString called
r.setBody(12345);
assertArrayEquals("12345".getBytes(StandardCharsets.UTF_8), r.getBody());
}
@Test
void setStatusCode() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.setStatusCode(201);
assertEquals(201, r.getStatusCode());
}
// --- streaming ---
@Test
void stream_knownLength() {
InputStream src = new ByteArrayInputStream("body".getBytes());
Response r = new Response(200, ContentType.TEXT_PLAIN).stream(src, 4L);
assertTrue(r.isStreaming());
assertFalse(r.isChunked());
assertEquals(4L, r.getStreamLength());
assertSame(src, r.getStream());
assertNull(r.getBody());
}
@Test
void chunked_setsChunkedFlag() {
InputStream src = new ByteArrayInputStream("x".getBytes());
Response r = new Response(200, ContentType.TEXT_PLAIN).chunked(src);
assertTrue(r.isStreaming());
assertTrue(r.isChunked());
assertSame(src, r.getStream());
assertNull(r.getBody());
}
@Test
void isStreaming_falseForFixedBody() {
assertFalse(new Response(200, new byte[0], ContentType.TEXT_PLAIN).isStreaming());
}
// --- header ---
@Test
void header_encodedCorrectly() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.header("X-Foo", "bar");
assertArrayEquals("X-Foo: bar\r\n".getBytes(StandardCharsets.UTF_8), r.getHeaders().get(0));
}
@Test
void header_chaining_returnsSelf() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
assertSame(r, r.header("A", "1").header("B", "2"));
assertEquals(2, r.getHeaders().size());
}
@Test
void getHeaders_emptyWhenNoneAdded() {
assertTrue(new Response(200, new byte[0], ContentType.TEXT_PLAIN).getHeaders().isEmpty());
}
}
@@ -44,7 +44,7 @@ class GlobalRouterTest {
RequestLine line = new RequestLine(
HttpMethod.GET, pathView, null,
new FastPathViews.RequestByteView("HTTP/1.1".getBytes(StandardCharsets.UTF_8), 0, 8),
new HeaderMap(new byte[0])
new HeaderMap()
);
return new Request(line, new byte[0]);
}
@@ -23,7 +23,7 @@ class FastPathRouterImplTest {
RequestLine line = new RequestLine(
method, pathView, null,
new FastPathViews.RequestByteView("HTTP/1.1".getBytes(StandardCharsets.UTF_8), 0, 8),
new HeaderMap(new byte[0])
new HeaderMap()
);
return new Request(line, new byte[0]);
}
@@ -31,7 +31,7 @@ class FastPathRouterImplTest {
// --- route ---
@Test
void route_lazyCompilationAndMatch() {
void route_lazyCompilationAndMatch() throws Exception {
FastPathRouterImpl router = new FastPathRouterImpl();
router.get("/a", (req, res) -> "A");
@@ -57,7 +57,7 @@ class FastPathRouterImplTest {
}
@Test
void route_extractsPathParams() {
void route_extractsPathParams() throws Exception {
FastPathRouterImpl router = new FastPathRouterImpl();
router.get("/users/{id}/items/{itemId}", (req, res) -> "Extract");
@@ -70,7 +70,7 @@ class FastPathRouterImplTest {
// Verify path params were injected
assertNotNull(request.getPathParams());
assertEquals("123", request.getPathParam("id"));
assertEquals("456", request.getPathParam("itemId"));
assertEquals("123", request.param("id"));
assertEquals("456", request.param("itemId"));
}
}
@@ -24,7 +24,7 @@ class ErrorPagesTest {
byte[] protoBytes = protocol.getBytes(StandardCharsets.UTF_8);
FastPathViews.RequestByteView protoView = new FastPathViews.RequestByteView(protoBytes, 0, protoBytes.length);
RequestLine line = new RequestLine(HttpMethod.GET, pathView, null, protoView, new HeaderMap(new byte[0]));
RequestLine line = new RequestLine(HttpMethod.GET, pathView, null, protoView, new HeaderMap());
return new Request(line, new byte[0]);
}