authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-05-11 08:37:42-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-05-11 08:37:42-07:00
log3bb3d39fb4158ba4b811bcae7e7a897febf07e17
tree51d408085e1e9c60003c02960e509e3498807ab5
parent5569e6b49d9b421d35e3175df36eb9fe7e4e8084
parent9017d758b96c1f296249670dffb774a598bc8598
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #15487 from truemedian/http-tests

std.http: more http fixes, add standalone http server test

8 files changed, 771 insertions(+), 183 deletions(-)

lib/std/Uri.zig+6-3
...@@ -216,6 +216,7 @@ pub fn format(...@@ -216,6 +216,7 @@ pub fn format(
216216
217 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;217 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
218 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;218 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
219 const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null;
219220
220 if (needs_absolute) {221 if (needs_absolute) {
221 try writer.writeAll(uri.scheme);222 try writer.writeAll(uri.scheme);
...@@ -253,9 +254,11 @@ pub fn format(...@@ -253,9 +254,11 @@ pub fn format(
253 try Uri.writeEscapedQuery(writer, q);254 try Uri.writeEscapedQuery(writer, q);
254 }255 }
255256
256 if (uri.fragment) |f| {257 if (needs_fragment) {
257 try writer.writeAll("#");258 if (uri.fragment) |f| {
258 try Uri.writeEscapedQuery(writer, f);259 try writer.writeAll("#");
260 try Uri.writeEscapedQuery(writer, f);
261 }
259 }262 }
260 }263 }
261}264}
lib/std/http.zig-1
...@@ -275,5 +275,4 @@ test {...@@ -275,5 +275,4 @@ test {
275 _ = Client;275 _ = Client;
276 _ = Method;276 _ = Method;
277 _ = Status;277 _ = Status;
278 _ = @import("http/test.zig");
279}278}
lib/std/http/Client.zig+53-27
...@@ -71,7 +71,7 @@ pub const ConnectionPool = struct {...@@ -71,7 +71,7 @@ pub const ConnectionPool = struct {
71 while (next) |node| : (next = node.prev) {71 while (next) |node| : (next = node.prev) {
72 if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue;72 if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue;
73 if (node.data.port != criteria.port) continue;73 if (node.data.port != criteria.port) continue;
74 if (mem.eql(u8, node.data.host, criteria.host)) continue;74 if (!mem.eql(u8, node.data.host, criteria.host)) continue;
7575
76 pool.acquireUnsafe(node);76 pool.acquireUnsafe(node);
77 return node;77 return node;
...@@ -251,47 +251,50 @@ pub const Connection = struct {...@@ -251,47 +251,50 @@ pub const Connection = struct {
251251
252/// A buffered (and peekable) Connection.252/// A buffered (and peekable) Connection.
253pub const BufferedConnection = struct {253pub const BufferedConnection = struct {
254 pub const buffer_size = 0x2000;254 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
255255
256 conn: Connection,256 conn: Connection,
257 buf: [buffer_size]u8 = undefined,257 read_buf: [buffer_size]u8 = undefined,
258 start: u16 = 0,258 read_start: u16 = 0,
259 end: u16 = 0,259 read_end: u16 = 0,
260
261 write_buf: [buffer_size]u8 = undefined,
262 write_end: u16 = 0,
260263
261 pub fn fill(bconn: *BufferedConnection) ReadError!void {264 pub fn fill(bconn: *BufferedConnection) ReadError!void {
262 if (bconn.end != bconn.start) return;265 if (bconn.read_end != bconn.read_start) return;
263266
264 const nread = try bconn.conn.read(bconn.buf[0..]);267 const nread = try bconn.conn.read(bconn.read_buf[0..]);
265 if (nread == 0) return error.EndOfStream;268 if (nread == 0) return error.EndOfStream;
266 bconn.start = 0;269 bconn.read_start = 0;
267 bconn.end = @truncate(u16, nread);270 bconn.read_end = @intCast(u16, nread);
268 }271 }
269272
270 pub fn peek(bconn: *BufferedConnection) []const u8 {273 pub fn peek(bconn: *BufferedConnection) []const u8 {
271 return bconn.buf[bconn.start..bconn.end];274 return bconn.read_buf[bconn.read_start..bconn.read_end];
272 }275 }
273276
274 pub fn clear(bconn: *BufferedConnection, num: u16) void {277 pub fn clear(bconn: *BufferedConnection, num: u16) void {
275 bconn.start += num;278 bconn.read_start += num;
276 }279 }
277280
278 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {281 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {
279 var out_index: u16 = 0;282 var out_index: u16 = 0;
280 while (out_index < len) {283 while (out_index < len) {
281 const available = bconn.end - bconn.start;284 const available = bconn.read_end - bconn.read_start;
282 const left = buffer.len - out_index;285 const left = buffer.len - out_index;
283286
284 if (available > 0) {287 if (available > 0) {
285 const can_read = @truncate(u16, @min(available, left));288 const can_read = @intCast(u16, @min(available, left));
286289
287 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);290 @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]);
288 out_index += can_read;291 out_index += can_read;
289 bconn.start += can_read;292 bconn.read_start += can_read;
290293
291 continue;294 continue;
292 }295 }
293296
294 if (left > bconn.buf.len) {297 if (left > bconn.read_buf.len) {
295 // skip the buffer if the output is large enough298 // skip the buffer if the output is large enough
296 return bconn.conn.read(buffer[out_index..]);299 return bconn.conn.read(buffer[out_index..]);
297 }300 }
...@@ -314,11 +317,30 @@ pub const BufferedConnection = struct {...@@ -314,11 +317,30 @@ pub const BufferedConnection = struct {
314 }317 }
315318
316 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {319 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {
317 return bconn.conn.writeAll(buffer);320 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
321 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
322 bconn.write_end += @intCast(u16, buffer.len);
323 } else {
324 try bconn.flush();
325 try bconn.conn.writeAll(buffer);
326 }
318 }327 }
319328
320 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {329 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {
321 return bconn.conn.write(buffer);330 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
331 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
332 bconn.write_end += @intCast(u16, buffer.len);
333
334 return buffer.len;
335 } else {
336 try bconn.flush();
337 return try bconn.conn.write(buffer);
338 }
339 }
340
341 pub fn flush(bconn: *BufferedConnection) WriteError!void {
342 defer bconn.write_end = 0;
343 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);
322 }344 }
323345
324 pub const WriteError = Connection.WriteError;346 pub const WriteError = Connection.WriteError;
...@@ -355,8 +377,6 @@ pub const Compression = union(enum) {...@@ -355,8 +377,6 @@ pub const Compression = union(enum) {
355/// A HTTP response originating from a server.377/// A HTTP response originating from a server.
356pub const Response = struct {378pub const Response = struct {
357 pub const ParseError = Allocator.Error || error{379 pub const ParseError = Allocator.Error || error{
358 ShortHttpStatusLine,
359 BadHttpVersion,
360 HttpHeadersInvalid,380 HttpHeadersInvalid,
361 HttpHeaderContinuationsUnsupported,381 HttpHeaderContinuationsUnsupported,
362 HttpTransferEncodingUnsupported,382 HttpTransferEncodingUnsupported,
...@@ -370,12 +390,12 @@ pub const Response = struct {...@@ -370,12 +390,12 @@ pub const Response = struct {
370390
371 const first_line = it.next() orelse return error.HttpHeadersInvalid;391 const first_line = it.next() orelse return error.HttpHeadersInvalid;
372 if (first_line.len < 12)392 if (first_line.len < 12)
373 return error.ShortHttpStatusLine;393 return error.HttpHeadersInvalid;
374394
375 const version: http.Version = switch (int64(first_line[0..8])) {395 const version: http.Version = switch (int64(first_line[0..8])) {
376 int64("HTTP/1.0") => .@"HTTP/1.0",396 int64("HTTP/1.0") => .@"HTTP/1.0",
377 int64("HTTP/1.1") => .@"HTTP/1.1",397 int64("HTTP/1.1") => .@"HTTP/1.1",
378 else => return error.BadHttpVersion,398 else => return error.HttpHeadersInvalid,
379 };399 };
380 if (first_line[8] != ' ') return error.HttpHeadersInvalid;400 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
381 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));401 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
...@@ -569,8 +589,7 @@ pub const Request = struct {...@@ -569,8 +589,7 @@ pub const Request = struct {
569589
570 /// Send the request to the server.590 /// Send the request to the server.
571 pub fn start(req: *Request) StartError!void {591 pub fn start(req: *Request) StartError!void {
572 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());592 const w = req.connection.data.buffered.writer();
573 const w = buffered.writer();
574593
575 try w.writeAll(@tagName(req.method));594 try w.writeAll(@tagName(req.method));
576 try w.writeByte(' ');595 try w.writeByte(' ');
...@@ -644,7 +663,7 @@ pub const Request = struct {...@@ -644,7 +663,7 @@ pub const Request = struct {
644663
645 try w.writeAll("\r\n");664 try w.writeAll("\r\n");
646665
647 try buffered.flush();666 try req.connection.data.buffered.flush();
648 }667 }
649668
650 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;669 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
...@@ -695,16 +714,16 @@ pub const Request = struct {...@@ -695,16 +714,16 @@ pub const Request = struct {
695714
696 if (req.method == .CONNECT and req.response.status == .ok) {715 if (req.method == .CONNECT and req.response.status == .ok) {
697 req.connection.data.closing = false;716 req.connection.data.closing = false;
698 req.connection.data.proxied = true;
699 req.response.parser.done = true;717 req.response.parser.done = true;
700 }718 }
701719
720 // we default to using keep-alive if not provided
702 const req_connection = req.headers.getFirstValue("connection");721 const req_connection = req.headers.getFirstValue("connection");
703 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);722 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
704723
705 const res_connection = req.response.headers.getFirstValue("connection");724 const res_connection = req.response.headers.getFirstValue("connection");
706 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);725 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
707 if (req_keepalive and res_keepalive) {726 if (res_keepalive and (req_keepalive or req_connection == null)) {
708 req.connection.data.closing = false;727 req.connection.data.closing = false;
709 } else {728 } else {
710 req.connection.data.closing = true;729 req.connection.data.closing = true;
...@@ -725,6 +744,11 @@ pub const Request = struct {...@@ -725,6 +744,11 @@ pub const Request = struct {
725 req.response.parser.done = true;744 req.response.parser.done = true;
726 }745 }
727746
747 // HEAD requests have no body
748 if (req.method == .HEAD) {
749 req.response.parser.done = true;
750 }
751
728 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {752 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {
729 req.response.skip = true;753 req.response.skip = true;
730754
...@@ -866,6 +890,8 @@ pub const Request = struct {...@@ -866,6 +890,8 @@ pub const Request = struct {
866 .content_length => |len| if (len != 0) return error.MessageNotCompleted,890 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
867 .none => {},891 .none => {},
868 }892 }
893
894 try req.connection.data.buffered.flush();
869 }895 }
870};896};
871897
lib/std/http/Headers.zig-11
...@@ -36,17 +36,6 @@ pub const Field = struct {...@@ -36,17 +36,6 @@ pub const Field = struct {
36 name: []const u8,36 name: []const u8,
37 value: []const u8,37 value: []const u8,
3838
39 pub fn modify(entry: *Field, allocator: Allocator, new_value: []const u8) !void {
40 if (entry.value.len <= new_value.len) {
41 // TODO: eliminate this use of `@constCast`.
42 @memcpy(@constCast(entry.value)[0..new_value.len], new_value);
43 } else {
44 allocator.free(entry.value);
45
46 entry.value = try allocator.dupe(u8, new_value);
47 }
48 }
49
50 fn lessThan(ctx: void, a: Field, b: Field) bool {39 fn lessThan(ctx: void, a: Field, b: Field) bool {
51 _ = ctx;40 _ = ctx;
52 if (a.name.ptr == b.name.ptr) return false;41 if (a.name.ptr == b.name.ptr) return false;
lib/std/http/Server.zig+167-69
...@@ -95,47 +95,50 @@ pub const Connection = struct {...@@ -95,47 +95,50 @@ pub const Connection = struct {
9595
96/// A buffered (and peekable) Connection.96/// A buffered (and peekable) Connection.
97pub const BufferedConnection = struct {97pub const BufferedConnection = struct {
98 pub const buffer_size = 0x2000;98 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
9999
100 conn: Connection,100 conn: Connection,
101 buf: [buffer_size]u8 = undefined,101 read_buf: [buffer_size]u8 = undefined,
102 start: u16 = 0,102 read_start: u16 = 0,
103 end: u16 = 0,103 read_end: u16 = 0,
104
105 write_buf: [buffer_size]u8 = undefined,
106 write_end: u16 = 0,
104107
105 pub fn fill(bconn: *BufferedConnection) ReadError!void {108 pub fn fill(bconn: *BufferedConnection) ReadError!void {
106 if (bconn.end != bconn.start) return;109 if (bconn.read_end != bconn.read_start) return;
107110
108 const nread = try bconn.conn.read(bconn.buf[0..]);111 const nread = try bconn.conn.read(bconn.read_buf[0..]);
109 if (nread == 0) return error.EndOfStream;112 if (nread == 0) return error.EndOfStream;
110 bconn.start = 0;113 bconn.read_start = 0;
111 bconn.end = @truncate(u16, nread);114 bconn.read_end = @intCast(u16, nread);
112 }115 }
113116
114 pub fn peek(bconn: *BufferedConnection) []const u8 {117 pub fn peek(bconn: *BufferedConnection) []const u8 {
115 return bconn.buf[bconn.start..bconn.end];118 return bconn.read_buf[bconn.read_start..bconn.read_end];
116 }119 }
117120
118 pub fn clear(bconn: *BufferedConnection, num: u16) void {121 pub fn clear(bconn: *BufferedConnection, num: u16) void {
119 bconn.start += num;122 bconn.read_start += num;
120 }123 }
121124
122 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {125 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {
123 var out_index: u16 = 0;126 var out_index: u16 = 0;
124 while (out_index < len) {127 while (out_index < len) {
125 const available = bconn.end - bconn.start;128 const available = bconn.read_end - bconn.read_start;
126 const left = buffer.len - out_index;129 const left = buffer.len - out_index;
127130
128 if (available > 0) {131 if (available > 0) {
129 const can_read = @truncate(u16, @min(available, left));132 const can_read = @intCast(u16, @min(available, left));
130133
131 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);134 @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]);
132 out_index += can_read;135 out_index += can_read;
133 bconn.start += can_read;136 bconn.read_start += can_read;
134137
135 continue;138 continue;
136 }139 }
137140
138 if (left > bconn.buf.len) {141 if (left > bconn.read_buf.len) {
139 // skip the buffer if the output is large enough142 // skip the buffer if the output is large enough
140 return bconn.conn.read(buffer[out_index..]);143 return bconn.conn.read(buffer[out_index..]);
141 }144 }
...@@ -158,11 +161,30 @@ pub const BufferedConnection = struct {...@@ -158,11 +161,30 @@ pub const BufferedConnection = struct {
158 }161 }
159162
160 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {163 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {
161 return bconn.conn.writeAll(buffer);164 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
165 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
166 bconn.write_end += @intCast(u16, buffer.len);
167 } else {
168 try bconn.flush();
169 try bconn.conn.writeAll(buffer);
170 }
162 }171 }
163172
164 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {173 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {
165 return bconn.conn.write(buffer);174 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
175 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
176 bconn.write_end += @intCast(u16, buffer.len);
177
178 return buffer.len;
179 } else {
180 try bconn.flush();
181 return try bconn.conn.write(buffer);
182 }
183 }
184
185 pub fn flush(bconn: *BufferedConnection) WriteError!void {
186 defer bconn.write_end = 0;
187 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);
166 }188 }
167189
168 pub const WriteError = Connection.WriteError;190 pub const WriteError = Connection.WriteError;
...@@ -199,8 +221,6 @@ pub const Compression = union(enum) {...@@ -199,8 +221,6 @@ pub const Compression = union(enum) {
199/// A HTTP request originating from a client.221/// A HTTP request originating from a client.
200pub const Request = struct {222pub const Request = struct {
201 pub const ParseError = Allocator.Error || error{223 pub const ParseError = Allocator.Error || error{
202 ShortHttpStatusLine,
203 BadHttpVersion,
204 UnknownHttpMethod,224 UnknownHttpMethod,
205 HttpHeadersInvalid,225 HttpHeadersInvalid,
206 HttpHeaderContinuationsUnsupported,226 HttpHeaderContinuationsUnsupported,
...@@ -215,7 +235,7 @@ pub const Request = struct {...@@ -215,7 +235,7 @@ pub const Request = struct {
215235
216 const first_line = it.next() orelse return error.HttpHeadersInvalid;236 const first_line = it.next() orelse return error.HttpHeadersInvalid;
217 if (first_line.len < 10)237 if (first_line.len < 10)
218 return error.ShortHttpStatusLine;238 return error.HttpHeadersInvalid;
219239
220 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;240 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
221 const method_str = first_line[0..method_end];241 const method_str = first_line[0..method_end];
...@@ -229,7 +249,7 @@ pub const Request = struct {...@@ -229,7 +249,7 @@ pub const Request = struct {
229 const version: http.Version = switch (int64(version_str[0..8])) {249 const version: http.Version = switch (int64(version_str[0..8])) {
230 int64("HTTP/1.0") => .@"HTTP/1.0",250 int64("HTTP/1.0") => .@"HTTP/1.0",
231 int64("HTTP/1.1") => .@"HTTP/1.1",251 int64("HTTP/1.1") => .@"HTTP/1.1",
232 else => return error.BadHttpVersion,252 else => return error.HttpHeadersInvalid,
233 };253 };
234254
235 const target = first_line[method_end + 1 .. version_start];255 const target = first_line[method_end + 1 .. version_start];
...@@ -312,7 +332,7 @@ pub const Request = struct {...@@ -312,7 +332,7 @@ pub const Request = struct {
312 transfer_encoding: ?http.TransferEncoding = null,332 transfer_encoding: ?http.TransferEncoding = null,
313 transfer_compression: ?http.ContentEncoding = null,333 transfer_compression: ?http.ContentEncoding = null,
314334
315 headers: http.Headers = undefined,335 headers: http.Headers,
316 parser: proto.HeadersParser,336 parser: proto.HeadersParser,
317 compression: Compression = .none,337 compression: Compression = .none,
318};338};
...@@ -329,21 +349,63 @@ pub const Response = struct {...@@ -329,21 +349,63 @@ pub const Response = struct {
329349
330 transfer_encoding: ResponseTransfer = .none,350 transfer_encoding: ResponseTransfer = .none,
331351
332 server: *Server,352 allocator: Allocator,
333 address: net.Address,353 address: net.Address,
334 connection: BufferedConnection,354 connection: BufferedConnection,
335355
336 headers: http.Headers,356 headers: http.Headers,
337 request: Request,357 request: Request,
338358
359 state: State = .first,
360
361 const State = enum {
362 first,
363 start,
364 waited,
365 responded,
366 finished,
367 };
368
339 pub fn deinit(res: *Response) void {369 pub fn deinit(res: *Response) void {
340 res.server.allocator.destroy(res);370 res.connection.close();
371
372 res.headers.deinit();
373 res.request.headers.deinit();
374
375 if (res.request.parser.header_bytes_owned) {
376 res.request.parser.header_bytes.deinit(res.allocator);
377 }
341 }378 }
342379
380 pub const ResetState = enum { reset, closing };
381
343 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.382 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
344 pub fn reset(res: *Response) void {383 pub fn reset(res: *Response) ResetState {
345 res.request.headers.deinit();384 if (res.state == .first) {
346 res.headers.deinit();385 res.state = .start;
386 return .reset;
387 }
388
389 if (!res.request.parser.done) {
390 // If the response wasn't fully read, then we need to close the connection.
391 res.connection.conn.closing = true;
392 return .closing;
393 }
394
395 // A connection is only keep-alive if the Connection header is present and it's value is not "close".
396 // The server and client must both agree
397 //
398 // do() defaults to using keep-alive if the client requests it.
399 const res_connection = res.headers.getFirstValue("connection");
400 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
401
402 const req_connection = res.request.headers.getFirstValue("connection");
403 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
404 if (req_keepalive and (res_keepalive or res_connection == null)) {
405 res.connection.conn.closing = false;
406 } else {
407 res.connection.conn.closing = true;
408 }
347409
348 switch (res.request.compression) {410 switch (res.request.compression) {
349 .none => {},411 .none => {},
...@@ -352,19 +414,30 @@ pub const Response = struct {...@@ -352,19 +414,30 @@ pub const Response = struct {
352 .zstd => |*zstd| zstd.deinit(),414 .zstd => |*zstd| zstd.deinit(),
353 }415 }
354416
355 if (!res.request.parser.done) {417 res.state = .start;
356 // If the response wasn't fully read, then we need to close the connection.418 res.version = .@"HTTP/1.1";
357 res.connection.conn.closing = true;419 res.status = .ok;
358 }420 res.reason = null;
359421
360 if (res.connection.conn.closing) {422 res.transfer_encoding = .none;
361 res.connection.close();
362423
363 if (res.request.parser.header_bytes_owned) {424 res.headers.clearRetainingCapacity();
364 res.request.parser.header_bytes.deinit(res.server.allocator);425
365 }426 res.request.headers.clearAndFree(); // FIXME: figure out why `clearRetainingCapacity` causes a leak in hash_map here
427 res.request.parser.reset();
428
429 res.request = Request{
430 .version = undefined,
431 .method = undefined,
432 .target = undefined,
433 .headers = res.request.headers,
434 .parser = res.request.parser,
435 };
436
437 if (res.connection.conn.closing) {
438 return .closing;
366 } else {439 } else {
367 res.request.parser.reset();440 return .reset;
368 }441 }
369 }442 }
370443
...@@ -372,8 +445,12 @@ pub const Response = struct {...@@ -372,8 +445,12 @@ pub const Response = struct {
372445
373 /// Send the response headers.446 /// Send the response headers.
374 pub fn do(res: *Response) !void {447 pub fn do(res: *Response) !void {
375 var buffered = std.io.bufferedWriter(res.connection.writer());448 switch (res.state) {
376 const w = buffered.writer();449 .waited => res.state = .responded,
450 .first, .start, .responded, .finished => unreachable,
451 }
452
453 const w = res.connection.writer();
377454
378 try w.writeAll(@tagName(res.version));455 try w.writeAll(@tagName(res.version));
379 try w.writeByte(' ');456 try w.writeByte(' ');
...@@ -391,7 +468,14 @@ pub const Response = struct {...@@ -391,7 +468,14 @@ pub const Response = struct {
391 }468 }
392469
393 if (!res.headers.contains("connection")) {470 if (!res.headers.contains("connection")) {
394 try w.writeAll("Connection: keep-alive\r\n");471 const req_connection = res.request.headers.getFirstValue("connection");
472 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
473
474 if (req_keepalive) {
475 try w.writeAll("Connection: keep-alive\r\n");
476 } else {
477 try w.writeAll("Connection: close\r\n");
478 }
395 }479 }
396480
397 const has_transfer_encoding = res.headers.contains("transfer-encoding");481 const has_transfer_encoding = res.headers.contains("transfer-encoding");
...@@ -424,7 +508,7 @@ pub const Response = struct {...@@ -424,7 +508,7 @@ pub const Response = struct {
424508
425 try w.writeAll("\r\n");509 try w.writeAll("\r\n");
426510
427 try buffered.flush();511 try res.connection.flush();
428 }512 }
429513
430 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;514 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
...@@ -452,29 +536,23 @@ pub const Response = struct {...@@ -452,29 +536,23 @@ pub const Response = struct {
452536
453 /// Wait for the client to send a complete request head.537 /// Wait for the client to send a complete request head.
454 pub fn wait(res: *Response) WaitError!void {538 pub fn wait(res: *Response) WaitError!void {
539 switch (res.state) {
540 .first, .start => res.state = .waited,
541 .waited, .responded, .finished => unreachable,
542 }
543
455 while (true) {544 while (true) {
456 try res.connection.fill();545 try res.connection.fill();
457546
458 const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek());547 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());
459 res.connection.clear(@intCast(u16, nchecked));548 res.connection.clear(@intCast(u16, nchecked));
460549
461 if (res.request.parser.state.isContent()) break;550 if (res.request.parser.state.isContent()) break;
462 }551 }
463552
464 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };553 res.request.headers = .{ .allocator = res.allocator, .owned = true };
465 try res.request.parse(res.request.parser.header_bytes.items);554 try res.request.parse(res.request.parser.header_bytes.items);
466555
467 const res_connection = res.headers.getFirstValue("connection");
468 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
469
470 const req_connection = res.request.headers.getFirstValue("connection");
471 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
472 if (res_keepalive and req_keepalive) {
473 res.connection.conn.closing = false;
474 } else {
475 res.connection.conn.closing = true;
476 }
477
478 if (res.request.transfer_encoding) |te| {556 if (res.request.transfer_encoding) |te| {
479 switch (te) {557 switch (te) {
480 .chunked => {558 .chunked => {
...@@ -494,13 +572,13 @@ pub const Response = struct {...@@ -494,13 +572,13 @@ pub const Response = struct {
494 if (res.request.transfer_compression) |tc| switch (tc) {572 if (res.request.transfer_compression) |tc| switch (tc) {
495 .compress => return error.CompressionNotSupported,573 .compress => return error.CompressionNotSupported,
496 .deflate => res.request.compression = .{574 .deflate => res.request.compression = .{
497 .deflate = std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,575 .deflate = std.compress.zlib.zlibStream(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
498 },576 },
499 .gzip => res.request.compression = .{577 .gzip => res.request.compression = .{
500 .gzip = std.compress.gzip.decompress(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,578 .gzip = std.compress.gzip.decompress(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
501 },579 },
502 .zstd => res.request.compression = .{580 .zstd => res.request.compression = .{
503 .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()),581 .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()),
504 },582 },
505 };583 };
506 }584 }
...@@ -515,6 +593,11 @@ pub const Response = struct {...@@ -515,6 +593,11 @@ pub const Response = struct {
515 }593 }
516594
517 pub fn read(res: *Response, buffer: []u8) ReadError!usize {595 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
596 switch (res.state) {
597 .waited, .responded, .finished => {},
598 .first, .start => unreachable,
599 }
600
518 const out_index = switch (res.request.compression) {601 const out_index = switch (res.request.compression) {
519 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,602 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
520 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,603 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
...@@ -528,12 +611,12 @@ pub const Response = struct {...@@ -528,12 +611,12 @@ pub const Response = struct {
528 while (!res.request.parser.state.isContent()) { // read trailing headers611 while (!res.request.parser.state.isContent()) { // read trailing headers
529 try res.connection.fill();612 try res.connection.fill();
530613
531 const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek());614 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());
532 res.connection.clear(@intCast(u16, nchecked));615 res.connection.clear(@intCast(u16, nchecked));
533 }616 }
534617
535 if (has_trail) {618 if (has_trail) {
536 res.request.headers = http.Headers{ .allocator = res.server.allocator, .owned = false };619 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };
537620
538 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.621 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
539 // This will *only* fail for a malformed trailer.622 // This will *only* fail for a malformed trailer.
...@@ -564,6 +647,11 @@ pub const Response = struct {...@@ -564,6 +647,11 @@ pub const Response = struct {
564647
565 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.648 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
566 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {649 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
650 switch (res.state) {
651 .responded => {},
652 .first, .waited, .start, .finished => unreachable,
653 }
654
567 switch (res.transfer_encoding) {655 switch (res.transfer_encoding) {
568 .chunked => {656 .chunked => {
569 try res.connection.writer().print("{x}\r\n", .{bytes.len});657 try res.connection.writer().print("{x}\r\n", .{bytes.len});
...@@ -583,7 +671,7 @@ pub const Response = struct {...@@ -583,7 +671,7 @@ pub const Response = struct {
583 }671 }
584 }672 }
585673
586 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {674 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {
587 var index: usize = 0;675 var index: usize = 0;
588 while (index < bytes.len) {676 while (index < bytes.len) {
589 index += try write(req, bytes[index..]);677 index += try write(req, bytes[index..]);
...@@ -594,11 +682,18 @@ pub const Response = struct {...@@ -594,11 +682,18 @@ pub const Response = struct {
594682
595 /// Finish the body of a request. This notifies the server that you have no more data to send.683 /// Finish the body of a request. This notifies the server that you have no more data to send.
596 pub fn finish(res: *Response) FinishError!void {684 pub fn finish(res: *Response) FinishError!void {
685 switch (res.state) {
686 .responded => res.state = .finished,
687 .first, .waited, .start, .finished => unreachable,
688 }
689
597 switch (res.transfer_encoding) {690 switch (res.transfer_encoding) {
598 .chunked => try res.connection.writeAll("0\r\n\r\n"),691 .chunked => try res.connection.writeAll("0\r\n\r\n"),
599 .content_length => |len| if (len != 0) return error.MessageNotCompleted,692 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
600 .none => {},693 .none => {},
601 }694 }
695
696 try res.connection.flush();
602 }697 }
603};698};
604699
...@@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) {...@@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) {
635 static: []u8,730 static: []u8,
636};731};
637732
638/// Accept a new connection and allocate a Response for it.733pub const AcceptOptions = struct {
639pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {734 allocator: Allocator,
735 header_strategy: HeaderStrategy = .{ .dynamic = 8192 },
736};
737
738/// Accept a new connection.
739pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
640 const in = try server.socket.accept();740 const in = try server.socket.accept();
641741
642 const res = try server.allocator.create(Response);742 return Response{
643 res.* = .{743 .allocator = options.allocator,
644 .server = server,
645 .address = in.address,744 .address = in.address,
646 .connection = .{ .conn = .{745 .connection = .{ .conn = .{
647 .stream = in.stream,746 .stream = in.stream,
648 .protocol = .plain,747 .protocol = .plain,
649 } },748 } },
650 .headers = .{ .allocator = server.allocator },749 .headers = .{ .allocator = options.allocator },
651 .request = .{750 .request = .{
652 .version = undefined,751 .version = undefined,
653 .method = undefined,752 .method = undefined,
654 .target = undefined,753 .target = undefined,
655 .parser = switch (options) {754 .headers = .{ .allocator = options.allocator, .owned = false },
755 .parser = switch (options.header_strategy) {
656 .dynamic => |max| proto.HeadersParser.initDynamic(max),756 .dynamic => |max| proto.HeadersParser.initDynamic(max),
657 .static => |buf| proto.HeadersParser.initStatic(buf),757 .static => |buf| proto.HeadersParser.initStatic(buf),
658 },758 },
659 },759 },
660 };760 };
661
662 return res;
663}761}
664762
665test "HTTP server handles a chunked transfer coding request" {763test "HTTP server handles a chunked transfer coding request" {
lib/std/http/test.zig deleted-72
...@@ -1,72 +0,0 @@
1const std = @import("std");
2const expect = std.testing.expect;
3
4test "client requests server" {
5 const builtin = @import("builtin");
6
7 // This test requires spawning threads.
8 if (builtin.single_threaded) {
9 return error.SkipZigTest;
10 }
11
12 const native_endian = comptime builtin.cpu.arch.endian();
13 if (builtin.zig_backend == .stage2_llvm and native_endian == .Big) {
14 // https://github.com/ziglang/zig/issues/13782
15 return error.SkipZigTest;
16 }
17
18 if (builtin.os.tag == .wasi) return error.SkipZigTest;
19
20 const allocator = std.testing.allocator;
21
22 const max_header_size = 8192;
23 var server = std.http.Server.init(allocator, .{ .reuse_address = true });
24 defer server.deinit();
25
26 const address = try std.net.Address.parseIp("127.0.0.1", 0);
27 try server.listen(address);
28 const server_port = server.socket.listen_address.in.getPort();
29
30 const server_thread = try std.Thread.spawn(.{}, (struct {
31 fn apply(s: *std.http.Server) !void {
32 const res = try s.accept(.{ .dynamic = max_header_size });
33 defer res.deinit();
34 defer res.reset();
35 try res.wait();
36
37 const server_body: []const u8 = "message from server!\n";
38 res.transfer_encoding = .{ .content_length = server_body.len };
39 try res.headers.append("content-type", "text/plain");
40 try res.headers.append("connection", "close");
41 try res.do();
42
43 var buf: [128]u8 = undefined;
44 const n = try res.readAll(&buf);
45 try expect(std.mem.eql(u8, buf[0..n], "Hello, World!\n"));
46 _ = try res.writer().writeAll(server_body);
47 try res.finish();
48 }
49 }).apply, .{&server});
50
51 var uri_buf: [22]u8 = undefined;
52 const uri = try std.Uri.parse(try std.fmt.bufPrint(&uri_buf, "http://127.0.0.1:{d}", .{server_port}));
53 var client = std.http.Client{ .allocator = allocator };
54 defer client.deinit();
55 var client_headers = std.http.Headers{ .allocator = allocator };
56 defer client_headers.deinit();
57 var client_req = try client.request(.POST, uri, client_headers, .{});
58 defer client_req.deinit();
59
60 client_req.transfer_encoding = .{ .content_length = 14 }; // this will be checked to ensure you sent exactly 14 bytes
61 try client_req.start(); // this sends the request
62 try client_req.writeAll("Hello, ");
63 try client_req.writeAll("World!\n");
64 try client_req.finish();
65 try client_req.wait(); // this waits for a response
66
67 const body = try client_req.reader().readAllAlloc(allocator, 8192 * 1024);
68 defer allocator.free(body);
69 try expect(std.mem.eql(u8, body, "message from server!\n"));
70
71 server_thread.join();
72}
test/standalone.zig+4
...@@ -55,6 +55,10 @@ pub const simple_cases = [_]SimpleCase{...@@ -55,6 +55,10 @@ pub const simple_cases = [_]SimpleCase{
55 .os_filter = .windows,55 .os_filter = .windows,
56 .link_libc = true,56 .link_libc = true,
57 },57 },
58 .{
59 .src_path = "test/standalone/http.zig",
60 .all_modes = true,
61 },
5862
59 // Ensure the development tools are buildable. Alphabetically sorted.63 // Ensure the development tools are buildable. Alphabetically sorted.
60 // No need to build `tools/spirv/grammar.zig`.64 // No need to build `tools/spirv/grammar.zig`.
test/standalone/http.zig created+541
...@@ -0,0 +1,541 @@
1const std = @import("std");
2
3const http = std.http;
4const Server = http.Server;
5const Client = http.Client;
6
7const mem = std.mem;
8const testing = std.testing;
9
10const max_header_size = 8192;
11
12var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
13var gpa_client = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
14
15const salloc = gpa_server.allocator();
16const calloc = gpa_client.allocator();
17
18var server: Server = undefined;
19
20fn handleRequest(res: *Server.Response) !void {
21 const log = std.log.scoped(.server);
22
23 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });
24
25 const body = try res.reader().readAllAlloc(salloc, 8192);
26 defer salloc.free(body);
27
28 if (res.request.headers.contains("connection")) {
29 try res.headers.append("connection", "keep-alive");
30 }
31
32 if (mem.startsWith(u8, res.request.target, "/get")) {
33 if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) {
34 res.transfer_encoding = .chunked;
35 } else {
36 res.transfer_encoding = .{ .content_length = 14 };
37 }
38
39 try res.headers.append("content-type", "text/plain");
40
41 try res.do();
42 if (res.request.method != .HEAD) {
43 try res.writeAll("Hello, ");
44 try res.writeAll("World!\n");
45 try res.finish();
46 }
47 } else if (mem.startsWith(u8, res.request.target, "/large")) {
48 res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 };
49
50 try res.do();
51
52 var i: u32 = 0;
53 while (i < 5) : (i += 1) {
54 try res.writeAll("Hello, World!\n");
55 }
56
57 try res.writeAll("Hello, World!\n" ** 1024);
58
59 i = 0;
60 while (i < 5) : (i += 1) {
61 try res.writeAll("Hello, World!\n");
62 }
63
64 try res.finish();
65 } else if (mem.eql(u8, res.request.target, "/echo-content")) {
66 try testing.expectEqualStrings("Hello, World!\n", body);
67 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
68
69 if (res.request.headers.contains("transfer-encoding")) {
70 try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?);
71 res.transfer_encoding = .chunked;
72 } else {
73 res.transfer_encoding = .{ .content_length = 14 };
74 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);
75 }
76
77 try res.do();
78 try res.writeAll("Hello, ");
79 try res.writeAll("World!\n");
80 try res.finish();
81 } else if (mem.eql(u8, res.request.target, "/trailer")) {
82 res.transfer_encoding = .chunked;
83
84 try res.do();
85 try res.writeAll("Hello, ");
86 try res.writeAll("World!\n");
87 // try res.finish();
88 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");
89 try res.connection.flush();
90 } else if (mem.eql(u8, res.request.target, "/redirect/1")) {
91 res.transfer_encoding = .chunked;
92
93 res.status = .found;
94 try res.headers.append("location", "../../get");
95
96 try res.do();
97 try res.writeAll("Hello, ");
98 try res.writeAll("Redirected!\n");
99 try res.finish();
100 } else if (mem.eql(u8, res.request.target, "/redirect/2")) {
101 res.transfer_encoding = .chunked;
102
103 res.status = .found;
104 try res.headers.append("location", "/redirect/1");
105
106 try res.do();
107 try res.writeAll("Hello, ");
108 try res.writeAll("Redirected!\n");
109 try res.finish();
110 } else if (mem.eql(u8, res.request.target, "/redirect/3")) {
111 res.transfer_encoding = .chunked;
112
113 const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{server.socket.listen_address.getPort()});
114 defer salloc.free(location);
115
116 res.status = .found;
117 try res.headers.append("location", location);
118
119 try res.do();
120 try res.writeAll("Hello, ");
121 try res.writeAll("Redirected!\n");
122 try res.finish();
123 } else if (mem.eql(u8, res.request.target, "/redirect/4")) {
124 res.transfer_encoding = .chunked;
125
126 res.status = .found;
127 try res.headers.append("location", "/redirect/3");
128
129 try res.do();
130 try res.writeAll("Hello, ");
131 try res.writeAll("Redirected!\n");
132 try res.finish();
133 } else {
134 res.status = .not_found;
135 try res.do();
136 }
137}
138
139var handle_new_requests = true;
140
141fn runServer(srv: *Server) !void {
142 outer: while (handle_new_requests) {
143 var res = try srv.accept(.{
144 .allocator = salloc,
145 .header_strategy = .{ .dynamic = max_header_size },
146 });
147 defer res.deinit();
148
149 while (res.reset() != .closing) {
150 res.wait() catch |err| switch (err) {
151 error.HttpHeadersInvalid => continue :outer,
152 error.EndOfStream => continue,
153 else => return err,
154 };
155
156 try handleRequest(&res);
157 }
158 }
159}
160
161fn serverThread(srv: *Server) void {
162 defer srv.deinit();
163 defer _ = gpa_server.deinit();
164
165 runServer(srv) catch |err| {
166 std.debug.print("server error: {}\n", .{err});
167
168 if (@errorReturnTrace()) |trace| {
169 std.debug.dumpStackTrace(trace.*);
170 }
171
172 _ = gpa_server.deinit();
173 std.os.exit(1);
174 };
175}
176
177fn killServer(addr: std.net.Address) void {
178 handle_new_requests = false;
179
180 const conn = std.net.tcpConnectToAddress(addr) catch return;
181 conn.close();
182}
183
184pub fn main() !void {
185 const log = std.log.scoped(.client);
186
187 defer _ = gpa_client.deinit();
188
189 server = Server.init(salloc, .{ .reuse_address = true });
190
191 const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable;
192 try server.listen(addr);
193
194 const port = server.socket.listen_address.getPort();
195
196 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
197
198 var client = Client{ .allocator = calloc };
199 // defer client.deinit(); handled below
200
201 { // read content-length response
202 var h = http.Headers{ .allocator = calloc };
203 defer h.deinit();
204
205 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
206 defer calloc.free(location);
207 const uri = try std.Uri.parse(location);
208
209 log.info("{s}", .{location});
210 var req = try client.request(.GET, uri, h, .{});
211 defer req.deinit();
212
213 try req.start();
214 try req.wait();
215
216 const body = try req.reader().readAllAlloc(calloc, 8192);
217 defer calloc.free(body);
218
219 try testing.expectEqualStrings("Hello, World!\n", body);
220 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
221 }
222
223 // connection has been kept alive
224 try testing.expect(client.connection_pool.free_len == 1);
225
226 { // read large content-length response
227 var h = http.Headers{ .allocator = calloc };
228 defer h.deinit();
229
230 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port});
231 defer calloc.free(location);
232 const uri = try std.Uri.parse(location);
233
234 log.info("{s}", .{location});
235 var req = try client.request(.GET, uri, h, .{});
236 defer req.deinit();
237
238 try req.start();
239 try req.wait();
240
241 const body = try req.reader().readAllAlloc(calloc, 8192 * 1024);
242 defer calloc.free(body);
243
244 try testing.expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
245 }
246
247 // connection has been kept alive
248 try testing.expect(client.connection_pool.free_len == 1);
249
250 { // send head request and not read chunked
251 var h = http.Headers{ .allocator = calloc };
252 defer h.deinit();
253
254 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
255 defer calloc.free(location);
256 const uri = try std.Uri.parse(location);
257
258 log.info("{s}", .{location});
259 var req = try client.request(.HEAD, uri, h, .{});
260 defer req.deinit();
261
262 try req.start();
263 try req.wait();
264
265 const body = try req.reader().readAllAlloc(calloc, 8192);
266 defer calloc.free(body);
267
268 try testing.expectEqualStrings("", body);
269 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
270 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);
271 }
272
273 // connection has been kept alive
274 try testing.expect(client.connection_pool.free_len == 1);
275
276 { // read chunked response
277 var h = http.Headers{ .allocator = calloc };
278 defer h.deinit();
279
280 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
281 defer calloc.free(location);
282 const uri = try std.Uri.parse(location);
283
284 log.info("{s}", .{location});
285 var req = try client.request(.GET, uri, h, .{});
286 defer req.deinit();
287
288 try req.start();
289 try req.wait();
290
291 const body = try req.reader().readAllAlloc(calloc, 8192);
292 defer calloc.free(body);
293
294 try testing.expectEqualStrings("Hello, World!\n", body);
295 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
296 }
297
298 // connection has been kept alive
299 try testing.expect(client.connection_pool.free_len == 1);
300
301 { // send head request and not read chunked
302 var h = http.Headers{ .allocator = calloc };
303 defer h.deinit();
304
305 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
306 defer calloc.free(location);
307 const uri = try std.Uri.parse(location);
308
309 log.info("{s}", .{location});
310 var req = try client.request(.HEAD, uri, h, .{});
311 defer req.deinit();
312
313 try req.start();
314 try req.wait();
315
316 const body = try req.reader().readAllAlloc(calloc, 8192);
317 defer calloc.free(body);
318
319 try testing.expectEqualStrings("", body);
320 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
321 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);
322 }
323
324 // connection has been kept alive
325 try testing.expect(client.connection_pool.free_len == 1);
326
327 { // check trailing headers
328 var h = http.Headers{ .allocator = calloc };
329 defer h.deinit();
330
331 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});
332 defer calloc.free(location);
333 const uri = try std.Uri.parse(location);
334
335 log.info("{s}", .{location});
336 var req = try client.request(.GET, uri, h, .{});
337 defer req.deinit();
338
339 try req.start();
340 try req.wait();
341
342 const body = try req.reader().readAllAlloc(calloc, 8192);
343 defer calloc.free(body);
344
345 try testing.expectEqualStrings("Hello, World!\n", body);
346 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
347 }
348
349 // connection has been kept alive
350 try testing.expect(client.connection_pool.free_len == 1);
351
352 { // send content-length request
353 var h = http.Headers{ .allocator = calloc };
354 defer h.deinit();
355
356 try h.append("content-type", "text/plain");
357
358 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
359 defer calloc.free(location);
360 const uri = try std.Uri.parse(location);
361
362 log.info("{s}", .{location});
363 var req = try client.request(.POST, uri, h, .{});
364 defer req.deinit();
365
366 req.transfer_encoding = .{ .content_length = 14 };
367
368 try req.start();
369 try req.writeAll("Hello, ");
370 try req.writeAll("World!\n");
371 try req.finish();
372
373 try req.wait();
374
375 const body = try req.reader().readAllAlloc(calloc, 8192);
376 defer calloc.free(body);
377
378 try testing.expectEqualStrings("Hello, World!\n", body);
379 }
380
381 // connection has been kept alive
382 try testing.expect(client.connection_pool.free_len == 1);
383
384 { // read content-length response with connection close
385 var h = http.Headers{ .allocator = calloc };
386 defer h.deinit();
387
388 try h.append("connection", "close");
389
390 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
391 defer calloc.free(location);
392 const uri = try std.Uri.parse(location);
393
394 log.info("{s}", .{location});
395 var req = try client.request(.GET, uri, h, .{});
396 defer req.deinit();
397
398 try req.start();
399 try req.wait();
400
401 const body = try req.reader().readAllAlloc(calloc, 8192);
402 defer calloc.free(body);
403
404 try testing.expectEqualStrings("Hello, World!\n", body);
405 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
406 }
407
408 // connection has been closed
409 try testing.expect(client.connection_pool.free_len == 0);
410
411 { // send chunked request
412 var h = http.Headers{ .allocator = calloc };
413 defer h.deinit();
414
415 try h.append("content-type", "text/plain");
416
417 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
418 defer calloc.free(location);
419 const uri = try std.Uri.parse(location);
420
421 log.info("{s}", .{location});
422 var req = try client.request(.POST, uri, h, .{});
423 defer req.deinit();
424
425 req.transfer_encoding = .chunked;
426
427 try req.start();
428 try req.writeAll("Hello, ");
429 try req.writeAll("World!\n");
430 try req.finish();
431
432 try req.wait();
433
434 const body = try req.reader().readAllAlloc(calloc, 8192);
435 defer calloc.free(body);
436
437 try testing.expectEqualStrings("Hello, World!\n", body);
438 }
439
440 // connection has been kept alive
441 try testing.expect(client.connection_pool.free_len == 1);
442
443 { // relative redirect
444 var h = http.Headers{ .allocator = calloc };
445 defer h.deinit();
446
447 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});
448 defer calloc.free(location);
449 const uri = try std.Uri.parse(location);
450
451 log.info("{s}", .{location});
452 var req = try client.request(.GET, uri, h, .{});
453 defer req.deinit();
454
455 try req.start();
456 try req.wait();
457
458 const body = try req.reader().readAllAlloc(calloc, 8192);
459 defer calloc.free(body);
460
461 try testing.expectEqualStrings("Hello, World!\n", body);
462 }
463
464 // connection has been kept alive
465 try testing.expect(client.connection_pool.free_len == 1);
466
467 { // redirect from root
468 var h = http.Headers{ .allocator = calloc };
469 defer h.deinit();
470
471 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});
472 defer calloc.free(location);
473 const uri = try std.Uri.parse(location);
474
475 log.info("{s}", .{location});
476 var req = try client.request(.GET, uri, h, .{});
477 defer req.deinit();
478
479 try req.start();
480 try req.wait();
481
482 const body = try req.reader().readAllAlloc(calloc, 8192);
483 defer calloc.free(body);
484
485 try testing.expectEqualStrings("Hello, World!\n", body);
486 }
487
488 // connection has been kept alive
489 try testing.expect(client.connection_pool.free_len == 1);
490
491 { // absolute redirect
492 var h = http.Headers{ .allocator = calloc };
493 defer h.deinit();
494
495 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});
496 defer calloc.free(location);
497 const uri = try std.Uri.parse(location);
498
499 log.info("{s}", .{location});
500 var req = try client.request(.GET, uri, h, .{});
501 defer req.deinit();
502
503 try req.start();
504 try req.wait();
505
506 const body = try req.reader().readAllAlloc(calloc, 8192);
507 defer calloc.free(body);
508
509 try testing.expectEqualStrings("Hello, World!\n", body);
510 }
511
512 // connection has been kept alive
513 try testing.expect(client.connection_pool.free_len == 1);
514
515 { // too many redirects
516 var h = http.Headers{ .allocator = calloc };
517 defer h.deinit();
518
519 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});
520 defer calloc.free(location);
521 const uri = try std.Uri.parse(location);
522
523 log.info("{s}", .{location});
524 var req = try client.request(.GET, uri, h, .{});
525 defer req.deinit();
526
527 try req.start();
528 req.wait() catch |err| switch (err) {
529 error.TooManyHttpRedirects => {},
530 else => return err,
531 };
532 }
533
534 // connection has been kept alive
535 try testing.expect(client.connection_pool.free_len == 1);
536
537 client.deinit();
538
539 killServer(server.socket.listen_address);
540 server_thread.join();
541}