lib/http/src/client/response.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const http = @import("../root.zig");
4 const field = @import("../field/root.zig");
5 const chunks = http.chunk;
6
7 pub const default_header_count: usize = 100;
8 pub const default_head_bytes: usize = 16 * 1024;
9 pub const default_body_bytes: usize = 16 * 1024 * 1024;
10
11 pub const Header = struct {
12 name: []const u8,
13 value: []const u8,
14 };
15
16 pub const Limits = struct {
17 response_count: usize,
18 header_count_per_response: usize,
19 head_bytes_per_response: usize,
20 body_bytes_per_response: usize,
21 };
22
23 pub const Capacity = struct {
24 response_count: usize,
25 header_count_per_response: usize,
26 head_bytes_per_response: usize,
27 body_bytes_per_response: usize,
28 header_count: usize,
29 header_bytes: usize,
30 head_bytes: usize,
31 body_bytes: usize,
32 storage_bytes: usize,
33
34 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
35 const header_count = try alloc_phase.capacity.mul(
36 usize,
37 limits.response_count,
38 limits.header_count_per_response,
39 );
40 const header_bytes = try alloc_phase.capacity.mul(
41 usize,
42 header_count,
43 @sizeOf(Header),
44 );
45 const head_bytes = try alloc_phase.capacity.mul(
46 usize,
47 limits.response_count,
48 limits.head_bytes_per_response,
49 );
50 const body_bytes = try alloc_phase.capacity.mul(
51 usize,
52 limits.response_count,
53 limits.body_bytes_per_response,
54 );
55 const header_and_head_bytes = try alloc_phase.capacity.add(
56 usize,
57 header_bytes,
58 head_bytes,
59 );
60 const storage_bytes = try alloc_phase.capacity.add(
61 usize,
62 header_and_head_bytes,
63 body_bytes,
64 );
65 return .{
66 .response_count = limits.response_count,
67 .header_count_per_response = limits.header_count_per_response,
68 .head_bytes_per_response = limits.head_bytes_per_response,
69 .body_bytes_per_response = limits.body_bytes_per_response,
70 .header_count = header_count,
71 .header_bytes = header_bytes,
72 .head_bytes = head_bytes,
73 .body_bytes = body_bytes,
74 .storage_bytes = storage_bytes,
75 };
76 }
77 };
78
79 pub const Scratch = struct {
80 headers: []Header,
81 head: []u8,
82 body: []u8,
83 };
84
85 pub const Error = error{
86 MalformedResponse,
87 ReadFailed,
88 ResponseHeaderCapacityExceeded,
89 ResponseHeadCapacityExceeded,
90 ResponseBodyCapacityExceeded,
91 };
92
93 pub const ClientResponse = struct {
94 status: u16,
95 headers: []const Header,
96 body: []const u8,
97 connection_reusable: bool,
98
99 pub fn header(self: ClientResponse, name: []const u8) ?[]const u8 {
100 for (self.headers) |item| {
101 if (std.ascii.eqlIgnoreCase(item.name, name)) return item.value;
102 }
103 return null;
104 }
105
106 /// Parses one complete response out of `data` and copies its bytes into
107 /// `scratch` . The returned headers and body are slices of `scratch` , so they
108 /// stay valid until that scratch is parsed into again. A chunked body is
109 /// decoded in place inside the body region, over the chunk framing already
110 /// written there. Capacity is checked before any byte is copied, so a scratch
111 /// too small for the headers, the head, or the body returns an error and
112 /// publishes nothing. A caller can enlarge the scratch and call again.
113 pub fn parse(scratch: Scratch, data: []const u8) Error!ClientResponse {
114 return parseWithMethod(scratch, data, null);
115 }
116
117 pub fn parseForMethod(scratch: Scratch, data: []const u8, method: []const u8) Error!ClientResponse {
118 return parseWithMethod(scratch, data, method);
119 }
120 };
121
122 pub const Framing = union(enum) {
123 none,
124 tunnel,
125 fixed: usize,
126 chunked,
127 close,
128 };
129
130 pub const VersionClass = enum {
131 http_1_0,
132 http_1_1_or_later,
133 };
134
135 pub const HeadSurvey = struct {
136 version: VersionClass,
137 status: u16,
138 header_count: usize,
139 framing: Framing,
140 };
141
142 const Survey = struct {
143 head_length: usize,
144 head: HeadSurvey,
145 body_length: usize,
146 chunk_capacity: ?chunks.Capacity,
147 };
148
149 fn parseWithMethod(scratch: Scratch, data: []const u8, method: ?[]const u8) Error!ClientResponse {
150 const survey = try surveyResponse(data, method);
151 try checkCapacity(scratch, survey);
152
153 @memcpy(scratch.head[0..survey.head_length], data[0..survey.head_length]);
154 const body_source = data[survey.head_length..][0..survey.body_length];
155 @memcpy(scratch.body[0..survey.body_length], body_source);
156 return materialize(
157 scratch,
158 survey.head_length,
159 survey.body_length,
160 survey.head,
161 survey.chunk_capacity,
162 );
163 }
164
165 pub fn read(scratch: Scratch, reader: *std.Io.Reader, method: []const u8) Error!ClientResponse {
166 var head_length: usize = 0;
167 var body_length: usize = 0;
168 var head_survey: ?HeadSurvey = null;
169
170 while (true) {
171 const read_buffer = readAvailable(reader) catch {
172 return error.ReadFailed;
173 };
174 const read_length = read_buffer.len;
175 if (read_length == 0) {
176 const head = head_survey orelse {
177 if (head_length == 0) return error.ReadFailed;
178 return error.MalformedResponse;
179 };
180 return switch (head.framing) {
181 .none, .tunnel => materialize(scratch, head_length, 0, head, null),
182 .fixed => |expected| if (body_length == expected)
183 materialize(scratch, head_length, body_length, head, null)
184 else
185 error.MalformedResponse,
186 .chunked => error.MalformedResponse,
187 .close => materialize(scratch, head_length, body_length, head, null),
188 };
189 }
190 var toss_length = read_length;
191 defer reader.toss(toss_length);
192
193 var offset: usize = 0;
194 if (head_survey == null) {
195 while (offset < read_length) : (offset += 1) {
196 if (head_length == scratch.head.len) {
197 return error.ResponseHeadCapacityExceeded;
198 }
199 scratch.head[head_length] = read_buffer[offset];
200 head_length += 1;
201 if (!std.mem.endsWith(u8, scratch.head[0..head_length], "\r\n\r\n")) {
202 continue;
203 }
204 const head = try surveyHead(scratch.head[0..head_length], method);
205 if (head.header_count > scratch.headers.len) {
206 return error.ResponseHeaderCapacityExceeded;
207 }
208 switch (head.framing) {
209 .fixed => |expected| if (expected > scratch.body.len) {
210 return error.ResponseBodyCapacityExceeded;
211 },
212 else => {},
213 }
214 head_survey = head;
215 offset += 1;
216 if (head.framing == .none or head.framing == .tunnel) {
217 toss_length = offset;
218 return materialize(scratch, head_length, 0, head, null);
219 }
220 if (head.framing == .fixed and head.framing.fixed == 0) {
221 toss_length = offset;
222 return materialize(scratch, head_length, 0, head, null);
223 }
224 break;
225 }
226 }
227
228 const head = head_survey orelse continue;
229 const input = read_buffer[offset..read_length];
230 switch (head.framing) {
231 .none, .tunnel => return materialize(scratch, head_length, 0, head, null),
232 .fixed => |expected| {
233 const take = @min(expected - body_length, input.len);
234 @memcpy(scratch.body[body_length..][0..take], input[0..take]);
235 body_length += take;
236 if (body_length == expected) {
237 toss_length = offset + take;
238 return materialize(scratch, head_length, body_length, head, null);
239 }
240 },
241 .chunked => {
242 const previous_body_length = body_length;
243 const room = scratch.body.len - body_length;
244 const take = @min(room, input.len);
245 @memcpy(scratch.body[body_length..][0..take], input[0..take]);
246 body_length += take;
247 switch (chunks.scanBody(scratch.body[0..body_length])) {
248 .complete => |consumed| {
249 std.debug.assert(consumed >= previous_body_length);
250 std.debug.assert(consumed <= body_length);
251 const consumed_input = consumed - previous_body_length;
252 std.debug.assert(consumed_input <= take);
253 toss_length = offset + consumed_input;
254 body_length = consumed;
255 return materialize(
256 scratch,
257 head_length,
258 body_length,
259 head,
260 null,
261 );
262 },
263 .malformed => return error.MalformedResponse,
264 .incomplete => {},
265 }
266 if (take != input.len) return error.ResponseBodyCapacityExceeded;
267 },
268 .close => {
269 if (input.len > scratch.body.len - body_length) {
270 return error.ResponseBodyCapacityExceeded;
271 }
272 @memcpy(scratch.body[body_length..][0..input.len], input);
273 body_length += input.len;
274 },
275 }
276 }
277 }
278
279 fn surveyResponse(data: []const u8, method: ?[]const u8) Error!Survey {
280 const header_end = findHeaderEnd(data) orelse return error.MalformedResponse;
281 const head_length = header_end + 4;
282 const head = try surveyHead(data[0..head_length], method);
283 const body_data = data[head_length..];
284 var chunk_capacity: ?chunks.Capacity = null;
285 const body_length = switch (head.framing) {
286 .none, .tunnel => 0,
287 .fixed => |expected| blk: {
288 if (expected > body_data.len) return error.MalformedResponse;
289 break :blk expected;
290 },
291 .chunked => blk: {
292 const capacity = chunks.survey(body_data, null) catch {
293 return error.MalformedResponse;
294 };
295 chunk_capacity = capacity;
296 break :blk capacity.consumed;
297 },
298 .close => body_data.len,
299 };
300 return .{
301 .head_length = head_length,
302 .head = head,
303 .body_length = body_length,
304 .chunk_capacity = chunk_capacity,
305 };
306 }
307
308 pub fn surveyHead(head: []const u8, method: ?[]const u8) Error!HeadSurvey {
309 const parts = splitHead(head) orelse return error.MalformedResponse;
310 const status_line = try parseStatusLine(parts.status_line);
311 const fields = try surveyFields(parts.header_lines);
312 return .{
313 .version = status_line.version,
314 .status = status_line.status,
315 .header_count = fields.header_count,
316 .framing = try decideFraming(
317 status_line.version,
318 method,
319 status_line.status,
320 fields,
321 ),
322 };
323 }
324
325 const HeadParts = struct {
326 status_line: []const u8,
327 header_lines: []const u8,
328 };
329
330 fn splitHead(head: []const u8) ?HeadParts {
331 if (!std.mem.endsWith(u8, head, "\r\n\r\n")) return null;
332 const header_end = head.len - 4;
333 const first_line_end = std.mem.indexOf(
334 u8,
335 head[0 .. header_end + 2],
336 "\r\n",
337 ) orelse return null;
338 if (first_line_end > header_end) return null;
339 return .{
340 .status_line = head[0..first_line_end],
341 .header_lines = if (first_line_end == header_end)
342 ""
343 else
344 head[first_line_end + 2 .. header_end],
345 };
346 }
347
348 const FieldSurvey = struct {
349 header_count: usize = 0,
350 content_length: ?[]const u8 = null,
351 transfer_encoding: ?[]const u8 = null,
352 duplicate_content_length: bool = false,
353 duplicate_transfer_encoding: bool = false,
354 };
355
356 fn surveyFields(header_lines: []const u8) Error!FieldSurvey {
357 var survey = FieldSurvey{};
358 var lines = std.mem.splitSequence(u8, header_lines, "\r\n");
359 while (lines.next()) |line| {
360 if (line.len == 0) continue;
361 const parsed = field.parseLine(line) orelse return error.MalformedResponse;
362 survey.header_count += 1;
363 if (std.ascii.eqlIgnoreCase(parsed.name, "Content-Length")) {
364 if (survey.content_length != null) {
365 survey.duplicate_content_length = true;
366 } else {
367 survey.content_length = parsed.value;
368 }
369 }
370 if (std.ascii.eqlIgnoreCase(parsed.name, "Transfer-Encoding")) {
371 if (survey.transfer_encoding != null) {
372 survey.duplicate_transfer_encoding = true;
373 } else {
374 survey.transfer_encoding = parsed.value;
375 }
376 }
377 }
378 return survey;
379 }
380
381 fn decideFraming(
382 version: VersionClass,
383 method: ?[]const u8,
384 status: u16,
385 fields: FieldSurvey,
386 ) Error!Framing {
387 if (version == .http_1_0 and fields.transfer_encoding != null) {
388 return error.MalformedResponse;
389 }
390 if (method) |name| {
391 if (std.mem.eql(u8, name, "HEAD")) return .none;
392 }
393 if (status >= 100 and status < 200) return .none;
394 if (status == 204 or status == 304) return .none;
395 if (method) |name| {
396 if (std.mem.eql(u8, name, "CONNECT") and status >= 200 and status < 300) {
397 return .tunnel;
398 }
399 }
400 if (fields.duplicate_content_length or
401 fields.duplicate_transfer_encoding or
402 (fields.content_length != null and fields.transfer_encoding != null))
403 {
404 return error.MalformedResponse;
405 }
406 if (fields.transfer_encoding) |value| {
407 const final_coding = field.parseTransferEncoding(value) orelse {
408 return error.MalformedResponse;
409 };
410 return if (final_coding == .chunked) .chunked else .close;
411 }
412 if (fields.content_length) |value| {
413 const body_length = field.parseContentLength(value) orelse {
414 return error.MalformedResponse;
415 };
416 return .{ .fixed = body_length };
417 }
418 return .close;
419 }
420
421 fn checkCapacity(scratch: Scratch, survey: Survey) Error!void {
422 if (survey.head.header_count > scratch.headers.len) {
423 return error.ResponseHeaderCapacityExceeded;
424 }
425 if (survey.head_length > scratch.head.len) {
426 return error.ResponseHeadCapacityExceeded;
427 }
428 if (survey.body_length > scratch.body.len) {
429 return error.ResponseBodyCapacityExceeded;
430 }
431 }
432
433 fn materialize(
434 scratch: Scratch,
435 head_length: usize,
436 body_length: usize,
437 survey: HeadSurvey,
438 surveyed_chunks: ?chunks.Capacity,
439 ) Error!ClientResponse {
440 const headers = fillHeaders(
441 scratch.headers,
442 scratch.head[0..head_length],
443 survey.header_count,
444 );
445 const body = switch (survey.framing) {
446 .none, .tunnel => @as([]const u8, &.{}),
447 .fixed => |expected| scratch.body[0..expected],
448 .chunked => blk: {
449 const capacity = surveyed_chunks orelse chunks.survey(
450 scratch.body[0..body_length],
451 null,
452 ) catch return error.MalformedResponse;
453 const decoded = chunks.decodeSurveyedInto(
454 scratch.body,
455 scratch.body[0..body_length],
456 capacity,
457 ) catch return error.MalformedResponse;
458 break :blk decoded.body;
459 },
460 .close => scratch.body[0..body_length],
461 };
462 return .{
463 .status = survey.status,
464 .headers = headers,
465 .body = body,
466 .connection_reusable = connectionReusable(
467 survey,
468 headers,
469 ),
470 };
471 }
472
473 fn connectionReusable(
474 survey: HeadSurvey,
475 headers: []const Header,
476 ) bool {
477 switch (survey.framing) {
478 .close, .tunnel => return false,
479 .none, .fixed, .chunked => {},
480 }
481 var keep_alive = false;
482 for (headers) |header| {
483 if (!std.ascii.eqlIgnoreCase(
484 header.name,
485 "Connection",
486 )) continue;
487 var tokens = std.mem.splitScalar(
488 u8,
489 header.value,
490 ',',
491 );
492 while (tokens.next()) |raw| {
493 const token = std.mem.trim(u8, raw, " \t");
494 if (std.ascii.eqlIgnoreCase(token, "close")) {
495 return false;
496 }
497 if (std.ascii.eqlIgnoreCase(
498 token,
499 "keep-alive",
500 )) {
501 keep_alive = true;
502 }
503 }
504 }
505 return switch (survey.version) {
506 .http_1_1_or_later => true,
507 .http_1_0 => keep_alive,
508 };
509 }
510
511 pub fn fillHeaders(entries: []Header, head: []const u8, expected_count: usize) []const Header {
512 const parts = splitHead(head).?;
513 var count: usize = 0;
514 var lines = std.mem.splitSequence(u8, parts.header_lines, "\r\n");
515 while (lines.next()) |line| {
516 if (line.len == 0) continue;
517 const parsed = field.parseLine(line).?;
518 entries[count] = .{
519 .name = parsed.name,
520 .value = parsed.value,
521 };
522 count += 1;
523 }
524 std.debug.assert(count == expected_count);
525 return entries[0..count];
526 }
527
528 const StatusLine = struct {
529 version: VersionClass,
530 status: u16,
531 };
532
533 fn parseStatusLine(status_line: []const u8) Error!StatusLine {
534 const first_space = std.mem.indexOfScalar(u8, status_line, ' ') orelse {
535 return error.MalformedResponse;
536 };
537 const version = parseVersion(status_line[0..first_space]) orelse {
538 return error.MalformedResponse;
539 };
540 const after_first = status_line[first_space + 1 ..];
541 if (after_first.len < 4 or after_first[3] != ' ') {
542 return error.MalformedResponse;
543 }
544 const status = parseStatusCode(after_first[0..3]) orelse
545 return error.MalformedResponse;
546 if (!field.validValue(after_first[4..])) return error.MalformedResponse;
547 return .{ .version = version, .status = status };
548 }
549
550 fn parseStatusCode(text: []const u8) ?u16 {
551 if (text.len != 3) return null;
552 for (text) |byte| {
553 if (byte < '0' or byte > '9') return null;
554 }
555 return @as(u16, text[0] - '0') * 100 +
556 @as(u16, text[1] - '0') * 10 +
557 @as(u16, text[2] - '0');
558 }
559
560 fn parseVersion(text: []const u8) ?VersionClass {
561 if (text.len != "HTTP/1.0".len) return null;
562 if (!std.mem.eql(u8, text[0..7], "HTTP/1.")) return null;
563 if (text[7] < '0' or text[7] > '9') return null;
564 return if (text[7] == '0') .http_1_0 else .http_1_1_or_later;
565 }
566
567 fn findHeaderEnd(data: []const u8) ?usize {
568 return std.mem.indexOf(u8, data, "\r\n\r\n");
569 }
570
571 fn readAvailable(reader: *std.Io.Reader) ![]const u8 {
572 return reader.peekGreedy(1) catch |err| switch (err) {
573 error.EndOfStream => return &.{},
574 else => return err,
575 };
576 }
577
578 fn independentCapacity(limits: Limits) error{CapacityOverflow}!Capacity {
579 const header_count = @as(u128, limits.response_count) * limits.header_count_per_response;
580 const header_bytes = header_count * @sizeOf(Header);
581 const head_bytes = @as(u128, limits.response_count) * limits.head_bytes_per_response;
582 const body_bytes = @as(u128, limits.response_count) * limits.body_bytes_per_response;
583 const storage_bytes = header_bytes + head_bytes + body_bytes;
584 if (header_count > std.math.maxInt(usize) or
585 header_bytes > std.math.maxInt(usize) or
586 head_bytes > std.math.maxInt(usize) or
587 body_bytes > std.math.maxInt(usize) or
588 storage_bytes > std.math.maxInt(usize))
589 {
590 return error.CapacityOverflow;
591 }
592 return .{
593 .response_count = limits.response_count,
594 .header_count_per_response = limits.header_count_per_response,
595 .head_bytes_per_response = limits.head_bytes_per_response,
596 .body_bytes_per_response = limits.body_bytes_per_response,
597 .header_count = @intCast(header_count),
598 .header_bytes = @intCast(header_bytes),
599 .head_bytes = @intCast(head_bytes),
600 .body_bytes = @intCast(body_bytes),
601 .storage_bytes = @intCast(storage_bytes),
602 };
603 }
604
605 test "Client response capacity matches independent arithmetic" {
606 comptime {
607 @stardustClaim(
608 @import("alloc_phase").capacity.witness(@import("./root.zig").ClientResponseStorage, "http_client_response_capacity"),
609 null,
610 null,
611 null,
612 null,
613 null,
614 null,
615 );
616 }
617
618 for (0..9) |response_count| {
619 for (0..9) |header_count_per_response| {
620 for (0..9) |head_bytes_per_response| {
621 for (0..9) |body_bytes_per_response| {
622 const limits = Limits{
623 .response_count = response_count,
624 .header_count_per_response = header_count_per_response,
625 .head_bytes_per_response = head_bytes_per_response,
626 .body_bytes_per_response = body_bytes_per_response,
627 };
628 try std.testing.expectEqual(
629 try independentCapacity(limits),
630 try Capacity.derive(limits),
631 );
632 }
633 }
634 }
635 }
636 const maximum = std.math.maxInt(usize);
637 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
638 .response_count = 2,
639 .header_count_per_response = maximum,
640 .head_bytes_per_response = 0,
641 .body_bytes_per_response = 0,
642 }));
643 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
644 .response_count = 1,
645 .header_count_per_response = maximum,
646 .head_bytes_per_response = 0,
647 .body_bytes_per_response = 0,
648 }));
649 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
650 .response_count = maximum,
651 .header_count_per_response = 0,
652 .head_bytes_per_response = 2,
653 .body_bytes_per_response = 0,
654 }));
655 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
656 .response_count = maximum,
657 .header_count_per_response = 0,
658 .head_bytes_per_response = 0,
659 .body_bytes_per_response = 2,
660 }));
661 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
662 .response_count = 1,
663 .header_count_per_response = 1,
664 .head_bytes_per_response = maximum,
665 .body_bytes_per_response = 0,
666 }));
667 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
668 .response_count = 1,
669 .header_count_per_response = 0,
670 .head_bytes_per_response = 1,
671 .body_bytes_per_response = maximum,
672 }));
673 }
674
675 test "Client response capacity failures preserve result storage" {
676 comptime {
677 @stardustClaim(
678 @import("alloc_phase").capacity.witness(@import("./root.zig").ClientResponseStorage, "http_client_response_atomic"),
679 null,
680 null,
681 null,
682 null,
683 null,
684 null,
685 );
686 }
687
688 const two_headers = "HTTP/1.1 200 OK\r\nX-One: one\r\nX-Two: two\r\nContent-Length: 5\r\n\r\nHello";
689 var headers = [_]Header{.{ .name = "sentinel", .value = "value" }};
690 var head = @as([128]u8, @splat(0xA5));
691 var body = @as([8]u8, @splat(0x5A));
692
693 try std.testing.expectError(
694 error.ResponseHeaderCapacityExceeded,
695 ClientResponse.parse(.{ .headers = &headers, .head = &head, .body = &body }, two_headers),
696 );
697 try std.testing.expectEqualStrings("sentinel", headers[0].name);
698 try std.testing.expectEqualSlices(u8, &(@as([128]u8, @splat(0xA5))), &head);
699 try std.testing.expectEqualSlices(u8, &(@as([8]u8, @splat(0x5A))), &body);
700
701 var enough_headers: [3]Header = undefined;
702 var short_head = @as([8]u8, @splat(0xA5));
703 try std.testing.expectError(
704 error.ResponseHeadCapacityExceeded,
705 ClientResponse.parse(.{ .headers = &enough_headers, .head = &short_head, .body = &body }, two_headers),
706 );
707 try std.testing.expectEqualSlices(u8, &(@as([8]u8, @splat(0xA5))), &short_head);
708 try std.testing.expectEqualSlices(u8, &(@as([8]u8, @splat(0x5A))), &body);
709
710 var short_body = @as([4]u8, @splat(0x5A));
711 try std.testing.expectError(
712 error.ResponseBodyCapacityExceeded,
713 ClientResponse.parse(.{ .headers = &enough_headers, .head = &head, .body = &short_body }, two_headers),
714 );
715 try std.testing.expectEqualSlices(u8, &(@as([128]u8, @splat(0xA5))), &head);
716 try std.testing.expectEqualSlices(u8, &(@as([4]u8, @splat(0x5A))), &short_body);
717 }
718
719 test "Client response parsing accepts exact capacities" {
720 comptime {
721 @stardustClaim(
722 @import("alloc_phase").capacity.witness(@import("./root.zig").ClientResponseStorage, "http_client_response_boundary"),
723 null,
724 null,
725 null,
726 null,
727 null,
728 null,
729 );
730 }
731
732 const head_text = "HTTP/1.1 200 OK\r\nX-One: one\r\nContent-Length: 5\r\n\r\n";
733 const raw = head_text ++ "Hello";
734 var headers: [2]Header = undefined;
735 var head: [head_text.len]u8 = undefined;
736 var body: [5]u8 = undefined;
737 const parsed = try ClientResponse.parse(
738 .{ .headers = &headers, .head = &head, .body = &body },
739 raw,
740 );
741
742 try std.testing.expectEqual(@as(u16, 200), parsed.status);
743 try std.testing.expectEqualStrings("one", parsed.header("x-one").?);
744 try std.testing.expectEqualStrings("Hello", parsed.body);
745 }
746
747 test "Client response parsing preserves HTTP body semantics" {
748 var headers: [4]Header = undefined;
749 var head_storage: [256]u8 = undefined;
750 var body: [128]u8 = undefined;
751 const scratch = Scratch{
752 .headers = &headers,
753 .head = &head_storage,
754 .body = &body,
755 };
756
757 const head = try ClientResponse.parseForMethod(
758 scratch,
759 "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello",
760 "HEAD",
761 );
762 try std.testing.expectEqual(@as(usize, 0), head.body.len);
763 const no_content = try ClientResponse.parse(
764 scratch,
765 "HTTP/1.1 204 No Content\r\nContent-Length: 5\r\n\r\nHello",
766 );
767 try std.testing.expectEqual(@as(usize, 0), no_content.body.len);
768 const chunked = try ClientResponse.parse(
769 scratch,
770 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip, CHUNKED\r\n\r\n2\r\nHe\r\n3\r\nllo\r\n0\r\nDigest: value\r\n\r\n",
771 );
772 try std.testing.expectEqualStrings("Hello", chunked.body);
773 }
774
775 test "Client response head survey rejects malformed field syntax" {
776 const malformed = [_][]const u8{
777 "HTTP/1.1 200 OK\r\nBadHeader\r\n\r\n",
778 "HTTP/1.1 200 OK\r\n: value\r\n\r\n",
779 "HTTP/1.1 200 OK\r\nBad Header: value\r\n\r\n",
780 "HTTP/1.1 200 OK\r\nContent-Length : 0\r\n\r\n",
781 "HTTP/1.1 200 OK\r\nX-Test: bad\x00value\r\n\r\n",
782 "HTTP/1.1 200 OK\r\nX-Test: bad\x0bvalue\r\n\r\n",
783 "HTTP/1.1 200 OK\r\nX-Test: bad\x7fvalue\r\n\r\n",
784 };
785 for (malformed) |head| {
786 try std.testing.expectError(
787 error.MalformedResponse,
788 surveyHead(head, "HEAD"),
789 );
790 }
791 }
792
793 test "Client response status line enforces exact grammar" {
794 const empty_reason = try parseStatusLine("HTTP/1.0 000 ");
795 try std.testing.expectEqual(VersionClass.http_1_0, empty_reason.version);
796 try std.testing.expectEqual(@as(u16, 0), empty_reason.status);
797
798 const later = try parseStatusLine("HTTP/1.9 999 \tVisible\x80");
799 try std.testing.expectEqual(VersionClass.http_1_1_or_later, later.version);
800 try std.testing.expectEqual(@as(u16, 999), later.status);
801 try std.testing.expectEqual(
802 @as(u16, 200),
803 (try parseStatusLine("HTTP/1.1 200 OK")).status,
804 );
805
806 const malformed = [_][]const u8{
807 "HTTP/1.1 +200 ",
808 "HTTP/1.1 -00 ",
809 "HTTP/1.1 2_00 ",
810 "HTTP/1.1 20 ",
811 "HTTP/1.1 2000 ",
812 "HTTP/1.1 200",
813 "HTTP/1.1 200\tReason",
814 "HTTP/1.1\t200 ",
815 "HTTP/1.1 200 ",
816 "HTTP/1.1 200 OK\x00",
817 "HTTP/1.1 200 OK\x7f",
818 "HTTP/1.1 200 OK\r",
819 };
820 for (malformed) |status_line| {
821 try std.testing.expectError(
822 error.MalformedResponse,
823 parseStatusLine(status_line),
824 );
825 }
826
827 const code_template = "HTTP/1.1 200 ";
828 var code_line: [code_template.len]u8 = undefined;
829 for (9..12) |position| {
830 for (0..256) |raw| {
831 @memcpy(&code_line, code_template);
832 const byte: u8 = @intCast(raw);
833 code_line[position] = byte;
834 if (byte >= '0' and byte <= '9') {
835 _ = try parseStatusLine(&code_line);
836 } else {
837 try std.testing.expectError(
838 error.MalformedResponse,
839 parseStatusLine(&code_line),
840 );
841 }
842 }
843 }
844
845 for ([_]usize{ 8, 12 }) |position| {
846 for (0..256) |raw| {
847 @memcpy(&code_line, code_template);
848 const byte: u8 = @intCast(raw);
849 code_line[position] = byte;
850 if (byte == ' ') {
851 _ = try parseStatusLine(&code_line);
852 } else {
853 try std.testing.expectError(
854 error.MalformedResponse,
855 parseStatusLine(&code_line),
856 );
857 }
858 }
859 }
860
861 const reason_template = "HTTP/1.1 200 ABC";
862 var reason_line: [reason_template.len]u8 = undefined;
863 for (13..reason_template.len) |position| {
864 for (0..256) |raw| {
865 @memcpy(&reason_line, reason_template);
866 const byte: u8 = @intCast(raw);
867 reason_line[position] = byte;
868 const allowed = byte == '\t' or
869 (byte >= 0x20 and byte != 0x7f);
870 if (allowed) {
871 _ = try parseStatusLine(&reason_line);
872 } else {
873 try std.testing.expectError(
874 error.MalformedResponse,
875 parseStatusLine(&reason_line),
876 );
877 }
878 }
879 }
880
881 @memcpy(&code_line, code_template);
882 for (0..1000) |raw| {
883 code_line[9] = @intCast('0' + raw / 100);
884 code_line[10] = @intCast('0' + (raw / 10) % 10);
885 code_line[11] = @intCast('0' + raw % 10);
886 try std.testing.expectEqual(
887 @as(u16, @intCast(raw)),
888 (try parseStatusLine(&code_line)).status,
889 );
890 }
891 }
892
893 test "Client response rejects transfer encoding on HTTP 1.0" {
894 const faulty = [_]struct {
895 head: []const u8,
896 method: []const u8,
897 }{
898 .{
899 .head = "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
900 .method = "GET",
901 },
902 .{
903 .head = "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
904 .method = "HEAD",
905 },
906 .{
907 .head = "HTTP/1.0 204 No Content\r\nTransfer-Encoding: gzip\r\n\r\n",
908 .method = "GET",
909 },
910 .{
911 .head = "HTTP/1.0 200 Connected\r\nTransfer-Encoding: , ,\r\n\r\n",
912 .method = "CONNECT",
913 },
914 };
915 for (faulty) |case| {
916 try std.testing.expectError(
917 error.MalformedResponse,
918 surveyHead(case.head, case.method),
919 );
920 }
921
922 const fixed = try surveyHead(
923 "HTTP/1.0 200 OK\r\nContent-Length: 3\r\n\r\n",
924 "GET",
925 );
926 try std.testing.expectEqual(VersionClass.http_1_0, fixed.version);
927 try std.testing.expect(fixed.framing == .fixed);
928
929 const later = try surveyHead(
930 "HTTP/1.9 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
931 "GET",
932 );
933 try std.testing.expectEqual(VersionClass.http_1_1_or_later, later.version);
934 try std.testing.expect(later.framing == .chunked);
935 }
936
937 test "Client response head survey applies ordered framing" {
938 const nonfinal = try surveyHead(
939 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked,\tGZIP \r\n\r\n",
940 "GET",
941 );
942 try std.testing.expect(nonfinal.framing == .close);
943
944 const final = try surveyHead(
945 "HTTP/1.1 200 OK\r\ntRaNsFeR-EnCoDiNg:\t , tiny-coding; level=5, , CHUNKED, \t\r\n\r\n",
946 "GET",
947 );
948 try std.testing.expect(final.framing == .chunked);
949
950 const other = try surveyHead(
951 "HTTP/1.1 200 OK\r\nTransfer-Encoding: deflate\r\n\r\n",
952 "GET",
953 );
954 try std.testing.expect(other.framing == .close);
955
956 const empty = try surveyHead(
957 "HTTP/1.1 200 OK\r\nTransfer-Encoding: , ,\r\n\r\n",
958 "GET",
959 );
960 try std.testing.expect(empty.framing == .close);
961
962 const fixed = try surveyHead(
963 "HTTP/1.1 205 Reset Content\r\nContent-Length: 3\r\n\r\n",
964 "GET",
965 );
966 switch (fixed.framing) {
967 .fixed => |length| try std.testing.expectEqual(@as(usize, 3), length),
968 else => return error.TestUnexpectedFraming,
969 }
970
971 const fixed_zero = try surveyHead(
972 "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
973 "GET",
974 );
975 switch (fixed_zero.framing) {
976 .fixed => |length| try std.testing.expectEqual(@as(usize, 0), length),
977 else => return error.TestUnexpectedFraming,
978 }
979
980 const unframed = try surveyHead(
981 "HTTP/1.1 305 Use Proxy\r\nX-Test: value\r\n\r\n",
982 "GET",
983 );
984 try std.testing.expect(unframed.framing == .close);
985 }
986
987 test "Client response head survey rejects ordinary framing ambiguity" {
988 const malformed = [_][]const u8{
989 "HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Length: 3\r\n\r\n",
990 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\nTransfer-Encoding: chunked\r\n\r\n",
991 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\nContent-Length: 3\r\n\r\n",
992 "HTTP/1.1 200 OK\r\nTransfer-Encoding: tiny-coding; name=\r\n\r\n",
993 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked; name=value\r\n\r\n",
994 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip; name=value\r\n\r\n",
995 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\n\r\n",
996 "HTTP/1.1 200 OK\r\nContent-Length: invalid\r\n\r\n",
997 "HTTP/1.1 200 OK\r\nContent-Length: +3\r\n\r\n",
998 "HTTP/1.1 200 OK\r\nContent-Length: -0\r\n\r\n",
999 "HTTP/1.1 200 OK\r\nContent-Length: 1_0\r\n\r\n",
1000 "HTTP/1.1 200 OK\r\nContent-Length: 184467440737095516160\r\n\r\n",
1001 };
1002 for (malformed) |head| {
1003 try std.testing.expectError(
1004 error.MalformedResponse,
1005 surveyHead(head, "GET"),
1006 );
1007 }
1008 }
1009
1010 test "Client response method and status rules precede framing headers" {
1011 const ambiguous = "Transfer-Encoding: gzip\r\nContent-Length: 3\r\n";
1012 const head = try surveyHead(
1013 "HTTP/1.1 200 OK\r\n" ++ ambiguous ++ "\r\n",
1014 "HEAD",
1015 );
1016 try std.testing.expect(head.framing == .none);
1017
1018 const informational = try surveyHead(
1019 "HTTP/1.1 199 Informational\r\n" ++ ambiguous ++ "\r\n",
1020 "GET",
1021 );
1022 try std.testing.expect(informational.framing == .none);
1023
1024 const no_content = try surveyHead(
1025 "HTTP/1.1 204 No Content\r\n" ++ ambiguous ++ "\r\n",
1026 "GET",
1027 );
1028 try std.testing.expect(no_content.framing == .none);
1029
1030 const connect_no_content = try surveyHead(
1031 "HTTP/1.1 204 No Content\r\n" ++ ambiguous ++ "\r\n",
1032 "CONNECT",
1033 );
1034 try std.testing.expect(connect_no_content.framing == .none);
1035
1036 const not_modified = try surveyHead(
1037 "HTTP/1.1 304 Not Modified\r\n" ++ ambiguous ++ "\r\n",
1038 "GET",
1039 );
1040 try std.testing.expect(not_modified.framing == .none);
1041
1042 const tunnel = try surveyHead(
1043 "HTTP/1.1 299 Connected\r\n" ++ ambiguous ++ "\r\n",
1044 "CONNECT",
1045 );
1046 try std.testing.expect(tunnel.framing == .tunnel);
1047
1048 try std.testing.expectError(
1049 error.MalformedResponse,
1050 surveyHead(
1051 "HTTP/1.1 305 Proxy\r\n" ++ ambiguous ++ "\r\n",
1052 "CONNECT",
1053 ),
1054 );
1055
1056 const extension = try surveyHead(
1057 "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\n",
1058 "head",
1059 );
1060 try std.testing.expect(extension.framing == .fixed);
1061 }
1062
1063 test "Client response parsing retains nonfinal transfer coding semantics" {
1064 var headers: [2]Header = undefined;
1065 var head_storage: [128]u8 = undefined;
1066 var body: [32]u8 = undefined;
1067 const scratch = Scratch{
1068 .headers = &headers,
1069 .head = &head_storage,
1070 .body = &body,
1071 };
1072
1073 const below_informational = try ClientResponse.parseForMethod(
1074 scratch,
1075 "HTTP/1.1 099 Status\r\n\r\nraw",
1076 "GET",
1077 );
1078 try std.testing.expectEqual(@as(u16, 99), below_informational.status);
1079 try std.testing.expectEqualStrings("raw", below_informational.body);
1080
1081 const close = try ClientResponse.parseForMethod(
1082 scratch,
1083 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, tiny-coding; name=\"a,b\"\r\n\r\nraw",
1084 "GET",
1085 );
1086 try std.testing.expectEqualStrings("raw", close.body);
1087
1088 const tunnel = try ClientResponse.parseForMethod(
1089 scratch,
1090 "HTTP/1.1 200 Connected\r\nContent-Length: 3\r\n\r\nraw",
1091 "CONNECT",
1092 );
1093 try std.testing.expectEqual(@as(usize, 0), tunnel.body.len);
1094
1095 try std.testing.expectError(
1096 error.MalformedResponse,
1097 ClientResponse.parseForMethod(
1098 scratch,
1099 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\nContent-Length: 3\r\n\r\nraw",
1100 "GET",
1101 ),
1102 );
1103 }
1104
1105 test "Client response semantic failures preserve result storage" {
1106 const ambiguous = "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\nContent-Length: 3\r\n\r\nraw";
1107 var headers = [_]Header{.{ .name = "sentinel", .value = "value" }};
1108 var head = @as([128]u8, @splat(0xA5));
1109 var body = @as([8]u8, @splat(0x5A));
1110
1111 try std.testing.expectError(
1112 error.MalformedResponse,
1113 ClientResponse.parse(
1114 .{ .headers = &headers, .head = &head, .body = &body },
1115 ambiguous,
1116 ),
1117 );
1118 try std.testing.expectEqualStrings("sentinel", headers[0].name);
1119 try std.testing.expectEqualSlices(u8, &(@as([128]u8, @splat(0xA5))), &head);
1120 try std.testing.expectEqualSlices(u8, &(@as([8]u8, @splat(0x5A))), &body);
1121 }
1122
1123 test "Client response read materializes each HTTP body framing" {
1124 var headers: [4]Header = undefined;
1125 var head_storage: [256]u8 = undefined;
1126 var body: [128]u8 = undefined;
1127 const scratch = Scratch{
1128 .headers = &headers,
1129 .head = &head_storage,
1130 .body = &body,
1131 };
1132
1133 var fixed_reader = std.Io.Reader.fixed(
1134 "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHelloTAIL",
1135 );
1136 const fixed = try read(scratch, &fixed_reader, "GET");
1137 try std.testing.expectEqualStrings("Hello", fixed.body);
1138 try std.testing.expectEqualStrings("TAIL", try fixed_reader.peekGreedy(1));
1139
1140 var chunked_reader = std.Io.Reader.fixed(
1141 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nHe\r\n3\r\nllo\r\n0\r\nDigest: value\r\n\r\nTAIL",
1142 );
1143 const chunked = try read(scratch, &chunked_reader, "GET");
1144 try std.testing.expectEqualStrings("Hello", chunked.body);
1145 try std.testing.expectEqualStrings("TAIL", try chunked_reader.peekGreedy(1));
1146
1147 var close_reader = std.Io.Reader.fixed(
1148 "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello",
1149 );
1150 const close = try read(scratch, &close_reader, "GET");
1151 try std.testing.expectEqualStrings("Hello", close.body);
1152
1153 var nonfinal_reader = std.Io.Reader.fixed(
1154 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, tiny-coding; name=\"a,b\"\r\n\r\nraw",
1155 );
1156 const nonfinal = try read(scratch, &nonfinal_reader, "GET");
1157 try std.testing.expectEqualStrings("raw", nonfinal.body);
1158
1159 var ambiguous_reader = std.Io.Reader.fixed(
1160 "HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\nContent-Length: 3\r\n\r\nraw",
1161 );
1162 try std.testing.expectError(
1163 error.MalformedResponse,
1164 read(scratch, &ambiguous_reader, "GET"),
1165 );
1166
1167 var tunnel_reader = std.Io.Reader.fixed(
1168 "HTTP/1.1 200 Connected\r\nContent-Length: 5\r\n\r\nHello",
1169 );
1170 const tunnel = try read(scratch, &tunnel_reader, "CONNECT");
1171 try std.testing.expectEqual(@as(usize, 0), tunnel.body.len);
1172 try std.testing.expectEqualStrings(
1173 "Hello",
1174 try tunnel_reader.peekGreedy(1),
1175 );
1176 }
1177
1178 test "Client response read preserves framed tails across fragmentation" {
1179 const cases = [_]struct {
1180 wire: []const u8,
1181 body: []const u8,
1182 }{
1183 .{
1184 .wire = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabcTAIL",
1185 .body = "abc",
1186 },
1187 .{
1188 .wire = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\nTAIL",
1189 .body = "abc",
1190 },
1191 };
1192
1193 var headers: [2]Header = undefined;
1194 var head_storage: [128]u8 = undefined;
1195 var body_storage: [64]u8 = undefined;
1196 const scratch = Scratch{
1197 .headers = &headers,
1198 .head = &head_storage,
1199 .body = &body_storage,
1200 };
1201
1202 for (cases) |case| {
1203 for (1..9) |fragment_length| {
1204 var read_buffer: [32]u8 = undefined;
1205 var source = std.testing.Reader.init(
1206 &read_buffer,
1207 &.{.{ .buffer = case.wire }},
1208 );
1209 source.artificial_limit = .limited(fragment_length);
1210 const parsed = try read(scratch, &source.interface, "GET");
1211 try std.testing.expectEqualStrings(case.body, parsed.body);
1212 try std.testing.expectEqualStrings(
1213 "TAIL",
1214 try source.interface.peek(4),
1215 );
1216 }
1217 }
1218 }
1219
1220 test "Client response read preserves tails at exact body capacity" {
1221 var headers: [1]Header = undefined;
1222 var head_storage: [64]u8 = undefined;
1223
1224 var fixed_body: [3]u8 = undefined;
1225 var fixed_reader = std.Io.Reader.fixed(
1226 "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabcTAIL",
1227 );
1228 const fixed = try read(
1229 .{
1230 .headers = &headers,
1231 .head = &head_storage,
1232 .body = &fixed_body,
1233 },
1234 &fixed_reader,
1235 "GET",
1236 );
1237 try std.testing.expectEqualStrings("abc", fixed.body);
1238 try std.testing.expectEqualStrings("TAIL", try fixed_reader.peekGreedy(1));
1239
1240 const encoded = "3\r\nabc\r\n0\r\n\r\n";
1241 var chunk_body: [encoded.len]u8 = undefined;
1242 var chunk_reader = std.Io.Reader.fixed(
1243 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" ++
1244 encoded ++ "TAIL",
1245 );
1246 const chunked = try read(
1247 .{
1248 .headers = &headers,
1249 .head = &head_storage,
1250 .body = &chunk_body,
1251 },
1252 &chunk_reader,
1253 "GET",
1254 );
1255 try std.testing.expectEqualStrings("abc", chunked.body);
1256 try std.testing.expectEqualStrings("TAIL", try chunk_reader.peekGreedy(1));
1257 }
1258
1259 test "Client response read preserves a head across reader buffers" {
1260 const value = @as([(8200) * ("a").len]u8, @bitCast(@as([8200][("a").len]u8, @splat(("a")[0..("a").len].*))));
1261 const head_text = "HTTP/1.1 200 OK\r\nX-Large: " ++ value ++ "\r\nContent-Length: 5\r\n\r\n";
1262 const raw = head_text ++ "Hello";
1263 var headers: [2]Header = undefined;
1264 var head_storage: [head_text.len]u8 = undefined;
1265 var body: [5]u8 = undefined;
1266 var reader = std.Io.Reader.fixed(raw);
1267 const parsed = try read(
1268 .{ .headers = &headers, .head = &head_storage, .body = &body },
1269 &reader,
1270 "GET",
1271 );
1272
1273 try std.testing.expectEqualStrings(&value, parsed.header("X-Large").?);
1274 try std.testing.expectEqualStrings("Hello", parsed.body);
1275 }