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 }