feat(core): HTTP/2 Phase 6 — Request/Response model refactor

Pools Request/RequestBody/RequestLine/Response per connection (EX-20..EX-24),
following the same reset()/dev-mode-guard idiom Http1HeaderMap already used.
HeaderMap splits into HeaderView (interface) + Http1HeaderMap (impl, DEC-22).
Response gains byte-level structured headers, PreEncodedHeader, and
ResponseSerializer as the single source of truth for a response's header
sequence, consumed by Http1ResponseWriter's single-bulk-write rewrite (EX-27).
ByteTemplate gets O(1) slot lookup plus a buffer-writing overload (EX-28).
Multipart audited: three resource-exhaustion gaps found and fixed — unbounded
buffered part size, part count, and per-part header parsing (EX-38..EX-40) —
and boundary length confirmed already bounded (EX-41).

Re-measuring RequestPipelineBenchmark after the pooling work surfaced one more
per-request allocation underneath it (RequestParser building fresh
RequestByteViews every call) and, while checking the phase's own DoD text, an
unbounded Response.header(...) loop hazard neither had a limit — both fixed
(EX-42, EX-43). The h1 zero-alloc contract now holds: parseAndRoute measures
0.008 B/op (JMH noise floor), down from Phase 4's 120.008 B/op (DEC-20, DEC-23).

MESSAGE-MODEL.md records the pooling model; README gains an "Object lifetime"
section documenting the do-not-retain-past-the-handler contract. 503/503 tests
green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-13 15:26:08 +00:00
co-authored by Claude Sonnet 5
parent 0e1bbed42c
commit d882ea255c
51 changed files with 2395 additions and 372 deletions
@@ -56,6 +56,32 @@ class RequestParserTest {
assertEquals("2", r.query("page"));
}
// --- EX-42: pooled RequestByteViews (path/query/protocol) don't leak across requests ---
@Test
void samePooledParser_secondRequestWithoutQuery_doesNotLeakFirstRequestsQuery() throws IOException {
RequestParser parser = new RequestParser();
byte[] first = req("GET /search?token=super-secret HTTP/1.1", "Host: a").replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8);
Request r1 = parser.parse(source(first));
assertEquals("token=super-secret", r1.getRequestLine().getQuery().toString());
byte[] second = req("GET /health HTTP/1.1", "Host: a").replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8);
Request r2 = parser.parse(source(second));
assertNull(r2.getRequestLine().getQuery(), "the second request must not see the first request's leftover query view");
assertEquals("/health", r2.getRequestLine().getPath().toString());
}
@Test
void samePooledParser_secondRequest_seesOnlyItsOwnPathAndProtocol() throws IOException {
RequestParser parser = new RequestParser();
Request r1 = parser.parse(source(req("GET /first HTTP/1.1", "Host: a").replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8)));
assertEquals("/first", r1.getRequestLine().getPath().toString());
Request r2 = parser.parse(source(req("POST /second HTTP/1.0", "Host: a").replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8)));
assertEquals("/second", r2.getRequestLine().getPath().toString());
assertEquals("HTTP/1.0", r2.getRequestLine().getProtocol().toString());
}
// --- headers ---
@Test
@@ -2,7 +2,7 @@ package dev.relism.flash.api.multipart;
import dev.relism.fpr.core.ByteView;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.HeaderMap;
import dev.relism.flash.models.Http1HeaderMap;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestLine;
import org.junit.jupiter.api.Test;
@@ -41,7 +41,7 @@ class MultipartTest {
private static Request request(byte[] bodyBytes) {
String ct = "multipart/form-data; boundary=" + BOUNDARY;
byte[] headerBuf = ("Content-Type: " + ct).getBytes(StandardCharsets.US_ASCII);
HeaderMap headers = new HeaderMap();
Http1HeaderMap headers = new Http1HeaderMap();
headers.reset(headerBuf, 0, headerBuf.length);
RequestLine line = new RequestLine(HttpMethod.POST, viewOf("/upload"), null, viewOf("HTTP/1.1"), headers);
return new Request(line, bodyBytes);
@@ -236,11 +236,65 @@ class MultipartTest {
@Test
void of_notMultipart_throws() {
byte[] headerBuf = "Content-Type: application/json".getBytes(StandardCharsets.US_ASCII);
HeaderMap headers = new HeaderMap();
Http1HeaderMap headers = new Http1HeaderMap();
headers.reset(headerBuf, 0, headerBuf.length);
RequestLine line = new RequestLine(HttpMethod.POST, viewOf("/"), null, viewOf("HTTP/1.1"), headers);
Request req = new Request(line, new byte[0]);
assertThrows(IllegalArgumentException.class, () -> Multipart.of(req));
}
// -------------------------------------------------------------------------
// EX-29: resource-exhaustion bounds
// -------------------------------------------------------------------------
@Test
void field_bodyAboveMaxBufferedSize_throws() throws IOException {
// MAX_MULTIPART_BUFFERED_PART_SIZE is 10 MiB — one byte over must be rejected, not
// buffered whole into a single byte[].
String tooBig = "z".repeat((int) dev.relism.flash.http.Http1Limits.MAX_MULTIPART_BUFFERED_PART_SIZE + 1);
Multipart mp = Multipart.of(request(body(textPart("huge", tooBig))));
assertThrows(IOException.class, () -> mp.field("huge"));
}
@Test
void file_materializedDuringFullScan_aboveMaxBufferedSize_throws() throws IOException {
String tooBig = "z".repeat((int) dev.relism.flash.http.Http1Limits.MAX_MULTIPART_BUFFERED_PART_SIZE + 1);
Multipart mp = Multipart.of(request(body(filePart("f", "f.bin", "application/octet-stream", tooBig))));
assertThrows(IOException.class, mp::parts);
}
@Test
void scan_tooManyParts_throws() throws IOException {
String[] parts = new String[dev.relism.flash.http.Http1Limits.MAX_MULTIPART_PARTS + 1];
for (int i = 0; i < parts.length; i++) parts[i] = textPart("f" + i, "v");
Multipart mp = Multipart.of(request(body(parts)));
assertThrows(IOException.class, mp::parts);
}
@Test
void partHeaders_tooManyHeaderLines_throws() throws IOException {
StringBuilder part = new StringBuilder("Content-Disposition: form-data; name=\"x\"\r\n");
for (int i = 0; i <= dev.relism.flash.http.Http1Limits.MAX_MULTIPART_PART_HEADER_COUNT; i++) {
part.append("X-Extra-").append(i).append(": v\r\n");
}
part.append("\r\nbody");
Multipart mp = Multipart.of(request(body(part.toString())));
assertThrows(IOException.class, () -> mp.field("x"));
}
@Test
void partHeaderLine_tooLong_throws() throws IOException {
String longValue = "v".repeat(dev.relism.flash.http.Http1Limits.MAX_MULTIPART_HEADER_LINE_LENGTH + 1);
String part = "Content-Disposition: form-data; name=\"x\"\r\n"
+ "X-Long: " + longValue + "\r\n\r\nbody";
Multipart mp = Multipart.of(request(body(part)));
assertThrows(IOException.class, () -> mp.field("x"));
}
@Test
void withinAllLimits_stillWorksNormally() throws IOException {
// Sanity check the bounds above don't false-positive on a normal small request.
assertEquals("alice", Multipart.of(request(body(textPart("username", "alice")))).field("username"));
}
}
@@ -93,6 +93,13 @@ class ByteWriterTest {
assertEquals("content-type", asString(w));
}
@Test
void writeAscii_preservesCase() {
ByteWriter w = new ByteWriter(4);
w.writeAscii("Content-TYPE");
assertEquals("Content-TYPE", asString(w));
}
@Test
void writeUInt16_bigEndian() {
ByteWriter w = new ByteWriter(4);
@@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
@@ -133,4 +134,52 @@ class Http1ResponseWriterTest {
String raw = write(response, HttpMethod.GET, false, false);
assertTrue(raw.contains("Connection: close\r\n"), raw);
}
// --- EX-27: one bulk write for a small fixed body -----------------------------
/** Counts calls to {@code write(byte[], int, int)} — the only overload {@link Http1ResponseWriter} uses. */
private static final class CountingOutputStream extends java.io.OutputStream {
final ByteArrayOutputStream sink = new ByteArrayOutputStream();
int arrayWriteCalls;
@Override public void write(int b) { sink.write(b); }
@Override
public void write(byte[] b, int off, int len) {
arrayWriteCalls++;
sink.write(b, off, len);
}
}
@Test
void smallFixedBody_isWrittenInExactlyOneCall() throws IOException {
CountingOutputStream out = new CountingOutputStream();
Response response = new Response(200, "hello world", ContentType.TEXT_PLAIN);
Http1ResponseWriter.writeResponse(out, response, HttpMethod.GET, true, false, scratch());
assertEquals(1, out.arrayWriteCalls, "head + small body must leave in a single write() call");
assertTrue(out.sink.toString(StandardCharsets.UTF_8).endsWith("hello world"));
}
@Test
void bodyAboveInlineThreshold_isWrittenInTwoCalls() throws IOException {
CountingOutputStream out = new CountingOutputStream();
byte[] bigBody = new byte[dev.relism.flash.http.Http1Limits.INLINE_BODY_THRESHOLD + 1];
Arrays.fill(bigBody, (byte) 'x');
Response response = new Response(200, bigBody, ContentType.BINARY);
Http1ResponseWriter.writeResponse(out, response, HttpMethod.GET, true, false, scratch());
assertEquals(2, out.arrayWriteCalls, "head and an over-threshold body are written separately");
assertTrue(out.sink.toString(StandardCharsets.UTF_8).endsWith("x".repeat(bigBody.length)));
}
@Test
void headResponse_stillOneCall_noBodyBytes() throws IOException {
CountingOutputStream out = new CountingOutputStream();
Response response = new Response(200, "hello world", ContentType.TEXT_PLAIN);
Http1ResponseWriter.writeResponse(out, response, HttpMethod.HEAD, true, false, scratch());
assertEquals(1, out.arrayWriteCalls);
assertFalse(out.sink.toString(StandardCharsets.UTF_8).contains("hello world"));
}
}
@@ -8,25 +8,25 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-09}: dedicated correctness coverage for {@link HeaderMap}'s per-{@code reset()}
* {@code EX-09}: dedicated correctness coverage for {@link Http1HeaderMap}'s per-{@code reset()}
* index duplicate names, case variation, zero headers, and growth past the initial index
* capacity up to {@code Http1Limits.MAX_HEADER_COUNT}. {@link HeaderMapTest} already covers the
* ordinary lookup/forEach contract; this class targets the index machinery specifically.
*/
class HeaderMapIndexTest {
class Http1HeaderMapIndexTest {
private static HeaderMap parse(String... headers) {
private static Http1HeaderMap 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();
Http1HeaderMap map = new Http1HeaderMap();
map.reset(buffer, 0, buffer.length);
return map;
}
@Test
void zeroHeaders_everyLookupIsEmpty() {
HeaderMap map = parse();
Http1HeaderMap map = parse();
assertNull(map.first("Host"));
assertTrue(map.all("Host").isEmpty());
assertTrue(map.all().isEmpty());
@@ -36,14 +36,14 @@ class HeaderMapIndexTest {
@Test
void duplicateHeaderNames_firstReturnsTheFirstOne_allReturnsAllInOrder() {
HeaderMap map = parse("X-Trace: a", "X-Trace: b", "X-Trace: c");
Http1HeaderMap map = parse("X-Trace: a", "X-Trace: b", "X-Trace: c");
assertEquals("a", map.first("X-Trace"));
assertEquals(List.of("a", "b", "c"), map.all("X-Trace"));
}
@Test
void caseVariation_indexHashAndCompareBothIgnoreCase() {
HeaderMap map = parse("X-Custom-Header: value1");
Http1HeaderMap map = parse("X-Custom-Header: value1");
assertEquals("value1", map.first("x-custom-header"));
assertEquals("value1", map.first("X-CUSTOM-HEADER"));
assertEquals("value1", map.first("X-cUsToM-hEaDeR"));
@@ -52,7 +52,7 @@ class HeaderMapIndexTest {
@Test
void similarButDistinctNames_doNotCollideInTheIndex() {
// Names sharing a hash-prefix-adjacent shape must still resolve independently.
HeaderMap map = parse("Accept: a", "Accept-Encoding: b", "Accept-Language: c");
Http1HeaderMap map = parse("Accept: a", "Accept-Encoding: b", "Accept-Language: c");
assertEquals("a", map.first("Accept"));
assertEquals("b", map.first("Accept-Encoding"));
assertEquals("c", map.first("Accept-Language"));
@@ -63,7 +63,7 @@ class HeaderMapIndexTest {
int n = dev.relism.flash.http.Http1Limits.MAX_HEADER_COUNT;
String[] headers = new String[n];
for (int i = 0; i < n; i++) headers[i] = "X-Header-" + i + ": value-" + i;
HeaderMap map = parse(headers);
Http1HeaderMap map = parse(headers);
assertEquals("value-0", map.first("X-Header-0"));
assertEquals("value-" + (n - 1), map.first("X-Header-" + (n - 1)));
@@ -73,7 +73,7 @@ class HeaderMapIndexTest {
@Test
void reset_rebuildsIndexFromScratch_noStaleEntriesFromPreviousRequest() {
HeaderMap map = parse("Host: first-request");
Http1HeaderMap map = parse("Host: first-request");
assertEquals("first-request", map.first("Host"));
assertNull(map.first("X-Only-In-Second"));
@@ -88,7 +88,7 @@ class HeaderMapIndexTest {
void repeatedResetsAcrossVaryingHeaderCounts_shrinkAndGrowSafely() {
// A connection whose successive keep-alive requests have very different header counts
// must never see stale entries from a larger previous request bleed into a smaller one.
HeaderMap map = new HeaderMap();
Http1HeaderMap map = new Http1HeaderMap();
for (int round = 0; round < 5; round++) {
int n = (round % 2 == 0) ? 20 : 2;
String[] headers = new String[n];
@@ -109,7 +109,7 @@ class HeaderMapIndexTest {
// re-trigger index growth (Arrays.copyOf inside ensureIndexCapacity) after the first
// reset() has already sized the arrays for this header count asserted by identity: the
// backing array references must be the exact same objects before and after 100k lookups.
HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4");
Http1HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4");
int[] namesBefore = arrayFieldValue(map, "nameOffsets");
for (int i = 0; i < 100_000; i++) {
@@ -124,11 +124,11 @@ class HeaderMapIndexTest {
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
// EX-05's documented hazard, demonstrated through the actual public API: HeaderMap's
// EX-05's documented hazard, demonstrated through the actual public API: Http1HeaderMap's
// view() pool is sized 4 (VIEW_POOL_SIZE); a 5th call in the same request wraps around
// and silently repositions the object the 1st call returned.
dev.relism.fpr.core.ByteView v1 = null;
HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4", "E: 5");
Http1HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4", "E: 5");
for (String name : new String[]{"A", "B", "C", "D"}) {
dev.relism.fpr.core.ByteView v = map.view(name);
if (v1 == null) v1 = v;
@@ -139,9 +139,9 @@ class HeaderMapIndexTest {
assertEquals('5', v1.byteAt(0)); // v1 is now silently "E"'s value, not "A"'s
}
private static int[] arrayFieldValue(HeaderMap map, String fieldName) {
private static int[] arrayFieldValue(Http1HeaderMap map, String fieldName) {
try {
var field = HeaderMap.class.getDeclaredField(fieldName);
var field = Http1HeaderMap.class.getDeclaredField(fieldName);
field.setAccessible(true);
return (int[]) field.get(map);
} catch (ReflectiveOperationException e) {
@@ -8,15 +8,15 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class HeaderMapTest {
class Http1HeaderMapTest {
// --- helpers ---
private static HeaderMap parse(String... headers) {
private static Http1HeaderMap 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();
Http1HeaderMap map = new Http1HeaderMap();
map.reset(buffer, 0, buffer.length);
return map;
}
@@ -25,21 +25,21 @@ class HeaderMapTest {
@Test
void first_existingHeader() {
HeaderMap map = parse("Host: localhost", "Accept: text/plain");
Http1HeaderMap map = parse("Host: localhost", "Accept: text/plain");
assertEquals("localhost", map.first("Host"));
assertEquals("text/plain", map.first("Accept"));
}
@Test
void first_caseInsensitive() {
HeaderMap map = parse("ConteNT-tYPe: application/json");
Http1HeaderMap map = parse("ConteNT-tYPe: application/json");
assertEquals("application/json", map.first("content-type"));
assertEquals("application/json", map.first("CONTENT-TYPE"));
}
@Test
void first_missingHeader_returnsNull() {
HeaderMap map = parse("Host: localhost");
Http1HeaderMap map = parse("Host: localhost");
assertNull(map.first("Accept"));
}
@@ -47,19 +47,19 @@ class HeaderMapTest {
@Test
void all_multipleValuesByName() {
HeaderMap map = parse("Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
Http1HeaderMap map = parse("Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
assertEquals(List.of("a=1", "b=2"), map.all("Cookie"));
}
@Test
void all_missingHeader_returnsEmptyList() {
HeaderMap map = parse("Host: localhost");
Http1HeaderMap map = parse("Host: localhost");
assertTrue(map.all("Cookie").isEmpty());
}
@Test
void all_returnsAllHeaders() {
HeaderMap map = parse("A: 1", "B: 2");
Http1HeaderMap map = parse("A: 1", "B: 2");
assertEquals(List.of("1", "2"), map.all());
}
@@ -67,7 +67,7 @@ class HeaderMapTest {
@Test
void view_returnsZeroCopyView() {
HeaderMap map = parse("Host: localhost");
Http1HeaderMap map = parse("Host: localhost");
ByteView view = map.view("Host");
assertNotNull(view);
assertEquals(9, view.length());
@@ -77,7 +77,7 @@ class HeaderMapTest {
@Test
void view_missingHeader_returnsNull() {
HeaderMap map = parse("Host: localhost");
Http1HeaderMap map = parse("Host: localhost");
assertNull(map.view("Accept"));
}
@@ -85,7 +85,7 @@ class HeaderMapTest {
@Test
void emptyMap_returnsNullAndEmptyList() {
HeaderMap map = new HeaderMap();
Http1HeaderMap map = new Http1HeaderMap();
assertNull(map.first("Host"));
assertTrue(map.all("Host").isEmpty());
assertTrue(map.all().isEmpty());
@@ -95,7 +95,7 @@ class HeaderMapTest {
@Test
void forEach_visitsEveryHeaderInDeclarationOrder() {
HeaderMap map = parse("Host: localhost", "Accept: text/plain", "Cookie: a=1");
Http1HeaderMap map = parse("Host: localhost", "Accept: text/plain", "Cookie: a=1");
List<String> seen = new java.util.ArrayList<>();
map.forEach((name, value) -> seen.add(toStr(name) + "=" + toStr(value)));
assertEquals(List.of("Host=localhost", "Accept=text/plain", "Cookie=a=1"), seen);
@@ -103,7 +103,7 @@ class HeaderMapTest {
@Test
void forEach_emptyMap_neverInvokesConsumer() {
HeaderMap map = new HeaderMap();
Http1HeaderMap map = new Http1HeaderMap();
map.forEach((name, value) -> fail("must not be called on an empty map"));
}
@@ -111,7 +111,7 @@ class HeaderMapTest {
void forEach_reusesTheSameTwoViewInstancesAcrossEveryHeader() {
// The zero-allocation contract: forEach must reposition two ByteViews in place, not
// allocate a fresh pair per header same instances across all three calls here.
HeaderMap map = parse("A: 1", "B: 2", "C: 3");
Http1HeaderMap map = parse("A: 1", "B: 2", "C: 3");
List<ByteView> names = new java.util.ArrayList<>();
List<ByteView> values = new java.util.ArrayList<>();
map.forEach((name, value) -> { names.add(name); values.add(value); });
@@ -161,4 +161,46 @@ class RequestBodyTest {
body.drain();
assertEquals(0, socket.available());
}
// --- EX-22/EX-23: pooled instance, repositioned via reset() --------------------
@Test
void reset_repositionsSamePooledInstance_overSuccessiveRequests() throws IOException {
RequestBody body = new RequestBody(); // pooled ctor — no I/O configured yet
byte[] first = "first".getBytes(StandardCharsets.UTF_8);
body.reset(new ByteArrayInputStream(first), 5, new byte[0], 0, 0);
assertArrayEquals(first, body.bytes());
byte[] second = "second-request".getBytes(StandardCharsets.UTF_8);
body.reset(new ByteArrayInputStream(second), second.length, new byte[0], 0, 0);
assertArrayEquals(second, body.bytes(), "reset() must not leak the previous request's resolved body");
}
@Test
void stream_reusesTheSameBoundedStreamInstance_acrossResets() throws IOException {
RequestBody body = new RequestBody();
body.reset(new ByteArrayInputStream("one".getBytes(StandardCharsets.UTF_8)), 3, new byte[0], 0, 0);
InputStream stream1 = body.stream();
assertEquals("one", new String(stream1.readAllBytes(), StandardCharsets.UTF_8));
body.reset(new ByteArrayInputStream("two".getBytes(StandardCharsets.UTF_8)), 3, new byte[0], 0, 0);
InputStream stream2 = body.stream();
assertSame(stream1, stream2, "EX-23: stream() must reposition the one pooled BoundedBufferedInputStream, not allocate a new one per request");
assertEquals("two", new String(stream2.readAllBytes(), StandardCharsets.UTF_8));
}
@Test
void drain_reusesTheSameDrainBuffer_acrossChunkedResets() throws IOException {
RequestBody body = new RequestBody();
body.reset(new ByteArrayInputStream("chunk one".getBytes(StandardCharsets.UTF_8)), -1L, null, 0, 0);
body.drain();
ByteArrayInputStream secondSocket = new ByteArrayInputStream("chunk two".getBytes(StandardCharsets.UTF_8));
body.reset(secondSocket, -1L, null, 0, 0);
body.drain();
assertEquals(0, secondSocket.available(), "drain() must fully consume the second request's chunked body too");
}
}
@@ -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();
Http1HeaderMap headers = new Http1HeaderMap();
RequestLine rl = new RequestLine(HttpMethod.GET, path, query, proto, headers);
@@ -0,0 +1,103 @@
package dev.relism.flash.models;
import dev.relism.flash.http.HttpMethod;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-22}: {@link Request} is pooled <em>per connection</em> (one instance owned by
* {@code RequestParser}, repositioned via {@link Request#forParsed} for every request on that
* connection) — not via a shared cross-connection pool. The plan's own safety-check wording
* ("connection A's {@code Authorization} header must never be visible on connection B") describes
* a threat model that does not structurally apply to this design: two different connections
* never share a {@code Request} instance at all (each owns its own {@code RequestParser}, hence
* its own {@code Request}) — see {@code DECISIONS.md} for the pooling-granularity decision this
* follows from. The real, applicable threat this class actually tests: request <em>N+1</em> on
* the *same* keep-alive connection must never see stale data left over from request <em>N</em>,
* since those two requests genuinely do share one {@code Request} instance.
*/
class RequestPoolingTest {
private static ByteView viewOf(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return new ByteView() {
public int length() { return bytes.length; }
public byte byteAt(int idx) { return bytes[idx]; }
};
}
private static Http1HeaderMap headersOf(String... rawLines) {
StringBuilder sb = new StringBuilder();
for (String line : rawLines) sb.append(line).append("\r\n");
byte[] buf = sb.toString().getBytes(StandardCharsets.UTF_8);
Http1HeaderMap map = new Http1HeaderMap();
map.reset(buf, 0, buf.length);
return map;
}
@Test
void forParsed_reusesTheSamePooledInstance_neverAllocatesANewOne() {
Request pooled = new Request();
RequestLine line1 = new RequestLine(HttpMethod.GET, viewOf("/a"), null, viewOf("HTTP/1.1"), headersOf());
Request r1 = Request.forParsed(pooled, line1, RequestBody.empty(), null, null);
assertSame(pooled, r1);
RequestLine line2 = new RequestLine(HttpMethod.POST, viewOf("/b"), null, viewOf("HTTP/1.1"), headersOf());
Request r2 = Request.forParsed(pooled, line2, RequestBody.empty(), null, null);
assertSame(pooled, r2);
assertSame(r1, r2, "the same pooled instance must be returned for every request on one connection");
}
@Test
void secondRequest_onSameConnection_doesNotSeeFirstRequestsAuthorizationHeader() {
Request pooled = new Request();
RequestLine first = new RequestLine(HttpMethod.GET, viewOf("/secure"), null, viewOf("HTTP/1.1"),
headersOf("Authorization: Bearer super-secret-token-A"));
Request r1 = Request.forParsed(pooled, first, RequestBody.empty(), null, null);
assertEquals("Bearer super-secret-token-A", r1.header("Authorization"));
// A second request on the same keep-alive connection, with no Authorization header at all.
RequestLine second = new RequestLine(HttpMethod.GET, viewOf("/public"), null, viewOf("HTTP/1.1"),
headersOf("Host: example.com"));
Request r2 = Request.forParsed(pooled, second, RequestBody.empty(), null, null);
assertNull(r2.header("Authorization"), "the second request must not see the first request's Authorization header");
assertNull(r2.header("authorization"));
for (String value : r2.headers()) {
assertFalse(value.contains("super-secret-token-A"), "leaked secret found in: " + value);
}
}
@Test
void secondRequest_doesNotSeeFirstRequestsPathParams() {
Request pooled = new Request();
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/users/123"), null, viewOf("HTTP/1.1"), headersOf());
Request r1 = Request.forParsed(pooled, line, RequestBody.empty(), null, null);
PathParams.inject(r1, new PathParams(viewOf("/users/123"), new String[]{"id"}, new int[]{7}, new int[]{3}));
assertEquals("123", r1.param("id"));
RequestLine line2 = new RequestLine(HttpMethod.GET, viewOf("/health"), null, viewOf("HTTP/1.1"), headersOf());
Request r2 = Request.forParsed(pooled, line2, RequestBody.empty(), null, null);
assertNull(r2.param("id"), "path params from the previous request on this connection must not leak");
assertNull(r2.getPathParams());
}
@Test
void secondRequest_doesNotSeeFirstRequestsCachedPathOrQueryParams() {
Request pooled = new Request();
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/first"), viewOf("token=abc"), viewOf("HTTP/1.1"), headersOf());
Request r1 = Request.forParsed(pooled, line, RequestBody.empty(), null, null);
assertEquals("/first", r1.path());
assertEquals("abc", r1.query("token"));
RequestLine line2 = new RequestLine(HttpMethod.GET, viewOf("/second"), null, viewOf("HTTP/1.1"), headersOf());
Request r2 = Request.forParsed(pooled, line2, RequestBody.empty(), null, null);
assertEquals("/second", r2.path(), "cachedPath from the previous request must not leak");
assertNull(r2.query("token"), "query params from the previous request must not leak");
}
}
@@ -0,0 +1,128 @@
package dev.relism.flash.models;
import dev.relism.flash.http.HttpMethod;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-22}'s dev-mode use-after-recycle guard. Exercises the poisoning check directly via
* {@code Request.setPoisoningEnabledForTesting} rather than the real {@code Flash.DEV} flag,
* which is a {@code static final boolean} fixed once at JVM startup and cannot be toggled by an
* individual test — see that field's own comment in {@code Request.java}.
*/
class RequestRecycleGuardTest {
@AfterEach
void restoreProductionDefault() {
// Never leak the test override into other test classes sharing this JVM/fork.
Request.setPoisoningEnabledForTesting(false);
}
private static ByteView viewOf(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return new ByteView() {
public int length() { return bytes.length; }
public byte byteAt(int idx) { return bytes[idx]; }
};
}
private static Request active() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/x"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
return new Request(line, new byte[0]);
}
@Test
void poisoningDisabled_recycledRequestStillAccessible() {
Request.setPoisoningEnabledForTesting(false);
Request r = active();
r.recycle();
assertDoesNotThrow(r::method, "poisoning disabled (production default) must never throw");
}
@Test
void poisoningEnabled_freshRequest_accessibleNormally() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
assertDoesNotThrow(r::path);
assertDoesNotThrow(() -> r.header("Host"));
assertDoesNotThrow(r::method);
}
@Test
void poisoningEnabled_afterRecycle_methodThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, r::method);
}
@Test
void poisoningEnabled_afterRecycle_pathThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, r::path);
}
@Test
void poisoningEnabled_afterRecycle_headerThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, () -> r.header("Host"));
}
@Test
void poisoningEnabled_afterRecycle_paramThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, () -> r.param("id"));
}
@Test
void poisoningEnabled_afterRecycle_queryThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, () -> r.query("q"));
}
@Test
void poisoningEnabled_afterRecycle_remoteAddressThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, r::remoteAddress);
}
@Test
void poisoningEnabled_afterRecycle_isSecureThrows() {
Request.setPoisoningEnabledForTesting(true);
Request r = active();
r.recycle();
assertThrows(IllegalStateException.class, r::isSecure);
}
@Test
void reusedAfterReset_becomesAccessibleAgain() {
Request.setPoisoningEnabledForTesting(true);
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/first"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
Request r = new Request(line, new byte[0]);
r.recycle();
assertThrows(IllegalStateException.class, r::path);
// Simulate the connection loop pulling this pooled instance back out for the next
// request: Request.forParsed's reset() call re-activates it.
RequestLine line2 = new RequestLine(HttpMethod.GET, viewOf("/second"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
Request reused = Request.forParsed(r, line2, RequestBody.empty(), null, null);
assertSame(r, reused, "forParsed must reposition the same pooled instance, not allocate a new one");
assertDoesNotThrow(reused::path);
assertEquals("/second", reused.path());
}
}
@@ -26,7 +26,7 @@ class RequestTest {
@Test
void request_creationAndAccessors() {
HeaderMap headers = new HeaderMap();
Http1HeaderMap headers = new Http1HeaderMap();
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/path"), viewOf("q=1"), viewOf("HTTP/1.1"), headers);
byte[] body = "body".getBytes(StandardCharsets.UTF_8);
@@ -43,7 +43,7 @@ class RequestTest {
@Test
void header_delegatesToRequestLine() {
byte[] buffer = "Host: localhost\r\n".getBytes(StandardCharsets.UTF_8);
HeaderMap headers = new HeaderMap();
Http1HeaderMap headers = new Http1HeaderMap();
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]);
@@ -57,7 +57,7 @@ class RequestTest {
@Test
void param_lazyGet() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
Request r = new Request(line, new byte[0]);
assertNull(r.param("id"));
@@ -70,7 +70,7 @@ class RequestTest {
@Test
void query_lazyGet_fromQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new HeaderMap());
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new Http1HeaderMap());
Request r = new Request(line, new byte[0]);
assertEquals("1", r.query("a"));
@@ -80,7 +80,7 @@ class RequestTest {
@Test
void query_lazyGet_nullQueryString() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
Request r = new Request(line, new byte[0]);
assertNull(r.query("a"));
@@ -0,0 +1,75 @@
package dev.relism.flash.models;
import dev.relism.flash.http.ContentType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** {@code EX-21}: mirrors {@code RequestPoolingTest} for {@link Response}. */
class ResponsePoolingTest {
@Test
void reset_returnsSameInstanceAndClearsPreviousState() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.header("X-Trace", "abc123").status(201).body("first body");
assertEquals(1, r.getHeaders().size());
Response reset = r.reset(200, ContentType.JSON);
assertSame(r, reset, "reset() must reposition the same instance, not allocate a new one");
assertEquals(200, reset.getStatusCode());
assertNull(reset.getBody(), "body from the previous cycle must not leak");
assertTrue(reset.getHeaders().isEmpty(), "headers from the previous cycle must not leak");
assertArrayEquals(ContentType.JSON.getBytes(), reset.getContentType());
}
@Test
void secondCycle_doesNotSeeFirstCyclesCustomHeader() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.header("X-Secret", "leaked-if-broken");
assertEquals(1, r.getHeaders().size());
r.reset(200, ContentType.TEXT_PLAIN);
r.header("X-Public", "fine");
assertEquals(1, r.getHeaders().size());
String only = new String(r.getHeaders().get(0));
assertTrue(only.contains("X-Public"));
assertFalse(only.contains("X-Secret"), "stale header from the previous cycle leaked: " + only);
}
@Test
void secondCycle_reusesHeaderRegionAcrossManyHeaders_staysCorrect() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
for (int cycle = 0; cycle < 5; cycle++) {
r.reset(200, ContentType.TEXT_PLAIN);
for (int i = 0; i < 10; i++) {
r.header("X-Cycle" + cycle + "-H" + i, "v" + i);
}
assertEquals(10, r.getHeaders().size(), "cycle " + cycle);
String last = new String(r.getHeaders().get(9));
assertTrue(last.contains("X-Cycle" + cycle + "-H9: v9"), "cycle " + cycle + ": " + last);
}
}
@Test
void mixedStructuredAndRawHeaders_preserveInsertionOrder() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.header("A", "1");
r.header("B-raw: 2\r\n".getBytes());
r.header("C", "3");
var headers = r.getHeaders();
assertEquals(3, headers.size());
assertEquals("A: 1\r\n", new String(headers.get(0)));
assertEquals("B-raw: 2\r\n", new String(headers.get(1)));
assertEquals("C: 3\r\n", new String(headers.get(2)));
}
@Test
void preEncodedHeader_roundTripsThroughGetHeaders() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
PreEncodedHeader h = new PreEncodedHeader("X-Static", "value");
r.header(h);
assertEquals("X-Static: value\r\n", new String(r.getHeaders().get(0)));
}
}
@@ -0,0 +1,58 @@
package dev.relism.flash.models;
import dev.relism.flash.http.ContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** {@code EX-21}'s dev-mode use-after-recycle guard — mirrors {@code RequestRecycleGuardTest}. */
class ResponseRecycleGuardTest {
@AfterEach
void restoreProductionDefault() {
Response.setPoisoningEnabledForTesting(false);
}
@Test
void poisoningDisabled_recycledResponseStillAccessible() {
Response.setPoisoningEnabledForTesting(false);
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.recycle();
assertDoesNotThrow(r::getStatusCode);
}
@Test
void poisoningEnabled_afterRecycle_getStatusCodeThrows() {
Response.setPoisoningEnabledForTesting(true);
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.recycle();
assertThrows(IllegalStateException.class, r::getStatusCode);
}
@Test
void poisoningEnabled_afterRecycle_headerThrows() {
Response.setPoisoningEnabledForTesting(true);
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.recycle();
assertThrows(IllegalStateException.class, () -> r.header("X", "Y"));
}
@Test
void poisoningEnabled_afterRecycle_bodyThrows() {
Response.setPoisoningEnabledForTesting(true);
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.recycle();
assertThrows(IllegalStateException.class, () -> r.body("x"));
}
@Test
void poisoningEnabled_afterReset_accessibleAgain() {
Response.setPoisoningEnabledForTesting(true);
Response r = new Response(200, ContentType.TEXT_PLAIN);
r.recycle();
assertThrows(IllegalStateException.class, r::getStatusCode);
r.reset(200, ContentType.TEXT_PLAIN);
assertDoesNotThrow(r::getStatusCode);
}
}
@@ -0,0 +1,73 @@
package dev.relism.flash.models;
import dev.relism.flash.http.ContentType;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ResponseSerializerTest {
private static List<String> collect(Response r) {
List<String> fields = new ArrayList<>();
ResponseSerializer.forEachField(r, (nameBuf, nameOff, nameLen, valueBuf, valueOff, valueLen) ->
fields.add(new String(nameBuf, nameOff, nameLen, StandardCharsets.US_ASCII)
+ "=" + new String(valueBuf, valueOff, valueLen, StandardCharsets.US_ASCII)));
return fields;
}
@Test
void contentTypeFirst_thenCustomHeadersInOrder() {
Response r = new Response(200, ContentType.JSON);
r.header("X-A", "1").header("X-B", "2");
assertEquals(List.of("Content-Type=application/json", "X-A=1", "X-B=2"), collect(r));
}
@Test
void contentTypeNone_isSkipped_notEmptyValue() {
Response r = new Response(200, ContentType.NONE);
r.header("X-Only", "here");
assertEquals(List.of("X-Only=here"), collect(r));
}
@Test
void noHeadersAtAll_onlyContentType() {
Response r = new Response(200, ContentType.TEXT_PLAIN);
assertEquals(List.of("Content-Type=text/plain"), collect(r));
}
@Test
void rawPreEncodedHeaderBytes_areExcludedFromEnumeration() {
// header(byte[]) has no recoverable (name, value) structure -- ResponseSerializer must
// skip it (Http1ResponseWriter still renders it, via writeHeaders, just not through this
// protocol-neutral path).
Response r = new Response(200, ContentType.NONE);
r.header("X-Structured", "yes");
r.header("X-Raw: no-structure\r\n".getBytes());
assertEquals(List.of("X-Structured=yes"), collect(r));
}
@Test
void preEncodedHeaderObject_isIncluded_withStructure() {
Response r = new Response(200, ContentType.NONE);
r.header(new PreEncodedHeader("X-Boot", "constant"));
assertEquals(List.of("X-Boot=constant"), collect(r));
}
@Test
void zeroAllocation_byteRangesAreSlicesOfResponsesOwnBuffers_notCopies() {
Response r = new Response(200, ContentType.NONE);
r.header("X-A", "value-a");
byte[][] captured = new byte[2][];
ResponseSerializer.forEachField(r, (nameBuf, nameOff, nameLen, valueBuf, valueOff, valueLen) -> {
captured[0] = nameBuf;
captured[1] = valueBuf;
});
// Both slices must reference the SAME backing array (the response's own header region) --
// proves no copy was made to hand the field to the consumer.
assertSame(captured[0], captured[1]);
}
}
@@ -134,4 +134,35 @@ class ResponseTest {
void getHeaders_emptyWhenNoneAdded() {
assertTrue(new Response(200, new byte[0], ContentType.TEXT_PLAIN).getHeaders().isEmpty());
}
// --- EX-43: response header budget (Phase 6 zero-alloc DoD) ---
@Test
void header_exceedingMaxCount_throws() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
for (int i = 0; i < dev.relism.flash.http.Http1Limits.MAX_RESPONSE_HEADER_COUNT; i++) {
r.header("X-" + i, "v");
}
assertThrows(IllegalStateException.class, () -> r.header("one-too-many", "v"));
}
@Test
void header_exceedingMaxRegionBytes_throws() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
String bigValue = "v".repeat(1024);
assertThrows(IllegalStateException.class, () -> {
// Each call adds ~1024 bytes; comfortably crosses MAX_RESPONSE_HEADER_BYTES well
// before MAX_RESPONSE_HEADER_COUNT would trigger first.
for (int i = 0; i < dev.relism.flash.http.Http1Limits.MAX_RESPONSE_HEADER_COUNT; i++) {
r.header("X-" + i, bigValue);
}
});
}
@Test
void header_withinBudget_stillWorksNormally() {
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
r.header("X-Foo", "bar");
assertEquals(1, r.getHeaders().size());
}
}
@@ -1,7 +1,7 @@
package dev.relism.flash.routing.routers.fastpathrouter;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.HeaderMap;
import dev.relism.flash.models.Http1HeaderMap;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.RequestLine;
@@ -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 Http1HeaderMap()
);
return new Request(line, new byte[0]);
}
@@ -28,6 +28,31 @@ class FastPathViewsTest {
assertThrows(IndexOutOfBoundsException.class, () -> view.byteAt(10));
}
// --- EX-42: reset() repositions the same instance, zero allocation ---------
@Test
void requestByteView_reset_repositionsSameInstance() {
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(SHARED_BUFFER, 4, 10);
assertEquals("/api/users", view.toString());
byte[] other = "PUT /orders/9 HTTP/1.1".getBytes(StandardCharsets.UTF_8);
view.reset(other, 4, 8);
assertEquals(8, view.length());
assertEquals("/orders/", view.toString());
}
@Test
void requestByteView_reset_updatesArrayBackedByteViewAccessors() {
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(SHARED_BUFFER, 0, 3);
byte[] other = "zzHELLOzz".getBytes(StandardCharsets.UTF_8);
view.reset(other, 2, 5);
assertSame(other, view.array());
assertEquals(2, view.offset());
assertEquals(5, view.length());
assertEquals("HELLO", view.toString());
}
// --- MethodPathByteView ---
@Test
@@ -55,4 +55,38 @@ class ByteTemplateTest {
byte[] result = tpl.render("v1", "1", "v2", "2");
assertEquals("A12B", new String(result, StandardCharsets.UTF_8));
}
// --- EX-28: renderInto(buffer, offset, ...) ------------------------------------
@Test
void renderInto_writesAtOffset_andReturnsLength() {
ByteTemplate tpl = new ByteTemplate("Hello {{name}}!");
byte[] buffer = new byte[64];
int len = tpl.renderInto(buffer, 5, "name", "World");
assertEquals("Hello World!".length(), len);
assertEquals("Hello World!", new String(buffer, 5, len, StandardCharsets.UTF_8));
}
@Test
void renderInto_repeatedPlaceholder_fillsEveryOccurrence() {
ByteTemplate tpl = new ByteTemplate("{{var}} == {{var}}");
byte[] buffer = new byte[32];
int len = tpl.renderInto(buffer, 0, "var", "test");
assertEquals("test == test", new String(buffer, 0, len, StandardCharsets.UTF_8));
}
@Test
void renderInto_bufferTooSmall_throws() {
ByteTemplate tpl = new ByteTemplate("Hello {{name}}!");
byte[] buffer = new byte[5];
assertThrows(IndexOutOfBoundsException.class, () -> tpl.renderInto(buffer, 0, "name", "World"));
}
@Test
void renderInto_negativeOffset_throws() {
ByteTemplate tpl = new ByteTemplate("Hi {{name}}");
byte[] buffer = new byte[32];
assertThrows(IndexOutOfBoundsException.class, () -> tpl.renderInto(buffer, -1, "name", "X"));
}
}
@@ -1,7 +1,7 @@
package dev.relism.flash.template;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.HeaderMap;
import dev.relism.flash.models.Http1HeaderMap;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestLine;
import dev.relism.flash.routing.routers.fastpathrouter.FastPathViews;
@@ -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());
RequestLine line = new RequestLine(HttpMethod.GET, pathView, null, protoView, new Http1HeaderMap());
return new Request(line, new byte[0]);
}
@@ -15,8 +15,9 @@ class ScratchPoolTest {
ConnectionScratch scratch = pool.acquire();
assertNotNull(scratch);
assertNotNull(scratch.sha1);
assertEquals(ConnectionScratch.DECIMAL_BUFFER_SIZE, scratch.decimalBuffer.length);
assertEquals(ConnectionScratch.RELAY_BUFFER_SIZE, scratch.relayBuffer.length);
assertNotNull(scratch.responseHead);
assertEquals(0, scratch.responseHead.length());
}
@Test
@@ -1,7 +1,7 @@
package dev.relism.flash.websocket;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.HeaderMap;
import dev.relism.flash.models.Http1HeaderMap;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestLine;
import dev.relism.fpr.core.ByteView;
@@ -25,7 +25,7 @@ class WebSocketSessionTest {
@Test
void request_returnsWhatWasPassedToConstructor() {
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/chat"), null, viewOf("HTTP/1.1"), new HeaderMap());
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/chat"), null, viewOf("HTTP/1.1"), new Http1HeaderMap());
Request req = new Request(line, new byte[0]);
WebSocketSession session = new WebSocketSession(
new ByteArrayInputStream(new byte[0]), new ByteArrayOutputStream(), 64, req, false);