lib/http/src/message.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const chunked = @import("chunk.zig");
4 const field = @import("field/root.zig");
5
6 pub const ParseError = error{
7 InvalidMethod,
8 InvalidVersion,
9 InvalidRequestLine,
10 InvalidHeader,
11 IncompleteRequest,
12 HeaderTooLong,
13 TooManyHeaders,
14 BodyTooLarge,
15 };
16
17 pub const default_request_header_count: usize = 100;
18 pub const default_request_header_line_bytes: usize = 8192;
19 pub const default_request_body_bytes: usize = 64 * 1024;
20
21 pub const RequestHeader = struct {
22 name: []const u8,
23 value: []const u8,
24 };
25
26 pub const RequestHeaders = struct {
27 entries: []const RequestHeader,
28
29 pub fn get(self: RequestHeaders, name: []const u8) ?[]const u8 {
30 var index = self.entries.len;
31 while (index != 0) {
32 index -= 1;
33 const entry = self.entries[index];
34 if (std.ascii.eqlIgnoreCase(entry.name, name)) return entry.value;
35 }
36 return null;
37 }
38
39 pub fn count(self: RequestHeaders) usize {
40 return self.entries.len;
41 }
42 };
43
44 pub const RequestLimits = struct {
45 request_count: usize,
46 header_count_per_request: usize,
47 header_line_bytes: usize,
48 body_bytes_per_request: usize,
49 };
50
51 pub const RequestCapacity = struct {
52 request_count: usize,
53 header_count_per_request: usize,
54 header_line_bytes: usize,
55 body_bytes_per_request: usize,
56 header_count: usize,
57 header_bytes: usize,
58 body_bytes: usize,
59 storage_bytes: usize,
60
61 pub fn derive(limits: RequestLimits) error{CapacityOverflow}!RequestCapacity {
62 const header_count = try alloc_phase.capacity.mul(
63 usize,
64 limits.request_count,
65 limits.header_count_per_request,
66 );
67 const header_bytes = try alloc_phase.capacity.mul(
68 usize,
69 header_count,
70 @sizeOf(RequestHeader),
71 );
72 const body_bytes = try alloc_phase.capacity.mul(
73 usize,
74 limits.request_count,
75 limits.body_bytes_per_request,
76 );
77 const storage_bytes = try alloc_phase.capacity.add(
78 usize,
79 header_bytes,
80 body_bytes,
81 );
82 return .{
83 .request_count = limits.request_count,
84 .header_count_per_request = limits.header_count_per_request,
85 .header_line_bytes = limits.header_line_bytes,
86 .body_bytes_per_request = limits.body_bytes_per_request,
87 .header_count = header_count,
88 .header_bytes = header_bytes,
89 .body_bytes = body_bytes,
90 .storage_bytes = storage_bytes,
91 };
92 }
93 };
94
95 pub const RequestStorageExhaustion = error{RequestCapacityExceeded};
96
97 const RequestStorageLimits = RequestLimits;
98 const RequestStorageCapacity = RequestCapacity;
99
100 pub const RequestScratch = struct {
101 headers: []RequestHeader,
102 body: []u8,
103 header_line_bytes: usize,
104 };
105
106 pub const RequestStorage = struct {
107 phase: alloc_phase.capacity.Phase,
108 capacity: RequestStorageCapacity,
109 bytes: []align(@alignOf(RequestHeader)) u8,
110
111 pub const Limits: type = RequestStorageLimits;
112 pub const Capacity: type = RequestStorageCapacity;
113 pub const Exhaustion: type = RequestStorageExhaustion;
114 pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};
115
116 pub const claim: alloc_phase.capacity.Declaration = .{
117 .source = .{
118 .id = "http.request_storage",
119 .kind = .phase_static,
120 .limit_source = .caller,
121 .storage = .{
122 .covered = &.{
123 .{
124 .id = "fixed_parsed_header_entry_region_for_every_request_slot",
125 .lifetime = .steady,
126 .detail = "fixed parsed-header entry region for every request slot",
127 },
128 .{
129 .id = "fixed_decoded_chunked_body_byte_region_for_every_request_slot",
130 .lifetime = .steady,
131 .detail = "fixed decoded chunked-body byte region for every request slot",
132 },
133 },
134 .excluded = &.{
135 "connection wire input and borrowed content-length bodies",
136 "response output, handlers, clients, TLS, sockets, and kernel queues",
137 },
138 },
139 .capacity = .{
140 .inputs = &.{
141 alloc_phase.capacity.bindInput(Limits, "request_count", "request_count"),
142 alloc_phase.capacity.bindInput(Limits, "header_count_per_request", "header_count_per_request"),
143 alloc_phase.capacity.bindInput(Limits, "body_bytes_per_request", "body_bytes_per_request"),
144 },
145 .type_selectors = &.{
146 alloc_phase.capacity.bindType(RequestHeader, "requestheader"),
147 },
148 .nodes = &.{
149 .{ .input = 0 },
150 .{ .input = 1 },
151 .{ .product = .{ .left = 0, .right = 1 } },
152 .{ .constant = 1 },
153 .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 0 } } },
154 .{ .product = .{ .left = 2, .right = 4 } },
155 .{ .input = 2 },
156 .{ .product = .{ .left = 0, .right = 6 } },
157 .{ .add = .{ .left = 5, .right = 7 } },
158 },
159 .assertions = &.{.{
160 .scope = .closure_total,
161 .measure = .retained,
162 .relation = .exact,
163 .expression = 8,
164 }},
165 },
166 .overload = .{
167 .kind = .reject_before_mutation,
168 .detail = "header, line, and body surveys reject max plus one before populating request storage",
169 },
170 .risks = .{
171 .transitive = .{
172 .status = .witnessed,
173 .detail = "request and chunk surveys allocate no storage after activation",
174 },
175 .foreign = .{
176 .status = .excluded,
177 .detail = "request parsing transforms caller-owned memory without I/O",
178 },
179 },
180 .obligations = &.{
181 .{ .key = "http_request_capacity", .role = .capacity_model },
182 .{ .key = "http_request_oom_retry", .role = .custom },
183 .{ .key = "http_request_partition", .role = .custom },
184 .{ .key = "http_request_sealed", .role = .transitive_risk },
185 .{ .key = "http_request_atomic_overload", .role = .overload },
186 .{ .key = "http_request_atomic_foreign_risk", .role = .foreign_risk },
187 .{ .key = "http_request_boundary", .role = .custom },
188 .{ .key = "http_request_pipeline", .role = .custom },
189 },
190 },
191 .bindings = .{
192 .owner = @This(),
193 .seal = .{
194 .family = alloc_phase.capacity.selector(@This().activate),
195 .premise = .{
196 .class = .checked_semantic_fact,
197 .authority = .checker,
198 },
199 },
200 .teardown = .{
201 .family = alloc_phase.capacity.selector(@This().deinit),
202 .premise = .{
203 .class = .checked_semantic_fact,
204 .authority = .checker,
205 },
206 },
207 },
208 };
209
210 pub fn init(
211 allocator: std.mem.Allocator,
212 limits: RequestStorageLimits,
213 ) InitError!RequestStorage {
214 const capacity = try RequestStorageCapacity.derive(limits);
215 const bytes = if (capacity.storage_bytes == 0)
216 @as([]align(@alignOf(RequestHeader)) u8, &.{})
217 else
218 try allocator.alignedAlloc(
219 u8,
220 .of(RequestHeader),
221 capacity.storage_bytes,
222 );
223 return .{
224 .phase = .initialization,
225 .capacity = capacity,
226 .bytes = bytes,
227 };
228 }
229
230 pub fn activate(self: *RequestStorage) void {
231 std.debug.assert(self.phase == .initialization);
232 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
233 self.phase = .steady;
234 }
235
236 pub fn request(self: *RequestStorage, index: usize) Exhaustion!RequestScratch {
237 std.debug.assert(self.phase == .steady);
238 if (index >= self.capacity.request_count) return error.RequestCapacityExceeded;
239 const all_headers = std.mem.bytesAsSlice(
240 RequestHeader,
241 self.bytes[0..self.capacity.header_bytes],
242 );
243 const header_start = index * self.capacity.header_count_per_request;
244 const body_start = self.capacity.header_bytes +
245 index * self.capacity.body_bytes_per_request;
246 return .{
247 .headers = all_headers[header_start..][0..self.capacity.header_count_per_request],
248 .body = self.bytes[body_start..][0..self.capacity.body_bytes_per_request],
249 .header_line_bytes = self.capacity.header_line_bytes,
250 };
251 }
252
253 pub fn deinit(self: *RequestStorage, allocator: std.mem.Allocator) void {
254 std.debug.assert(self.phase != .teardown);
255 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
256 self.phase = .teardown;
257 if (self.bytes.len != 0) allocator.free(self.bytes);
258 self.bytes = &.{};
259 }
260 };
261
262 comptime {
263 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(RequestStorage);
264 }
265
266 pub const RequestParseResult = struct {
267 request: Request,
268 consumed: usize,
269 };
270
271 pub const Request = struct {
272 method: Method,
273 path: []const u8,
274 version: Version,
275 headers: RequestHeaders,
276 body: ?[]const u8,
277
278 pub const Method = enum {
279 GET,
280 POST,
281 PUT,
282 DELETE,
283 HEAD,
284 OPTIONS,
285 PATCH,
286 CONNECT,
287 TRACE,
288 };
289
290 pub const Version = enum {
291 http_1_0,
292 http_1_1,
293 };
294
295 pub const QueryIterator = struct {
296 pub const Param = struct {
297 key: []const u8,
298 value: []const u8,
299 };
300
301 query: ?[]const u8,
302 pos: usize = 0,
303
304 pub fn next(self: *QueryIterator) ?Param {
305 const q = self.query orelse return null;
306 while (self.pos < q.len) {
307 const remaining = q[self.pos..];
308 const param_end = std.mem.indexOf(u8, remaining, "&") orelse remaining.len;
309 const param = remaining[0..param_end];
310 self.pos += param_end + 1;
311
312 if (param.len == 0) continue;
313
314 if (std.mem.indexOf(u8, param, "=")) |eq| {
315 return .{
316 .key = param[0..eq],
317 .value = if (eq + 1 < param.len) param[eq + 1 ..] else "",
318 };
319 } else {
320 return .{ .key = param, .value = "" };
321 }
322 }
323 return null;
324 }
325
326 pub fn get(self: *QueryIterator, key: []const u8) ?[]const u8 {
327 var iter = QueryIterator{ .query = self.query, .pos = 0 };
328 while (iter.next()) |param| {
329 if (std.mem.eql(u8, param.key, key)) {
330 return param.value;
331 }
332 }
333 return null;
334 }
335 };
336
337 pub fn parse(scratch: RequestScratch, data: []const u8) ParseError!RequestParseResult {
338 const survey = try surveyRequest(scratch, data);
339
340 var header_count: usize = 0;
341 var pos = survey.first_line_end + 2;
342 while (pos < survey.header_end) {
343 const line_end = std.mem.indexOf(u8, data[pos .. survey.header_end + 2], "\r\n") orelse {
344 unreachable;
345 };
346 const line = data[pos..][0..line_end];
347 const parsed = field.parseLine(line).?;
348 scratch.headers[header_count] = .{
349 .name = parsed.name,
350 .value = parsed.value,
351 };
352 header_count += 1;
353 pos += line_end + 2;
354 }
355 std.debug.assert(header_count == survey.header_count);
356
357 const body: ?[]const u8 = switch (survey.body) {
358 .none => null,
359 .borrowed => |borrowed| data[borrowed.start..][0..borrowed.length],
360 .chunked => |capacity| blk: {
361 const decoded = chunked.decodeSurveyedInto(
362 scratch.body,
363 data[survey.body_start..],
364 capacity,
365 ) catch unreachable;
366 std.debug.assert(decoded.body.len == capacity.decoded_bytes);
367 std.debug.assert(decoded.consumed == capacity.consumed);
368 break :blk decoded.body;
369 },
370 };
371
372 return .{
373 .request = .{
374 .method = survey.method,
375 .path = survey.path,
376 .version = survey.version,
377 .headers = .{ .entries = scratch.headers[0..header_count] },
378 .body = body,
379 },
380 .consumed = survey.consumed,
381 };
382 }
383
384 fn parseMethod(s: []const u8) ?Method {
385 const methods = .{
386 .{ "GET", Method.GET },
387 .{ "POST", Method.POST },
388 .{ "PUT", Method.PUT },
389 .{ "DELETE", Method.DELETE },
390 .{ "HEAD", Method.HEAD },
391 .{ "OPTIONS", Method.OPTIONS },
392 .{ "PATCH", Method.PATCH },
393 .{ "CONNECT", Method.CONNECT },
394 .{ "TRACE", Method.TRACE },
395 };
396 inline for (methods) |pair| {
397 if (std.mem.eql(u8, s, pair[0])) return pair[1];
398 }
399 return null;
400 }
401
402 fn parseVersion(s: []const u8) ?Version {
403 if (std.mem.eql(u8, s, "HTTP/1.1")) return .http_1_1;
404 if (std.mem.eql(u8, s, "HTTP/1.0")) return .http_1_0;
405 return null;
406 }
407
408 fn findHeaderEnd(data: []const u8) ?usize {
409 if (data.len < 4) return null;
410 var i: usize = 0;
411 while (i + 3 < data.len) : (i += 1) {
412 if (data[i] == '\r' and data[i + 1] == '\n' and data[i + 2] == '\r' and data[i + 3] == '\n') {
413 return i;
414 }
415 }
416 return null;
417 }
418
419 pub fn isWebSocketUpgrade(self: *const Request) bool {
420 if (self.method != .GET or self.version != .http_1_1) return false;
421
422 const upgrade = self.headers.get("Upgrade") orelse return false;
423 const connection = self.headers.get("Connection") orelse return false;
424
425 return containsHeaderToken(upgrade, "websocket") and
426 containsHeaderToken(connection, "upgrade");
427 }
428
429 pub fn getWebSocketKey(self: *const Request) ?[]const u8 {
430 return self.headers.get("Sec-WebSocket-Key");
431 }
432
433 pub fn pathOnly(self: *const Request) []const u8 {
434 if (std.mem.indexOf(u8, self.path, "?")) |idx| {
435 return self.path[0..idx];
436 }
437 return self.path;
438 }
439
440 pub fn queryString(self: *const Request) ?[]const u8 {
441 if (std.mem.indexOf(u8, self.path, "?")) |idx| {
442 if (idx + 1 < self.path.len) {
443 return self.path[idx + 1 ..];
444 }
445 }
446 return null;
447 }
448
449 pub fn queryParams(self: *const Request) QueryIterator {
450 return QueryIterator{ .query = self.queryString() };
451 }
452 };
453
454 const RequestBodySurvey = union(enum) {
455 none,
456 borrowed: struct {
457 start: usize,
458 length: usize,
459 },
460 chunked: chunked.Capacity,
461 };
462
463 const RequestSurvey = struct {
464 method: Request.Method,
465 path: []const u8,
466 version: Request.Version,
467 first_line_end: usize,
468 header_end: usize,
469 header_count: usize,
470 body_start: usize,
471 body: RequestBodySurvey,
472 consumed: usize,
473 };
474
475 fn surveyRequest(scratch: RequestScratch, data: []const u8) ParseError!RequestSurvey {
476 const header_end = Request.findHeaderEnd(data) orelse return error.IncompleteRequest;
477 const first_line_end = std.mem.indexOf(u8, data, "\r\n") orelse {
478 return error.InvalidRequestLine;
479 };
480 if (first_line_end > header_end) return error.InvalidRequestLine;
481
482 var parts = std.mem.splitScalar(u8, data[0..first_line_end], ' ');
483 const method = Request.parseMethod(parts.next() orelse return error.InvalidRequestLine) orelse {
484 return error.InvalidMethod;
485 };
486 const path = parts.next() orelse return error.InvalidRequestLine;
487 if (path.len == 0) return error.InvalidRequestLine;
488 const version = Request.parseVersion(parts.next() orelse return error.InvalidRequestLine) orelse {
489 return error.InvalidVersion;
490 };
491 if (parts.next() != null) return error.InvalidRequestLine;
492
493 var header_count: usize = 0;
494 var pos = first_line_end + 2;
495 var content_length_header: ?[]const u8 = null;
496 var transfer_encoding_header: ?[]const u8 = null;
497 while (pos < header_end) {
498 const line_end = std.mem.indexOf(u8, data[pos .. header_end + 2], "\r\n") orelse {
499 unreachable;
500 };
501 const line = data[pos..][0..line_end];
502 if (line.len > scratch.header_line_bytes) return error.HeaderTooLong;
503 if (header_count >= scratch.headers.len) return error.TooManyHeaders;
504
505 const parsed = field.parseLine(line) orelse return error.InvalidHeader;
506
507 if (std.ascii.eqlIgnoreCase(parsed.name, "Content-Length")) {
508 if (content_length_header != null) return error.InvalidHeader;
509 content_length_header = parsed.value;
510 }
511 if (std.ascii.eqlIgnoreCase(parsed.name, "Transfer-Encoding")) {
512 if (transfer_encoding_header != null) return error.InvalidHeader;
513 transfer_encoding_header = parsed.value;
514 }
515
516 header_count += 1;
517 pos += line_end + 2;
518 }
519 std.debug.assert(pos == header_end + 2);
520
521 const body_start = header_end + 4;
522 var body: RequestBodySurvey = .none;
523 var consumed = body_start;
524 if (transfer_encoding_header) |transfer_encoding| {
525 if (version != .http_1_1) return error.InvalidHeader;
526 if (content_length_header != null) return error.InvalidHeader;
527 const final_coding = field.parseTransferEncoding(transfer_encoding) orelse {
528 return error.InvalidHeader;
529 };
530 if (final_coding != .chunked) {
531 return error.InvalidHeader;
532 }
533 const capacity = chunked.survey(data[body_start..], scratch.body.len) catch |err| switch (err) {
534 error.IncompleteBody => return error.IncompleteRequest,
535 error.MalformedBody => return error.InvalidHeader,
536 error.BodyTooLarge => return error.BodyTooLarge,
537 error.OutOfMemory => unreachable,
538 };
539 body = .{ .chunked = capacity };
540 consumed = body_start + capacity.consumed;
541 } else if (content_length_header) |length_text| {
542 const length = field.parseContentLength(length_text) orelse return error.InvalidHeader;
543 if (length > scratch.body.len) return error.BodyTooLarge;
544 const end = std.math.add(usize, body_start, length) catch {
545 return error.InvalidHeader;
546 };
547 if (end > data.len) return error.IncompleteRequest;
548 body = .{ .borrowed = .{ .start = body_start, .length = length } };
549 consumed = end;
550 }
551
552 return .{
553 .method = method,
554 .path = path,
555 .version = version,
556 .first_line_end = first_line_end,
557 .header_end = header_end,
558 .header_count = header_count,
559 .body_start = body_start,
560 .body = body,
561 .consumed = consumed,
562 };
563 }
564
565 fn containsHeaderToken(value: []const u8, token: []const u8) bool {
566 var iter = std.mem.splitScalar(u8, value, ',');
567 while (iter.next()) |part| {
568 const trimmed = std.mem.trim(u8, part, " \t");
569 if (std.ascii.eqlIgnoreCase(trimmed, token)) return true;
570 }
571 return false;
572 }
573
574 const TestParsedRequest = struct {
575 storage: RequestStorage,
576 result: RequestParseResult,
577
578 fn init(data: []const u8) !TestParsedRequest {
579 var storage = try RequestStorage.init(std.testing.allocator, .{
580 .request_count = 1,
581 .header_count_per_request = default_request_header_count,
582 .header_line_bytes = default_request_header_line_bytes,
583 .body_bytes_per_request = default_request_body_bytes,
584 });
585 errdefer storage.deinit(std.testing.allocator);
586 storage.activate();
587 const result = try Request.parse(try storage.request(0), data);
588 return .{ .storage = storage, .result = result };
589 }
590
591 fn deinit(self: *TestParsedRequest) void {
592 self.storage.deinit(std.testing.allocator);
593 }
594 };
595
596 fn independentRequestCapacity(limits: RequestLimits) error{CapacityOverflow}!RequestCapacity {
597 const header_count = @as(u128, limits.request_count) * limits.header_count_per_request;
598 const header_bytes = header_count * @sizeOf(RequestHeader);
599 const body_bytes = @as(u128, limits.request_count) * limits.body_bytes_per_request;
600 const storage_bytes = header_bytes + body_bytes;
601 if (header_count > std.math.maxInt(usize) or
602 header_bytes > std.math.maxInt(usize) or
603 body_bytes > std.math.maxInt(usize) or
604 storage_bytes > std.math.maxInt(usize))
605 {
606 return error.CapacityOverflow;
607 }
608 return .{
609 .request_count = limits.request_count,
610 .header_count_per_request = limits.header_count_per_request,
611 .header_line_bytes = limits.header_line_bytes,
612 .body_bytes_per_request = limits.body_bytes_per_request,
613 .header_count = @intCast(header_count),
614 .header_bytes = @intCast(header_bytes),
615 .body_bytes = @intCast(body_bytes),
616 .storage_bytes = @intCast(storage_bytes),
617 };
618 }
619
620 test "Request capacity matches independent arithmetic" {
621 comptime {
622 @stardustClaim(
623 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_capacity"),
624 null,
625 null,
626 null,
627 null,
628 null,
629 null,
630 );
631 }
632
633 for (0..17) |request_count| {
634 for (0..17) |header_count_per_request| {
635 for (0..17) |body_bytes_per_request| {
636 const limits = RequestLimits{
637 .request_count = request_count,
638 .header_count_per_request = header_count_per_request,
639 .header_line_bytes = 31,
640 .body_bytes_per_request = body_bytes_per_request,
641 };
642 try std.testing.expectEqual(
643 try independentRequestCapacity(limits),
644 try RequestCapacity.derive(limits),
645 );
646 }
647 }
648 }
649 try std.testing.expectError(error.CapacityOverflow, RequestCapacity.derive(.{
650 .request_count = 2,
651 .header_count_per_request = std.math.maxInt(usize),
652 .header_line_bytes = 0,
653 .body_bytes_per_request = 0,
654 }));
655 try std.testing.expectError(error.CapacityOverflow, RequestCapacity.derive(.{
656 .request_count = 1,
657 .header_count_per_request = std.math.maxInt(usize),
658 .header_line_bytes = 0,
659 .body_bytes_per_request = 0,
660 }));
661 try std.testing.expectError(error.CapacityOverflow, RequestCapacity.derive(.{
662 .request_count = std.math.maxInt(usize),
663 .header_count_per_request = 0,
664 .header_line_bytes = 0,
665 .body_bytes_per_request = 2,
666 }));
667 try std.testing.expectError(error.CapacityOverflow, RequestCapacity.derive(.{
668 .request_count = 1,
669 .header_count_per_request = 1,
670 .header_line_bytes = 0,
671 .body_bytes_per_request = std.math.maxInt(usize),
672 }));
673 }
674
675 fn checkRequestStorageInitFailures(allocator: std.mem.Allocator) !void {
676 var storage = try RequestStorage.init(allocator, .{
677 .request_count = 3,
678 .header_count_per_request = 7,
679 .header_line_bytes = 31,
680 .body_bytes_per_request = 17,
681 });
682 storage.deinit(allocator);
683 }
684
685 test "Request storage retries after every allocation failure" {
686 comptime {
687 @stardustClaim(
688 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_oom_retry"),
689 null,
690 null,
691 null,
692 null,
693 null,
694 null,
695 );
696 }
697
698 try std.testing.checkAllAllocationFailures(
699 std.testing.allocator,
700 checkRequestStorageInitFailures,
701 .{},
702 );
703 }
704
705 test "Request storage partitions reusable slots" {
706 comptime {
707 @stardustClaim(
708 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_partition"),
709 null,
710 null,
711 null,
712 null,
713 null,
714 null,
715 );
716 }
717
718 var storage = try RequestStorage.init(std.testing.allocator, .{
719 .request_count = 2,
720 .header_count_per_request = 2,
721 .header_line_bytes = 11,
722 .body_bytes_per_request = 3,
723 });
724 defer storage.deinit(std.testing.allocator);
725 storage.activate();
726
727 const first = try storage.request(0);
728 const second = try storage.request(1);
729 try std.testing.expect(first.headers.ptr + first.headers.len == second.headers.ptr);
730 try std.testing.expect(first.body.ptr + first.body.len == second.body.ptr);
731 try std.testing.expect(@intFromPtr(first.body.ptr) == @intFromPtr(storage.bytes.ptr) + storage.capacity.header_bytes);
732 @memset(first.body, 0x11);
733 @memset(second.body, 0x22);
734 try std.testing.expectEqualSlices(u8, &.{ 0x11, 0x11, 0x11 }, first.body);
735 try std.testing.expectEqualSlices(u8, &.{ 0x22, 0x22, 0x22 }, second.body);
736 try std.testing.expect((try storage.request(0)).headers.ptr == first.headers.ptr);
737 try std.testing.expectError(error.RequestCapacityExceeded, storage.request(2));
738 }
739
740 fn fillRequestSentinel(scratch: RequestScratch) void {
741 for (scratch.headers) |*header| header.* = .{ .name = "name", .value = "value" };
742 @memset(scratch.body, 0xA5);
743 }
744
745 fn expectRequestSentinel(scratch: RequestScratch) !void {
746 for (scratch.headers) |header| {
747 try std.testing.expectEqualStrings("name", header.name);
748 try std.testing.expectEqualStrings("value", header.value);
749 }
750 for (scratch.body) |byte| try std.testing.expectEqual(@as(u8, 0xA5), byte);
751 }
752
753 test "Request capacity failures preserve header and body storage" {
754 comptime {
755 @stardustClaim(
756 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_atomic_overload"),
757 null,
758 null,
759 null,
760 null,
761 null,
762 null,
763 );
764 }
765 comptime {
766 @stardustClaim(
767 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_atomic_foreign_risk"),
768 null,
769 null,
770 null,
771 null,
772 null,
773 null,
774 );
775 }
776
777 var storage = try RequestStorage.init(std.testing.allocator, .{
778 .request_count = 1,
779 .header_count_per_request = 2,
780 .header_line_bytes = 32,
781 .body_bytes_per_request = 4,
782 });
783 defer storage.deinit(std.testing.allocator);
784 storage.activate();
785 const scratch = try storage.request(0);
786
787 const cases = .{
788 .{ ParseError.TooManyHeaders, "GET / HTTP/1.1\r\nA: 1\r\nB: 2\r\nC: 3\r\n\r\n" },
789 .{ ParseError.HeaderTooLong, "GET / HTTP/1.1\r\nX: " ++ (@as([(30) * ("1").len]u8, @bitCast(@as([30][("1").len]u8, @splat(("1")[0..("1").len].*))))) ++ "\r\n\r\n" },
790 .{ ParseError.BodyTooLarge, "POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n12345" },
791 .{ ParseError.BodyTooLarge, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\n12345\r\n0\r\n\r\n" },
792 .{ ParseError.IncompleteRequest, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n12" },
793 };
794 inline for (cases) |case| {
795 fillRequestSentinel(scratch);
796 try std.testing.expectError(case[0], Request.parse(scratch, case[1]));
797 try expectRequestSentinel(scratch);
798 }
799 }
800
801 test "Request parser accepts exact limits and rejects max plus one" {
802 comptime {
803 @stardustClaim(
804 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_boundary"),
805 null,
806 null,
807 null,
808 null,
809 null,
810 null,
811 );
812 }
813
814 var storage = try RequestStorage.init(std.testing.allocator, .{
815 .request_count = 1,
816 .header_count_per_request = 2,
817 .header_line_bytes = 32,
818 .body_bytes_per_request = 4,
819 });
820 defer storage.deinit(std.testing.allocator);
821 storage.activate();
822 const scratch = try storage.request(0);
823
824 const exact_headers = try Request.parse(
825 scratch,
826 "GET / HTTP/1.1\r\nX: " ++ (@as([(29) * ("1").len]u8, @bitCast(@as([29][("1").len]u8, @splat(("1")[0..("1").len].*))))) ++ "\r\nB: 2\r\n\r\n",
827 );
828 try std.testing.expectEqual(@as(usize, 2), exact_headers.request.headers.count());
829 const exact_body = try Request.parse(scratch, "POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\n1234");
830 try std.testing.expectEqualStrings("1234", exact_body.request.body.?);
831 const exact_chunked = try Request.parse(scratch, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n1234\r\n0\r\n\r\n");
832 try std.testing.expectEqualStrings("1234", exact_chunked.request.body.?);
833 try std.testing.expectError(
834 error.TooManyHeaders,
835 Request.parse(scratch, "GET / HTTP/1.1\r\nA: 1\r\nB: 2\r\nC: 3\r\n\r\n"),
836 );
837 try std.testing.expectError(
838 error.HeaderTooLong,
839 Request.parse(scratch, "GET / HTTP/1.1\r\nX: " ++ (@as([(30) * ("1").len]u8, @bitCast(@as([30][("1").len]u8, @splat(("1")[0..("1").len].*))))) ++ "\r\n\r\n"),
840 );
841 try std.testing.expectError(
842 error.BodyTooLarge,
843 Request.parse(scratch, "POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n12345"),
844 );
845 }
846
847 test "Request content-length body borrows input and preserves decoded storage" {
848 var storage = try RequestStorage.init(std.testing.allocator, .{
849 .request_count = 1,
850 .header_count_per_request = 1,
851 .header_line_bytes = 32,
852 .body_bytes_per_request = 4,
853 });
854 defer storage.deinit(std.testing.allocator);
855 storage.activate();
856 const scratch = try storage.request(0);
857 @memset(scratch.body, 0xA5);
858 const data = "POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody";
859 const parsed = try Request.parse(scratch, data);
860
861 try std.testing.expectEqualStrings("body", parsed.request.body.?);
862 try std.testing.expect(@intFromPtr(parsed.request.body.?.ptr) >= @intFromPtr(data.ptr));
863 try std.testing.expect(@intFromPtr(parsed.request.body.?.ptr) < @intFromPtr(data.ptr) + data.len);
864 try std.testing.expectEqualSlices(u8, &.{ 0xA5, 0xA5, 0xA5, 0xA5 }, scratch.body);
865 }
866
867 test "Request parsing remains allocation-free after storage seals" {
868 comptime {
869 @stardustClaim(
870 @import("alloc_phase").capacity.witness(RequestStorage, "http_request_sealed"),
871 null,
872 null,
873 null,
874 null,
875 null,
876 null,
877 );
878 }
879
880 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
881 var storage = RequestStorage.init(phase_allocator.initializationAllocator(), .{
882 .request_count = 1,
883 .header_count_per_request = 2,
884 .header_line_bytes = 64,
885 .body_bytes_per_request = 5,
886 }) catch |err| {
887 phase_allocator.abortInitialization();
888 phase_allocator.deinit();
889 return err;
890 };
891 errdefer {
892 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
893 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
894 if (storage.phase != .teardown) storage.deinit(phase_allocator.teardownAllocator());
895 phase_allocator.deinit();
896 }
897
898 const pointer = storage.bytes.ptr;
899 const capacity = storage.capacity;
900 phase_allocator.seal();
901 storage.activate();
902 const parsed = try Request.parse(
903 try storage.request(0),
904 "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n",
905 );
906 try std.testing.expectEqualStrings("Hello", parsed.request.body.?);
907 try std.testing.expect(storage.bytes.ptr == pointer);
908 try std.testing.expectEqual(capacity, storage.capacity);
909 try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
910
911 phase_allocator.beginTeardown();
912 storage.deinit(phase_allocator.teardownAllocator());
913 phase_allocator.deinit();
914 }
915
916 test "Request.parse: simple GET" {
917 const data = "GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n";
918 var parsed = try TestParsedRequest.init(data);
919 defer parsed.deinit();
920 const result = parsed.result;
921 const req = result.request;
922
923 try std.testing.expectEqual(Request.Method.GET, req.method);
924 try std.testing.expectEqualStrings("/hello", req.path);
925 try std.testing.expectEqual(Request.Version.http_1_1, req.version);
926 try std.testing.expectEqualStrings("localhost", req.headers.get("Host").?);
927 try std.testing.expectEqual(@as(?[]const u8, null), req.body);
928 try std.testing.expectEqual(data.len, result.consumed);
929 }
930
931 test "Request.parse: POST with body" {
932 const data = "POST /api/data HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13\r\n\r\nHello, World!";
933 var parsed = try TestParsedRequest.init(data);
934 defer parsed.deinit();
935 const result = parsed.result;
936 const req = result.request;
937
938 try std.testing.expectEqual(Request.Method.POST, req.method);
939 try std.testing.expectEqualStrings("/api/data", req.path);
940 try std.testing.expectEqualStrings("Hello, World!", req.body.?);
941 try std.testing.expectEqual(data.len, result.consumed);
942 }
943
944 test "Request.parse: chunked body" {
945 const data = "POST /api/data HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n1\r\n \r\n5\r\nWorld\r\n0\r\n\r\n";
946 var parsed = try TestParsedRequest.init(data);
947 defer parsed.deinit();
948 const result = parsed.result;
949 const req = result.request;
950
951 try std.testing.expectEqual(Request.Method.POST, req.method);
952 try std.testing.expectEqualStrings("/api/data", req.path);
953 try std.testing.expectEqualStrings("Hello World", req.body.?);
954 try std.testing.expectEqual(data.len, result.consumed);
955 }
956
957 test "Request.parse: chunked body with trailers and pipelined data" {
958 const request = "POST /api/data HTTP/1.1\r\nTransfer-Encoding: , tiny-coding; level=5, , chunked,\r\n\r\n5\r\nHello\r\n0\r\nDigest: sha-256=abc123\r\n\r\n";
959 const data = request ++ "GET /next HTTP/1.1\r\n\r\n";
960 var parsed = try TestParsedRequest.init(data);
961 defer parsed.deinit();
962 const result = parsed.result;
963 const req = result.request;
964
965 try std.testing.expectEqualStrings("Hello", req.body.?);
966 try std.testing.expectEqual(request.len, result.consumed);
967 }
968
969 test "Request.parse: rejects ambiguous transfer encoding and content length" {
970 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n0\r\n\r\n";
971 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
972 }
973
974 test "Request.parse: rejects non-final chunked transfer encoding" {
975 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked, gzip\r\n\r\n0\r\n\r\n";
976 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
977 }
978
979 test "Request.parse: rejects unsupported transfer encoding" {
980 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\n\r\npayload";
981 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
982 }
983
984 test "Request.parse: rejects malformed transfer coding grammar" {
985 const malformed = [_][]const u8{
986 "POST / HTTP/1.1\r\nTransfer-Encoding: , ,\r\n\r\n",
987 "POST / HTTP/1.1\r\nTransfer-Encoding: tiny-coding; name=\r\n\r\n",
988 "POST / HTTP/1.1\r\nTransfer-Encoding: chunked; name=value\r\n\r\n0\r\n\r\n",
989 "POST / HTTP/1.1\r\nTransfer-Encoding: gzip; name=value, chunked\r\n\r\n0\r\n\r\n",
990 "POST / HTTP/1.1\r\nTransfer-Encoding: chunked, chunked\r\n\r\n0\r\n\r\n",
991 };
992 for (malformed) |data| {
993 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
994 }
995 }
996
997 test "Request.parse: rejects transfer encoding on HTTP/1.0" {
998 const data = "POST / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
999 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1000 }
1001
1002 test "Request.parse: incomplete chunked body" {
1003 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n";
1004 try std.testing.expectError(ParseError.IncompleteRequest, TestParsedRequest.init(data));
1005 }
1006
1007 test "Request.parse: malformed chunked body" {
1008 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\ng\r\nbad\r\n";
1009 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1010 }
1011
1012 test "Request.parse: chunked body capacity is checked before mutation" {
1013 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n";
1014 var headers: [1]RequestHeader = undefined;
1015 var body = @as([4]u8, @splat(0xA5));
1016 try std.testing.expectError(ParseError.BodyTooLarge, Request.parse(.{
1017 .headers = &headers,
1018 .body = &body,
1019 .header_line_bytes = default_request_header_line_bytes,
1020 }, data));
1021 try std.testing.expectEqualSlices(u8, &.{ 0xA5, 0xA5, 0xA5, 0xA5 }, &body);
1022 }
1023
1024 test "Request.parse: WebSocket upgrade request" {
1025 const data =
1026 "GET /ws HTTP/1.1\r\n" ++
1027 "Host: localhost:8080\r\n" ++
1028 "Upgrade: websocket\r\n" ++
1029 "Connection: Upgrade\r\n" ++
1030 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
1031 "Sec-WebSocket-Version: 13\r\n" ++
1032 "\r\n";
1033
1034 var parsed = try TestParsedRequest.init(data);
1035 defer parsed.deinit();
1036 const req = parsed.result.request;
1037
1038 try std.testing.expect(req.isWebSocketUpgrade());
1039 try std.testing.expectEqualStrings("dGhlIHNhbXBsZSBub25jZQ==", req.getWebSocketKey().?);
1040 }
1041
1042 test "Request.parse: multiple headers" {
1043 const data =
1044 "GET / HTTP/1.1\r\n" ++
1045 "Host: localhost\r\n" ++
1046 "Accept: text/html\r\n" ++
1047 "Accept-Language: en-US\r\n" ++
1048 "User-Agent: Test/1.0\r\n" ++
1049 "\r\n";
1050
1051 var parsed = try TestParsedRequest.init(data);
1052 defer parsed.deinit();
1053 const req = parsed.result.request;
1054
1055 try std.testing.expectEqualStrings("localhost", req.headers.get("Host").?);
1056 try std.testing.expectEqualStrings("text/html", req.headers.get("Accept").?);
1057 try std.testing.expectEqualStrings("en-US", req.headers.get("Accept-Language").?);
1058 try std.testing.expectEqualStrings("Test/1.0", req.headers.get("User-Agent").?);
1059 }
1060
1061 test "Request.parse: HTTP/1.0" {
1062 const data = "GET / HTTP/1.0\r\n\r\n";
1063 var parsed = try TestParsedRequest.init(data);
1064 defer parsed.deinit();
1065 const req = parsed.result.request;
1066
1067 try std.testing.expectEqual(Request.Version.http_1_0, req.version);
1068 }
1069
1070 test "Request.parse: header with leading whitespace in value" {
1071 const data = "GET / HTTP/1.1\r\nX-Custom: spaced value\r\n\r\n";
1072 var parsed = try TestParsedRequest.init(data);
1073 defer parsed.deinit();
1074 const req = parsed.result.request;
1075
1076 try std.testing.expectEqualStrings("spaced value", req.headers.get("X-Custom").?);
1077 }
1078
1079 test "Request.parse: incomplete request (no header terminator)" {
1080 const data = "GET / HTTP/1.1\r\nHost: localhost";
1081 try std.testing.expectError(ParseError.IncompleteRequest, TestParsedRequest.init(data));
1082 }
1083
1084 test "Request.parse: incomplete request (body shorter than Content-Length)" {
1085 const data = "POST / HTTP/1.1\r\nContent-Length: 100\r\n\r\nShort";
1086 try std.testing.expectError(ParseError.IncompleteRequest, TestParsedRequest.init(data));
1087 }
1088
1089 test "Request.parse: rejects Content-Length beyond body capacity" {
1090 const data = "POST / HTTP/1.1\r\nContent-Length: 18446744073709551615\r\n\r\nX";
1091 try std.testing.expectError(ParseError.BodyTooLarge, TestParsedRequest.init(data));
1092 }
1093
1094 test "Request.parse: rejects excessively large Content-Length (DoS protection)" {
1095 const data = "POST / HTTP/1.1\r\nContent-Length: 11534336\r\n\r\nX";
1096 try std.testing.expectError(ParseError.BodyTooLarge, TestParsedRequest.init(data));
1097 }
1098
1099 test "Request.parse: content length permits trailing whitespace" {
1100 const data = "POST / HTTP/1.1\r\nContent-Length: 5 \t\r\n\r\nHello";
1101 var parsed = try TestParsedRequest.init(data);
1102 defer parsed.deinit();
1103 const req = parsed.result.request;
1104
1105 try std.testing.expectEqualStrings("Hello", req.body.?);
1106 }
1107
1108 test "Request.parse: content length requires decimal digits" {
1109 const malformed = [_][]const u8{
1110 "POST / HTTP/1.1\r\nContent-Length:\r\n\r\n",
1111 "POST / HTTP/1.1\r\nContent-Length: +3\r\n\r\nabc",
1112 "POST / HTTP/1.1\r\nContent-Length: -0\r\n\r\n",
1113 "POST / HTTP/1.1\r\nContent-Length: 1_0\r\n\r\n0123456789",
1114 "POST / HTTP/1.1\r\nContent-Length: 3, 3\r\n\r\nabc",
1115 "POST / HTTP/1.1\r\nContent-Length: 184467440737095516160\r\n\r\n",
1116 };
1117 for (malformed) |data| {
1118 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1119 }
1120
1121 var parsed = try TestParsedRequest.init(
1122 "POST / HTTP/1.1\r\nContent-Length: 0003\r\n\r\nabc",
1123 );
1124 defer parsed.deinit();
1125 try std.testing.expectEqualStrings("abc", parsed.result.request.body.?);
1126 }
1127
1128 test "Request.parse: rejects duplicate Content-Length" {
1129 const data = "POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nHello";
1130 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1131 }
1132
1133 test "Request.parse: rejects duplicate Transfer-Encoding" {
1134 const data = "POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
1135 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1136 }
1137
1138 test "Request.parse: invalid method" {
1139 const data = "INVALID / HTTP/1.1\r\n\r\n";
1140 try std.testing.expectError(ParseError.InvalidMethod, TestParsedRequest.init(data));
1141 }
1142
1143 test "Request.parse: invalid version" {
1144 const data = "GET / HTTP/2.0\r\n\r\n";
1145 try std.testing.expectError(ParseError.InvalidVersion, TestParsedRequest.init(data));
1146 }
1147
1148 test "Request.parse: malformed header (no colon)" {
1149 const data = "GET / HTTP/1.1\r\nBadHeader\r\n\r\n";
1150 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1151 }
1152
1153 test "Request.parse: rejects malformed request line with extra tokens" {
1154 const data = "GET / HTTP/1.1 extra\r\n\r\n";
1155 try std.testing.expectError(ParseError.InvalidRequestLine, TestParsedRequest.init(data));
1156 }
1157
1158 test "Request.parse: rejects invalid header names" {
1159 const empty = "GET / HTTP/1.1\r\n: value\r\n\r\n";
1160 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(empty));
1161
1162 const spaced = "GET / HTTP/1.1\r\nBad Header: value\r\n\r\n";
1163 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(spaced));
1164 }
1165
1166 test "Request.parse: rejects invalid field value controls" {
1167 const malformed = [_][]const u8{
1168 "GET / HTTP/1.1\r\nX-Test: bad\x00value\r\n\r\n",
1169 "GET / HTTP/1.1\r\nX-Test: bad\x0bvalue\r\n\r\n",
1170 "GET / HTTP/1.1\r\nX-Test: bad\x0cvalue\r\n\r\n",
1171 "GET / HTTP/1.1\r\nX-Test: bad\x7fvalue\r\n\r\n",
1172 };
1173 for (malformed) |data| {
1174 try std.testing.expectError(ParseError.InvalidHeader, TestParsedRequest.init(data));
1175 }
1176 }
1177
1178 test "Request.parse: all HTTP methods" {
1179 const methods = .{
1180 .{ "GET", Request.Method.GET },
1181 .{ "POST", Request.Method.POST },
1182 .{ "PUT", Request.Method.PUT },
1183 .{ "DELETE", Request.Method.DELETE },
1184 .{ "HEAD", Request.Method.HEAD },
1185 .{ "OPTIONS", Request.Method.OPTIONS },
1186 .{ "PATCH", Request.Method.PATCH },
1187 .{ "CONNECT", Request.Method.CONNECT },
1188 .{ "TRACE", Request.Method.TRACE },
1189 };
1190
1191 inline for (methods) |pair| {
1192 const data = pair[0] ++ " / HTTP/1.1\r\n\r\n";
1193 var parsed = try TestParsedRequest.init(data);
1194 defer parsed.deinit();
1195 const req = parsed.result.request;
1196 try std.testing.expectEqual(pair[1], req.method);
1197 }
1198 }
1199
1200 test "Request.isWebSocketUpgrade: returns false without headers" {
1201 const data = "GET /ws HTTP/1.1\r\nHost: localhost\r\n\r\n";
1202 var parsed = try TestParsedRequest.init(data);
1203 defer parsed.deinit();
1204 const req = parsed.result.request;
1205
1206 try std.testing.expect(!req.isWebSocketUpgrade());
1207 }
1208
1209 test "Request.isWebSocketUpgrade: returns false with only Upgrade header" {
1210 const data = "GET /ws HTTP/1.1\r\nUpgrade: websocket\r\n\r\n";
1211 var parsed = try TestParsedRequest.init(data);
1212 defer parsed.deinit();
1213 const req = parsed.result.request;
1214
1215 try std.testing.expect(!req.isWebSocketUpgrade());
1216 }
1217
1218 test "Request headers are case-insensitive (RFC 7230)" {
1219 const data = "GET / HTTP/1.1\r\nhost: localhost\r\ncontent-type: text/html\r\nX-Custom-Header: value\r\n\r\n";
1220 var parsed = try TestParsedRequest.init(data);
1221 defer parsed.deinit();
1222 const req = parsed.result.request;
1223
1224 try std.testing.expectEqualStrings("localhost", req.headers.get("Host").?);
1225 try std.testing.expectEqualStrings("localhost", req.headers.get("host").?);
1226 try std.testing.expectEqualStrings("localhost", req.headers.get("HOST").?);
1227 try std.testing.expectEqualStrings("text/html", req.headers.get("Content-Type").?);
1228 try std.testing.expectEqualStrings("text/html", req.headers.get("content-type").?);
1229 try std.testing.expectEqualStrings("value", req.headers.get("x-custom-header").?);
1230 }
1231
1232 test "Request header lookup returns the final duplicate value" {
1233 const data = "GET / HTTP/1.1\r\nX-Value: first\r\nx-value: second\r\n\r\n";
1234 var parsed = try TestParsedRequest.init(data);
1235 defer parsed.deinit();
1236 const req = parsed.result.request;
1237
1238 try std.testing.expectEqual(@as(usize, 2), req.headers.count());
1239 try std.testing.expectEqualStrings("second", req.headers.get("X-Value").?);
1240 }
1241
1242 test "Request.isWebSocketUpgrade: works with lowercase headers" {
1243 const data =
1244 "GET /ws HTTP/1.1\r\n" ++
1245 "host: localhost:8080\r\n" ++
1246 "upgrade: websocket\r\n" ++
1247 "connection: Upgrade\r\n" ++
1248 "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
1249 "sec-websocket-version: 13\r\n" ++
1250 "\r\n";
1251
1252 var parsed = try TestParsedRequest.init(data);
1253 defer parsed.deinit();
1254 const req = parsed.result.request;
1255
1256 try std.testing.expect(req.isWebSocketUpgrade());
1257 try std.testing.expectEqualStrings("dGhlIHNhbXBsZSBub25jZQ==", req.getWebSocketKey().?);
1258 }
1259
1260 test "Request.isWebSocketUpgrade: parses connection tokens case-insensitively" {
1261 const data =
1262 "GET /ws HTTP/1.1\r\n" ++
1263 "Upgrade: websocket\r\n" ++
1264 "Connection: keep-alive, upgrade\r\n" ++
1265 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
1266 "\r\n";
1267
1268 var parsed = try TestParsedRequest.init(data);
1269 defer parsed.deinit();
1270 const req = parsed.result.request;
1271
1272 try std.testing.expect(req.isWebSocketUpgrade());
1273 }
1274
1275 test "Request.isWebSocketUpgrade: parses upgrade tokens case-insensitively" {
1276 const data =
1277 "GET /ws HTTP/1.1\r\n" ++
1278 "Upgrade: h2c, WebSocket\r\n" ++
1279 "Connection: keep-alive, upgrade\r\n" ++
1280 "\r\n";
1281
1282 var parsed = try TestParsedRequest.init(data);
1283 defer parsed.deinit();
1284 const req = parsed.result.request;
1285
1286 try std.testing.expect(req.isWebSocketUpgrade());
1287 }
1288
1289 test "Request.isWebSocketUpgrade: rejects non-GET requests" {
1290 const data =
1291 "POST /ws HTTP/1.1\r\n" ++
1292 "Upgrade: websocket\r\n" ++
1293 "Connection: upgrade\r\n" ++
1294 "\r\n";
1295
1296 var parsed = try TestParsedRequest.init(data);
1297 defer parsed.deinit();
1298 const req = parsed.result.request;
1299
1300 try std.testing.expect(!req.isWebSocketUpgrade());
1301 }
1302
1303 test "Request.isWebSocketUpgrade: rejects HTTP/1.0 requests" {
1304 const data =
1305 "GET /ws HTTP/1.0\r\n" ++
1306 "Upgrade: websocket\r\n" ++
1307 "Connection: upgrade\r\n" ++
1308 "\r\n";
1309
1310 var parsed = try TestParsedRequest.init(data);
1311 defer parsed.deinit();
1312 const req = parsed.result.request;
1313
1314 try std.testing.expect(!req.isWebSocketUpgrade());
1315 }
1316
1317 test "Request.QueryIterator skips empty parameters iteratively" {
1318 const data = "GET /search?&&&&q=tiny&&empty=&flag HTTP/1.1\r\n\r\n";
1319 var parsed = try TestParsedRequest.init(data);
1320 defer parsed.deinit();
1321 const req = parsed.result.request;
1322
1323 var iter = req.queryParams();
1324 const q = iter.next().?;
1325 try std.testing.expectEqualStrings("q", q.key);
1326 try std.testing.expectEqualStrings("tiny", q.value);
1327 const empty = iter.next().?;
1328 try std.testing.expectEqualStrings("empty", empty.key);
1329 try std.testing.expectEqualStrings("", empty.value);
1330 const flag = iter.next().?;
1331 try std.testing.expectEqualStrings("flag", flag.key);
1332 try std.testing.expectEqualStrings("", flag.value);
1333 try std.testing.expect(iter.next() == null);
1334 }