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 }