enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user