lib/http/src/response.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const Request = @import("message.zig").Request;
  4 
  5 pub const default_header_count: usize = 32;
  6 pub const default_head_bytes: usize = 16 * 1024;
  7 
  8 pub const Header = struct {
  9     name: []const u8,
 10     value: []const u8,
 11 };
 12 
 13 pub const Headers = struct {
 14     entries: []Header,
 15     length: usize,
 16 
 17     pub fn get(self: Headers, name: []const u8) ?[]const u8 {
 18         var index = self.length;
 19         while (index != 0) {
 20             index -= 1;
 21             const entry = self.entries[index];
 22             if (std.ascii.eqlIgnoreCase(entry.name, name)) return entry.value;
 23         }
 24         return null;
 25     }
 26 
 27     pub fn contains(self: Headers, name: []const u8) bool {
 28         return self.get(name) != null;
 29     }
 30 
 31     pub fn count(self: Headers) usize {
 32         return self.length;
 33     }
 34 
 35     fn set(self: *Headers, name: []const u8, value: []const u8) Error!void {
 36         for (self.entries[0..self.length]) |*entry| {
 37             if (!std.ascii.eqlIgnoreCase(entry.name, name)) continue;
 38             entry.* = .{ .name = name, .value = value };
 39             return;
 40         }
 41         if (self.length == self.entries.len) return error.HeaderCapacityExceeded;
 42         self.entries[self.length] = .{ .name = name, .value = value };
 43         self.length += 1;
 44     }
 45 };
 46 
 47 pub const Limits = struct {
 48     response_count: usize,
 49     header_count_per_response: usize,
 50     head_bytes_per_response: usize,
 51 };
 52 
 53 pub const Capacity = struct {
 54     response_count: usize,
 55     header_count_per_response: usize,
 56     head_bytes_per_response: usize,
 57     header_count: usize,
 58     header_bytes: usize,
 59     head_bytes: usize,
 60     storage_bytes: usize,
 61 
 62     pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
 63         const header_count = try alloc_phase.capacity.mul(
 64             usize,
 65             limits.response_count,
 66             limits.header_count_per_response,
 67         );
 68         const header_bytes = try alloc_phase.capacity.mul(
 69             usize,
 70             header_count,
 71             @sizeOf(Header),
 72         );
 73         const head_bytes = try alloc_phase.capacity.mul(
 74             usize,
 75             limits.response_count,
 76             limits.head_bytes_per_response,
 77         );
 78         const storage_bytes = try alloc_phase.capacity.add(
 79             usize,
 80             header_bytes,
 81             head_bytes,
 82         );
 83         return .{
 84             .response_count = limits.response_count,
 85             .header_count_per_response = limits.header_count_per_response,
 86             .head_bytes_per_response = limits.head_bytes_per_response,
 87             .header_count = header_count,
 88             .header_bytes = header_bytes,
 89             .head_bytes = head_bytes,
 90             .storage_bytes = storage_bytes,
 91         };
 92     }
 93 };
 94 
 95 pub const StorageExhaustion = error{ResponseCapacityExceeded};
 96 
 97 const StorageLimits = Limits;
 98 const StorageCapacity = Capacity;
 99 
100 pub const Scratch = struct {
101     headers: []Header,
102     head: []u8,
103 };
104 
105 pub const Storage = struct {
106     phase: alloc_phase.capacity.Phase,
107     capacity: StorageCapacity,
108     bytes: []align(@alignOf(Header)) u8,
109 
110     pub const Limits: type = StorageLimits;
111     pub const Capacity: type = StorageCapacity;
112     pub const Exhaustion: type = StorageExhaustion;
113     pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};
114 
115     pub const claim: alloc_phase.capacity.Declaration = .{
116         .source = .{
117             .id = "http.response_storage",
118             .kind = .phase_static,
119             .limit_source = .caller,
120             .storage = .{
121                 .covered = &.{
122                     .{
123                         .id = "fixed_response_header_entry_region_for_every_response_slot",
124                         .lifetime = .steady,
125                         .detail = "fixed response-header entry region for every response slot",
126                     },
127                     .{
128                         .id = "fixed_serialized_response_head_byte_region_for_ever_e148bf132b18",
129                         .lifetime = .steady,
130                         .detail = "fixed serialized response-head byte region for every response slot",
131                     },
132                 },
133                 .excluded = &.{
134                     "borrowed or transferred response body bytes and release callbacks",
135                     "connection input, socket and kernel queues, clients, TLS, and protocol sessions",
136                 },
137             },
138             .capacity = .{
139                 .inputs = &.{
140                     alloc_phase.capacity.bindInput(StorageLimits, "response_count", "response_count"),
141                     alloc_phase.capacity.bindInput(StorageLimits, "header_count_per_response", "header_count_per_response"),
142                     alloc_phase.capacity.bindInput(StorageLimits, "head_bytes_per_response", "head_bytes_per_response"),
143                 },
144                 .type_selectors = &.{
145                     alloc_phase.capacity.bindType(Header, "header"),
146                 },
147                 .nodes = &.{
148                     .{ .input = 0 },
149                     .{ .input = 1 },
150                     .{ .product = .{ .left = 0, .right = 1 } },
151                     .{ .constant = 1 },
152                     .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 0 } } },
153                     .{ .product = .{ .left = 2, .right = 4 } },
154                     .{ .input = 2 },
155                     .{ .product = .{ .left = 0, .right = 6 } },
156                     .{ .add = .{ .left = 5, .right = 7 } },
157                 },
158                 .assertions = &.{.{
159                     .scope = .closure_total,
160                     .measure = .retained,
161                     .relation = .exact,
162                     .expression = 8,
163                 }},
164             },
165             .overload = .{
166                 .kind = .reject_before_mutation,
167                 .detail = "header and serialized-head max plus one preserve prior storage",
168             },
169             .risks = .{
170                 .transitive = .{
171                     .status = .witnessed,
172                     .detail = "response mutation and serialization allocate no storage after activation",
173                 },
174                 .foreign = .{
175                     .status = .excluded,
176                     .detail = "serialization transforms borrowed values without performing I/O",
177                 },
178             },
179             .obligations = &.{
180                 .{ .key = "http_response_capacity", .role = .capacity_model },
181                 .{ .key = "http_response_oom_retry", .role = .custom },
182                 .{ .key = "http_response_partition", .role = .custom },
183                 .{ .key = "http_response_sealed", .role = .transitive_risk },
184                 .{ .key = "http_response_atomic_overload", .role = .overload },
185                 .{ .key = "http_response_atomic_foreign_risk", .role = .foreign_risk },
186                 .{ .key = "http_response_boundary", .role = .custom },
187                 .{ .key = "http_response_router", .role = .custom },
188             },
189         },
190         .bindings = .{
191             .owner = @This(),
192             .seal = .{
193                 .family = alloc_phase.capacity.selector(@This().activate),
194                 .premise = .{
195                     .class = .checked_semantic_fact,
196                     .authority = .checker,
197                 },
198             },
199             .teardown = .{
200                 .family = alloc_phase.capacity.selector(@This().deinit),
201                 .premise = .{
202                     .class = .checked_semantic_fact,
203                     .authority = .checker,
204                 },
205             },
206         },
207     };
208 
209     pub fn init(allocator: std.mem.Allocator, limits: StorageLimits) InitError!Storage {
210         const capacity = try StorageCapacity.derive(limits);
211         const bytes = if (capacity.storage_bytes == 0)
212             @as([]align(@alignOf(Header)) u8, &.{})
213         else
214             try allocator.alignedAlloc(u8, .of(Header), capacity.storage_bytes);
215         return .{
216             .phase = .initialization,
217             .capacity = capacity,
218             .bytes = bytes,
219         };
220     }
221 
222     pub fn activate(self: *Storage) void {
223         std.debug.assert(self.phase == .initialization);
224         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
225         self.phase = .steady;
226     }
227 
228     pub fn response(self: *Storage, index: usize) Exhaustion!Scratch {
229         std.debug.assert(self.phase == .steady);
230         if (index >= self.capacity.response_count) return error.ResponseCapacityExceeded;
231         const all_headers = std.mem.bytesAsSlice(
232             Header,
233             self.bytes[0..self.capacity.header_bytes],
234         );
235         const header_start = index * self.capacity.header_count_per_response;
236         const head_start = self.capacity.header_bytes +
237             index * self.capacity.head_bytes_per_response;
238         return .{
239             .headers = all_headers[header_start..][0..self.capacity.header_count_per_response],
240             .head = self.bytes[head_start..][0..self.capacity.head_bytes_per_response],
241         };
242     }
243 
244     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
245         std.debug.assert(self.phase != .teardown);
246         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
247         self.phase = .teardown;
248         if (self.bytes.len != 0) allocator.free(self.bytes);
249         self.bytes = &.{};
250     }
251 };
252 
253 comptime {
254     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
255 }
256 
257 pub const Error = error{
258     HeaderCapacityExceeded,
259     HeadCapacityExceeded,
260 };
261 
262 pub const Serialized = struct {
263     head: []const u8,
264     body: ?[]const u8,
265 
266     pub fn totalLength(self: Serialized) usize {
267         return self.head.len + if (self.body) |body| body.len else 0;
268     }
269 };
270 
271 const SerializeOptions = struct {
272     method: ?Request.Method = null,
273 };
274 
275 pub const Response = struct {
276     status: u16,
277     status_text: []const u8,
278     headers: Headers,
279     body: ?[]const u8,
280     body_release: ?BodyRelease,
281     head_storage: []u8,
282 
283     const BodyRelease = struct {
284         context: *anyopaque,
285         callback: *const fn (*anyopaque, []const u8) void,
286         body: []const u8,
287     };
288 
289     pub fn init(scratch: Scratch) Response {
290         return .{
291             .status = 200,
292             .status_text = "OK",
293             .headers = .{ .entries = scratch.headers, .length = 0 },
294             .body = null,
295             .body_release = null,
296             .head_storage = scratch.head,
297         };
298     }
299 
300     pub fn deinit(self: *Response) void {
301         if (self.body_release) |release| {
302             release.callback(release.context, release.body);
303         }
304         self.body = null;
305         self.body_release = null;
306     }
307 
308     pub fn setBodyWithRelease(
309         self: *Response,
310         body: []const u8,
311         context: anytype,
312         comptime release_body: anytype,
313     ) void {
314         const Context = @TypeOf(context);
315         const info = @typeInfo(Context);
316         if (info != .pointer or info.pointer.size != .one) {
317             @compileError("response body release context must be a single-item pointer");
318         }
319         const Erased = struct {
320             fn release(erased: *anyopaque, bytes: []const u8) void {
321                 const typed: Context = @ptrCast(@alignCast(erased));
322                 release_body(typed, bytes);
323             }
324         };
325         self.deinit();
326         self.body = body;
327         self.body_release = .{
328             .context = @ptrCast(@constCast(context)),
329             .callback = Erased.release,
330             .body = body,
331         };
332     }
333 
334     pub fn setHeader(self: *Response, name: []const u8, value: []const u8) Error!void {
335         try self.headers.set(name, value);
336     }
337 
338     pub fn serialize(self: *const Response) Error!Serialized {
339         return self.serializeWithOptions(.{});
340     }
341 
342     pub fn serializeForMethod(self: *const Response, method: Request.Method) Error!Serialized {
343         return self.serializeWithOptions(.{ .method = method });
344     }
345 
346     pub fn switchingProtocols(scratch: Scratch, accept_key: []const u8) Error!Response {
347         if (scratch.headers.len < 3) return error.HeaderCapacityExceeded;
348         var response = Response.init(scratch);
349         response.status = 101;
350         response.status_text = "Switching Protocols";
351         try response.setHeader("Upgrade", "websocket");
352         try response.setHeader("Connection", "Upgrade");
353         try response.setHeader("Sec-WebSocket-Accept", accept_key);
354         return response;
355     }
356 
357     fn serializeWithOptions(self: *const Response, options: SerializeOptions) Error!Serialized {
358         const head_length = try self.serializedHeadLength();
359         if (head_length > self.head_storage.len) return error.HeadCapacityExceeded;
360 
361         var fixed = std.Io.Writer.fixed(self.head_storage[0..head_length]);
362         const writer = &fixed;
363         writer.print("HTTP/1.1 {d} {s}\r\n", .{ self.status, self.status_text }) catch unreachable;
364         for (self.headers.entries[0..self.headers.length]) |entry| {
365             if (!self.shouldWriteHeader(entry.name)) continue;
366             writer.print("{s}: {s}\r\n", .{ entry.name, entry.value }) catch unreachable;
367         }
368         if (self.body) |body| {
369             if (self.shouldWriteContentLength() and !self.headers.contains("Content-Length")) {
370                 writer.print("Content-Length: {d}\r\n", .{body.len}) catch unreachable;
371             }
372         }
373         writer.writeAll("\r\n") catch unreachable;
374         std.debug.assert(writer.buffered().len == head_length);
375 
376         return .{
377             .head = writer.buffered(),
378             .body = if (self.shouldWriteBody(options)) self.body else null,
379         };
380     }
381 
382     fn serializedHeadLength(self: *const Response) Error!usize {
383         var length: usize = 0;
384         length = try addLength(length, "HTTP/1.1 ".len);
385         length = try addLength(length, decimalLength(self.status));
386         length = try addLength(length, " ".len);
387         length = try addLength(length, self.status_text.len);
388         length = try addLength(length, "\r\n".len);
389         for (self.headers.entries[0..self.headers.length]) |entry| {
390             if (!self.shouldWriteHeader(entry.name)) continue;
391             length = try addLength(length, entry.name.len);
392             length = try addLength(length, ": ".len);
393             length = try addLength(length, entry.value.len);
394             length = try addLength(length, "\r\n".len);
395         }
396         if (self.body) |body| {
397             if (self.shouldWriteContentLength() and !self.headers.contains("Content-Length")) {
398                 length = try addLength(length, "Content-Length: ".len);
399                 length = try addLength(length, decimalLength(body.len));
400                 length = try addLength(length, "\r\n".len);
401             }
402         }
403         return addLength(length, "\r\n".len);
404     }
405 
406     fn shouldWriteHeader(self: *const Response, name: []const u8) bool {
407         if (std.ascii.eqlIgnoreCase(name, "Transfer-Encoding")) {
408             return self.statusAllowsBody();
409         }
410         if (std.ascii.eqlIgnoreCase(name, "Content-Length")) {
411             return self.statusAllowsContentLength();
412         }
413         return true;
414     }
415 
416     fn shouldWriteContentLength(self: *const Response) bool {
417         return self.statusAllowsContentLength();
418     }
419 
420     fn shouldWriteBody(self: *const Response, options: SerializeOptions) bool {
421         if (self.body == null) return false;
422         if (!self.statusAllowsBody()) return false;
423         if (options.method) |method| {
424             if (method == .HEAD) return false;
425         }
426         return true;
427     }
428 
429     fn statusAllowsBody(self: *const Response) bool {
430         if (self.status >= 100 and self.status < 200) return false;
431         return self.status != 204 and self.status != 304;
432     }
433 
434     fn statusAllowsContentLength(self: *const Response) bool {
435         if (self.status >= 100 and self.status < 200) return false;
436         return self.status != 204;
437     }
438 };
439 
440 fn addLength(current: usize, additional: usize) Error!usize {
441     return std.math.add(usize, current, additional) catch error.HeadCapacityExceeded;
442 }
443 
444 fn decimalLength(value: anytype) usize {
445     var remaining: usize = @intCast(value);
446     var length: usize = 1;
447     while (remaining >= 10) {
448         remaining /= 10;
449         length += 1;
450     }
451     return length;
452 }
453 
454 fn independentCapacity(limits: Limits) error{CapacityOverflow}!Capacity {
455     const header_count = @as(u128, limits.response_count) * limits.header_count_per_response;
456     const header_bytes = header_count * @sizeOf(Header);
457     const head_bytes = @as(u128, limits.response_count) * limits.head_bytes_per_response;
458     const storage_bytes = header_bytes + head_bytes;
459     if (header_count > std.math.maxInt(usize) or
460         header_bytes > std.math.maxInt(usize) or
461         head_bytes > std.math.maxInt(usize) or
462         storage_bytes > std.math.maxInt(usize))
463     {
464         return error.CapacityOverflow;
465     }
466     return .{
467         .response_count = limits.response_count,
468         .header_count_per_response = limits.header_count_per_response,
469         .head_bytes_per_response = limits.head_bytes_per_response,
470         .header_count = @intCast(header_count),
471         .header_bytes = @intCast(header_bytes),
472         .head_bytes = @intCast(head_bytes),
473         .storage_bytes = @intCast(storage_bytes),
474     };
475 }
476 
477 test "Response capacity matches independent arithmetic" {
478     comptime {
479         @stardustClaim(
480             @import("alloc_phase").capacity.witness(Storage, "http_response_capacity"),
481             null,
482             null,
483             null,
484             null,
485             null,
486             null,
487         );
488     }
489 
490     for (0..17) |response_count| {
491         for (0..17) |header_count_per_response| {
492             for (0..17) |head_bytes_per_response| {
493                 const limits = Limits{
494                     .response_count = response_count,
495                     .header_count_per_response = header_count_per_response,
496                     .head_bytes_per_response = head_bytes_per_response,
497                 };
498                 try std.testing.expectEqual(
499                     try independentCapacity(limits),
500                     try Capacity.derive(limits),
501                 );
502             }
503         }
504     }
505     const maximum = std.math.maxInt(usize);
506     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
507         .response_count = 2,
508         .header_count_per_response = maximum,
509         .head_bytes_per_response = 0,
510     }));
511     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
512         .response_count = 1,
513         .header_count_per_response = maximum,
514         .head_bytes_per_response = 0,
515     }));
516     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
517         .response_count = maximum,
518         .header_count_per_response = 0,
519         .head_bytes_per_response = 2,
520     }));
521     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
522         .response_count = 1,
523         .header_count_per_response = 1,
524         .head_bytes_per_response = maximum,
525     }));
526 }
527 
528 fn checkStorageInitFailures(allocator: std.mem.Allocator) !void {
529     var storage = try Storage.init(allocator, .{
530         .response_count = 3,
531         .header_count_per_response = 7,
532         .head_bytes_per_response = 127,
533     });
534     storage.deinit(allocator);
535 }
536 
537 test "Response storage retries after every allocation failure" {
538     comptime {
539         @stardustClaim(
540             @import("alloc_phase").capacity.witness(Storage, "http_response_oom_retry"),
541             null,
542             null,
543             null,
544             null,
545             null,
546             null,
547         );
548     }
549 
550     try std.testing.checkAllAllocationFailures(
551         std.testing.allocator,
552         checkStorageInitFailures,
553         .{},
554     );
555 }
556 
557 test "Response storage partitions reusable slots" {
558     comptime {
559         @stardustClaim(
560             @import("alloc_phase").capacity.witness(Storage, "http_response_partition"),
561             null,
562             null,
563             null,
564             null,
565             null,
566             null,
567         );
568     }
569 
570     var storage = try Storage.init(std.testing.allocator, .{
571         .response_count = 2,
572         .header_count_per_response = 2,
573         .head_bytes_per_response = 3,
574     });
575     defer storage.deinit(std.testing.allocator);
576     storage.activate();
577 
578     const first = try storage.response(0);
579     const second = try storage.response(1);
580     try std.testing.expect(first.headers.ptr + first.headers.len == second.headers.ptr);
581     try std.testing.expect(first.head.ptr + first.head.len == second.head.ptr);
582     try std.testing.expect(@intFromPtr(first.head.ptr) == @intFromPtr(storage.bytes.ptr) + storage.capacity.header_bytes);
583     @memset(first.head, 0x11);
584     @memset(second.head, 0x22);
585     try std.testing.expectEqualSlices(u8, &.{ 0x11, 0x11, 0x11 }, first.head);
586     try std.testing.expectEqualSlices(u8, &.{ 0x22, 0x22, 0x22 }, second.head);
587     try std.testing.expect((try storage.response(0)).headers.ptr == first.headers.ptr);
588     try std.testing.expectError(error.ResponseCapacityExceeded, storage.response(2));
589 }
590 
591 test "Response capacity failures preserve header and head storage" {
592     comptime {
593         @stardustClaim(
594             @import("alloc_phase").capacity.witness(Storage, "http_response_atomic_overload"),
595             null,
596             null,
597             null,
598             null,
599             null,
600             null,
601         );
602     }
603     comptime {
604         @stardustClaim(
605             @import("alloc_phase").capacity.witness(Storage, "http_response_atomic_foreign_risk"),
606             null,
607             null,
608             null,
609             null,
610             null,
611             null,
612         );
613     }
614 
615     var headers = [_]Header{.{ .name = "sentinel", .value = "value" }};
616     var head = @as([18]u8, @splat(0xA5));
617     var response = Response.init(.{ .headers = &headers, .head = &head });
618     try response.setHeader("X-One", "one");
619     try std.testing.expectError(
620         error.HeaderCapacityExceeded,
621         response.setHeader("X-Two", "two"),
622     );
623     try std.testing.expectEqualStrings("X-One", headers[0].name);
624     try std.testing.expectEqualStrings("one", headers[0].value);
625     try std.testing.expectError(error.HeadCapacityExceeded, response.serialize());
626     try std.testing.expectEqualSlices(u8, &(@as([18]u8, @splat(0xA5))), &head);
627 
628     var handshake_headers = [_]Header{
629         .{ .name = "first", .value = "one" },
630         .{ .name = "second", .value = "two" },
631     };
632     try std.testing.expectError(
633         error.HeaderCapacityExceeded,
634         Response.switchingProtocols(
635             .{ .headers = &handshake_headers, .head = &head },
636             "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
637         ),
638     );
639     try std.testing.expectEqualStrings("first", handshake_headers[0].name);
640     try std.testing.expectEqualStrings("second", handshake_headers[1].name);
641 }
642 
643 test "Response serialization accepts exact head capacity" {
644     comptime {
645         @stardustClaim(
646             @import("alloc_phase").capacity.witness(Storage, "http_response_boundary"),
647             null,
648             null,
649             null,
650             null,
651             null,
652             null,
653         );
654     }
655 
656     const expected = "HTTP/1.1 200 OK\r\n\r\n";
657     var headers: [0]Header = .{};
658     var exact_head: [expected.len]u8 = undefined;
659     const exact = Response.init(.{ .headers = &headers, .head = &exact_head });
660     const serialized = try exact.serialize();
661     try std.testing.expectEqualStrings(expected, serialized.head);
662     try std.testing.expectEqual(@as(?[]const u8, null), serialized.body);
663 
664     var short_head: [expected.len - 1]u8 = undefined;
665     const short = Response.init(.{ .headers = &headers, .head = &short_head });
666     try std.testing.expectError(error.HeadCapacityExceeded, short.serialize());
667 }
668 
669 test "Response serialization remains allocation-free after storage seals" {
670     comptime {
671         @stardustClaim(
672             @import("alloc_phase").capacity.witness(Storage, "http_response_sealed"),
673             null,
674             null,
675             null,
676             null,
677             null,
678             null,
679         );
680     }
681 
682     var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
683     var storage = Storage.init(phase_allocator.initializationAllocator(), .{
684         .response_count = 1,
685         .header_count_per_response = 2,
686         .head_bytes_per_response = 128,
687     }) catch |err| {
688         phase_allocator.abortInitialization();
689         phase_allocator.deinit();
690         return err;
691     };
692     errdefer {
693         if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
694         if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
695         if (storage.phase != .teardown) storage.deinit(phase_allocator.teardownAllocator());
696         phase_allocator.deinit();
697     }
698 
699     const pointer = storage.bytes.ptr;
700     const capacity = storage.capacity;
701     phase_allocator.seal();
702     storage.activate();
703     var response = Response.init(try storage.response(0));
704     try response.setHeader("Content-Type", "text/plain");
705     response.body = "Hello";
706     const serialized = try response.serialize();
707     try std.testing.expectEqualStrings("Hello", serialized.body.?);
708     try std.testing.expect(storage.bytes.ptr == pointer);
709     try std.testing.expectEqual(capacity, storage.capacity);
710     try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
711 
712     phase_allocator.beginTeardown();
713     storage.deinit(phase_allocator.teardownAllocator());
714     phase_allocator.deinit();
715 }
716 
717 const TestResponse = struct {
718     storage: Storage,
719     response: Response,
720 
721     fn init() !TestResponse {
722         var storage = try Storage.init(std.testing.allocator, .{
723             .response_count = 1,
724             .header_count_per_response = default_header_count,
725             .head_bytes_per_response = default_head_bytes,
726         });
727         errdefer storage.deinit(std.testing.allocator);
728         storage.activate();
729         return .{
730             .response = Response.init(try storage.response(0)),
731             .storage = storage,
732         };
733     }
734 
735     fn deinit(self: *TestResponse) void {
736         self.response.deinit();
737         self.storage.deinit(std.testing.allocator);
738     }
739 };
740 
741 test "Response serializes borrowed body separately from its head" {
742     var owned = try TestResponse.init();
743     defer owned.deinit();
744     owned.response.body = "Hello, World!";
745     const serialized = try owned.response.serialize();
746 
747     try std.testing.expect(std.mem.startsWith(u8, serialized.head, "HTTP/1.1 200 OK\r\n"));
748     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Length: 13\r\n") != null);
749     try std.testing.expect(std.mem.endsWith(u8, serialized.head, "\r\n\r\n"));
750     try std.testing.expectEqualStrings("Hello, World!", serialized.body.?);
751 }
752 
753 test "Response HEAD preserves length and omits body" {
754     var owned = try TestResponse.init();
755     defer owned.deinit();
756     owned.response.body = "Hello, World!";
757     const serialized = try owned.response.serializeForMethod(.HEAD);
758 
759     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Length: 13\r\n") != null);
760     try std.testing.expectEqual(@as(?[]const u8, null), serialized.body);
761 }
762 
763 test "Response no-content status omits body framing" {
764     var owned = try TestResponse.init();
765     defer owned.deinit();
766     owned.response.status = 204;
767     owned.response.status_text = "No Content";
768     owned.response.body = "ignored";
769     try owned.response.setHeader("Content-Length", "7");
770     try owned.response.setHeader("Transfer-Encoding", "chunked");
771     const serialized = try owned.response.serialize();
772 
773     try std.testing.expect(std.mem.startsWith(u8, serialized.head, "HTTP/1.1 204 No Content\r\n"));
774     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Length") == null);
775     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Transfer-Encoding") == null);
776     try std.testing.expectEqual(@as(?[]const u8, null), serialized.body);
777 }
778 
779 test "Response not-modified status preserves length and omits body" {
780     var owned = try TestResponse.init();
781     defer owned.deinit();
782     owned.response.status = 304;
783     owned.response.status_text = "Not Modified";
784     owned.response.body = "cached body";
785     const serialized = try owned.response.serialize();
786 
787     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Length: 11\r\n") != null);
788     try std.testing.expectEqual(@as(?[]const u8, null), serialized.body);
789 }
790 
791 test "Response informational status omits body framing" {
792     var owned = try TestResponse.init();
793     defer owned.deinit();
794     owned.response.status = 103;
795     owned.response.status_text = "Early Hints";
796     owned.response.body = "ignored";
797     try owned.response.setHeader("Content-Length", "7");
798     const serialized = try owned.response.serialize();
799 
800     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Length") == null);
801     try std.testing.expectEqual(@as(?[]const u8, null), serialized.body);
802 }
803 
804 test "Response custom headers replace case-insensitive duplicates" {
805     var owned = try TestResponse.init();
806     defer owned.deinit();
807     try owned.response.setHeader("X-Custom", "first");
808     try owned.response.setHeader("x-custom", "second");
809     try owned.response.setHeader("Content-Type", "text/plain");
810     const serialized = try owned.response.serialize();
811 
812     try std.testing.expectEqual(@as(usize, 2), owned.response.headers.count());
813     try std.testing.expectEqualStrings("second", owned.response.headers.get("X-Custom").?);
814     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "x-custom: second\r\n") != null);
815     try std.testing.expect(std.mem.indexOf(u8, serialized.head, "Content-Type: text/plain\r\n") != null);
816 }
817 
818 const ReleaseState = struct {
819     count: usize = 0,
820     bytes: usize = 0,
821 };
822 
823 fn recordRelease(state: *ReleaseState, body: []const u8) void {
824     state.count += 1;
825     state.bytes += body.len;
826 }
827 
828 test "Response releases transferred bodies on replacement and teardown" {
829     var owned = try TestResponse.init();
830     defer owned.deinit();
831     var state = ReleaseState{};
832 
833     owned.response.setBodyWithRelease("first", &state, recordRelease);
834     owned.response.setBodyWithRelease("second", &state, recordRelease);
835     try std.testing.expectEqual(@as(usize, 1), state.count);
836     try std.testing.expectEqual(@as(usize, "first".len), state.bytes);
837     owned.response.deinit();
838     try std.testing.expectEqual(@as(usize, 2), state.count);
839     try std.testing.expectEqual(@as(usize, "first".len + "second".len), state.bytes);
840 }
841 
842 test "Response releases the transferred slice after direct body replacement" {
843     var owned = try TestResponse.init();
844     defer owned.deinit();
845     var state = ReleaseState{};
846 
847     owned.response.setBodyWithRelease("transferred", &state, recordRelease);
848     owned.response.body = "borrowed";
849     const serialized = try owned.response.serialize();
850     try std.testing.expectEqualStrings("borrowed", serialized.body.?);
851     owned.response.deinit();
852     try std.testing.expectEqual(@as(usize, 1), state.count);
853     try std.testing.expectEqual(@as(usize, "transferred".len), state.bytes);
854 }
855 
856 test "Response switchingProtocols populates handshake headers" {
857     var owned = try TestResponse.init();
858     defer owned.deinit();
859     const response = try Response.switchingProtocols(
860         try owned.storage.response(0),
861         "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
862     );
863 
864     try std.testing.expectEqual(@as(u16, 101), response.status);
865     try std.testing.expectEqualStrings("Switching Protocols", response.status_text);
866     try std.testing.expectEqualStrings("websocket", response.headers.get("Upgrade").?);
867     try std.testing.expectEqualStrings("Upgrade", response.headers.get("Connection").?);
868     try std.testing.expectEqualStrings(
869         "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
870         response.headers.get("Sec-WebSocket-Accept").?,
871     );
872 }