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(
216216
217217 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
218218 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
220221 if (needs_absolute) {
221222 try writer.writeAll(uri.scheme);
......@@ -253,9 +254,11 @@ pub fn format(
253254 try Uri.writeEscapedQuery(writer, q);
254255 }
255256
256 if (uri.fragment) |f| {
257 try writer.writeAll("#");
258 try Uri.writeEscapedQuery(writer, f);
257 if (needs_fragment) {
258 if (uri.fragment) |f| {
259 try writer.writeAll("#");
260 try Uri.writeEscapedQuery(writer, f);
261 }
259262 }
260263 }
261264}
lib/std/http.zig-1
......@@ -275,5 +275,4 @@ test {
275275 _ = Client;
276276 _ = Method;
277277 _ = Status;
278 _ = @import("http/test.zig");
279278}
lib/std/http/Client.zig+53-27
......@@ -71,7 +71,7 @@ pub const ConnectionPool = struct {
7171 while (next) |node| : (next = node.prev) {
7272 if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue;
7373 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
7676 pool.acquireUnsafe(node);
7777 return node;
......@@ -251,47 +251,50 @@ pub const Connection = struct {
251251
252252/// A buffered (and peekable) Connection.
253253pub const BufferedConnection = struct {
254 pub const buffer_size = 0x2000;
254 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
255255
256256 conn: Connection,
257 buf: [buffer_size]u8 = undefined,
258 start: u16 = 0,
259 end: u16 = 0,
257 read_buf: [buffer_size]u8 = undefined,
258 read_start: u16 = 0,
259 read_end: u16 = 0,
260
261 write_buf: [buffer_size]u8 = undefined,
262 write_end: u16 = 0,
260263
261264 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..]);
265268 if (nread == 0) return error.EndOfStream;
266 bconn.start = 0;
267 bconn.end = @truncate(u16, nread);
269 bconn.read_start = 0;
270 bconn.read_end = @intCast(u16, nread);
268271 }
269272
270273 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];
272275 }
273276
274277 pub fn clear(bconn: *BufferedConnection, num: u16) void {
275 bconn.start += num;
278 bconn.read_start += num;
276279 }
277280
278281 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {
279282 var out_index: u16 = 0;
280283 while (out_index < len) {
281 const available = bconn.end - bconn.start;
284 const available = bconn.read_end - bconn.read_start;
282285 const left = buffer.len - out_index;
283286
284287 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]);
288291 out_index += can_read;
289 bconn.start += can_read;
292 bconn.read_start += can_read;
290293
291294 continue;
292295 }
293296
294 if (left > bconn.buf.len) {
297 if (left > bconn.read_buf.len) {
295298 // skip the buffer if the output is large enough
296299 return bconn.conn.read(buffer[out_index..]);
297300 }
......@@ -314,11 +317,30 @@ pub const BufferedConnection = struct {
314317 }
315318
316319 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 }
318327 }
319328
320329 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]);
322344 }
323345
324346 pub const WriteError = Connection.WriteError;
......@@ -355,8 +377,6 @@ pub const Compression = union(enum) {
355377/// A HTTP response originating from a server.
356378pub const Response = struct {
357379 pub const ParseError = Allocator.Error || error{
358 ShortHttpStatusLine,
359 BadHttpVersion,
360380 HttpHeadersInvalid,
361381 HttpHeaderContinuationsUnsupported,
362382 HttpTransferEncodingUnsupported,
......@@ -370,12 +390,12 @@ pub const Response = struct {
370390
371391 const first_line = it.next() orelse return error.HttpHeadersInvalid;
372392 if (first_line.len < 12)
373 return error.ShortHttpStatusLine;
393 return error.HttpHeadersInvalid;
374394
375395 const version: http.Version = switch (int64(first_line[0..8])) {
376396 int64("HTTP/1.0") => .@"HTTP/1.0",
377397 int64("HTTP/1.1") => .@"HTTP/1.1",
378 else => return error.BadHttpVersion,
398 else => return error.HttpHeadersInvalid,
379399 };
380400 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
381401 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
......@@ -569,8 +589,7 @@ pub const Request = struct {
569589
570590 /// Send the request to the server.
571591 pub fn start(req: *Request) StartError!void {
572 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
573 const w = buffered.writer();
592 const w = req.connection.data.buffered.writer();
574593
575594 try w.writeAll(@tagName(req.method));
576595 try w.writeByte(' ');
......@@ -644,7 +663,7 @@ pub const Request = struct {
644663
645664 try w.writeAll("\r\n");
646665
647 try buffered.flush();
666 try req.connection.data.buffered.flush();
648667 }
649668
650669 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
......@@ -695,16 +714,16 @@ pub const Request = struct {
695714
696715 if (req.method == .CONNECT and req.response.status == .ok) {
697716 req.connection.data.closing = false;
698 req.connection.data.proxied = true;
699717 req.response.parser.done = true;
700718 }
701719
720 // we default to using keep-alive if not provided
702721 const req_connection = req.headers.getFirstValue("connection");
703722 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
704723
705724 const res_connection = req.response.headers.getFirstValue("connection");
706725 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)) {
708727 req.connection.data.closing = false;
709728 } else {
710729 req.connection.data.closing = true;
......@@ -725,6 +744,11 @@ pub const Request = struct {
725744 req.response.parser.done = true;
726745 }
727746
747 // HEAD requests have no body
748 if (req.method == .HEAD) {
749 req.response.parser.done = true;
750 }
751
728752 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {
729753 req.response.skip = true;
730754
......@@ -866,6 +890,8 @@ pub const Request = struct {
866890 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
867891 .none => {},
868892 }
893
894 try req.connection.data.buffered.flush();
869895 }
870896};
871897
lib/std/http/Headers.zig-11
......@@ -36,17 +36,6 @@ pub const Field = struct {
3636 name: []const u8,
3737 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
5039 fn lessThan(ctx: void, a: Field, b: Field) bool {
5140 _ = ctx;
5241 if (a.name.ptr == b.name.ptr) return false;
lib/std/http/Server.zig+167-69
......@@ -95,47 +95,50 @@ pub const Connection = struct {
9595
9696/// A buffered (and peekable) Connection.
9797pub const BufferedConnection = struct {
98 pub const buffer_size = 0x2000;
98 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
9999
100100 conn: Connection,
101 buf: [buffer_size]u8 = undefined,
102 start: u16 = 0,
103 end: u16 = 0,
101 read_buf: [buffer_size]u8 = undefined,
102 read_start: u16 = 0,
103 read_end: u16 = 0,
104
105 write_buf: [buffer_size]u8 = undefined,
106 write_end: u16 = 0,
104107
105108 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..]);
109112 if (nread == 0) return error.EndOfStream;
110 bconn.start = 0;
111 bconn.end = @truncate(u16, nread);
113 bconn.read_start = 0;
114 bconn.read_end = @intCast(u16, nread);
112115 }
113116
114117 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];
116119 }
117120
118121 pub fn clear(bconn: *BufferedConnection, num: u16) void {
119 bconn.start += num;
122 bconn.read_start += num;
120123 }
121124
122125 pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize {
123126 var out_index: u16 = 0;
124127 while (out_index < len) {
125 const available = bconn.end - bconn.start;
128 const available = bconn.read_end - bconn.read_start;
126129 const left = buffer.len - out_index;
127130
128131 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]);
132135 out_index += can_read;
133 bconn.start += can_read;
136 bconn.read_start += can_read;
134137
135138 continue;
136139 }
137140
138 if (left > bconn.buf.len) {
141 if (left > bconn.read_buf.len) {
139142 // skip the buffer if the output is large enough
140143 return bconn.conn.read(buffer[out_index..]);
141144 }
......@@ -158,11 +161,30 @@ pub const BufferedConnection = struct {
158161 }
159162
160163 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 }
162171 }
163172
164173 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]);
166188 }
167189
168190 pub const WriteError = Connection.WriteError;
......@@ -199,8 +221,6 @@ pub const Compression = union(enum) {
199221/// A HTTP request originating from a client.
200222pub const Request = struct {
201223 pub const ParseError = Allocator.Error || error{
202 ShortHttpStatusLine,
203 BadHttpVersion,
204224 UnknownHttpMethod,
205225 HttpHeadersInvalid,
206226 HttpHeaderContinuationsUnsupported,
......@@ -215,7 +235,7 @@ pub const Request = struct {
215235
216236 const first_line = it.next() orelse return error.HttpHeadersInvalid;
217237 if (first_line.len < 10)
218 return error.ShortHttpStatusLine;
238 return error.HttpHeadersInvalid;
219239
220240 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
221241 const method_str = first_line[0..method_end];
......@@ -229,7 +249,7 @@ pub const Request = struct {
229249 const version: http.Version = switch (int64(version_str[0..8])) {
230250 int64("HTTP/1.0") => .@"HTTP/1.0",
231251 int64("HTTP/1.1") => .@"HTTP/1.1",
232 else => return error.BadHttpVersion,
252 else => return error.HttpHeadersInvalid,
233253 };
234254
235255 const target = first_line[method_end + 1 .. version_start];
......@@ -312,7 +332,7 @@ pub const Request = struct {
312332 transfer_encoding: ?http.TransferEncoding = null,
313333 transfer_compression: ?http.ContentEncoding = null,
314334
315 headers: http.Headers = undefined,
335 headers: http.Headers,
316336 parser: proto.HeadersParser,
317337 compression: Compression = .none,
318338};
......@@ -329,21 +349,63 @@ pub const Response = struct {
329349
330350 transfer_encoding: ResponseTransfer = .none,
331351
332 server: *Server,
352 allocator: Allocator,
333353 address: net.Address,
334354 connection: BufferedConnection,
335355
336356 headers: http.Headers,
337357 request: Request,
338358
359 state: State = .first,
360
361 const State = enum {
362 first,
363 start,
364 waited,
365 responded,
366 finished,
367 };
368
339369 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 }
341378 }
342379
380 pub const ResetState = enum { reset, closing };
381
343382 /// 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 {
345 res.request.headers.deinit();
346 res.headers.deinit();
383 pub fn reset(res: *Response) ResetState {
384 if (res.state == .first) {
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
348410 switch (res.request.compression) {
349411 .none => {},
......@@ -352,19 +414,30 @@ pub const Response = struct {
352414 .zstd => |*zstd| zstd.deinit(),
353415 }
354416
355 if (!res.request.parser.done) {
356 // If the response wasn't fully read, then we need to close the connection.
357 res.connection.conn.closing = true;
358 }
417 res.state = .start;
418 res.version = .@"HTTP/1.1";
419 res.status = .ok;
420 res.reason = null;
359421
360 if (res.connection.conn.closing) {
361 res.connection.close();
422 res.transfer_encoding = .none;
362423
363 if (res.request.parser.header_bytes_owned) {
364 res.request.parser.header_bytes.deinit(res.server.allocator);
365 }
424 res.headers.clearRetainingCapacity();
425
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;
366439 } else {
367 res.request.parser.reset();
440 return .reset;
368441 }
369442 }
370443
......@@ -372,8 +445,12 @@ pub const Response = struct {
372445
373446 /// Send the response headers.
374447 pub fn do(res: *Response) !void {
375 var buffered = std.io.bufferedWriter(res.connection.writer());
376 const w = buffered.writer();
448 switch (res.state) {
449 .waited => res.state = .responded,
450 .first, .start, .responded, .finished => unreachable,
451 }
452
453 const w = res.connection.writer();
377454
378455 try w.writeAll(@tagName(res.version));
379456 try w.writeByte(' ');
......@@ -391,7 +468,14 @@ pub const Response = struct {
391468 }
392469
393470 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 }
395479 }
396480
397481 const has_transfer_encoding = res.headers.contains("transfer-encoding");
......@@ -424,7 +508,7 @@ pub const Response = struct {
424508
425509 try w.writeAll("\r\n");
426510
427 try buffered.flush();
511 try res.connection.flush();
428512 }
429513
430514 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
......@@ -452,29 +536,23 @@ pub const Response = struct {
452536
453537 /// Wait for the client to send a complete request head.
454538 pub fn wait(res: *Response) WaitError!void {
539 switch (res.state) {
540 .first, .start => res.state = .waited,
541 .waited, .responded, .finished => unreachable,
542 }
543
455544 while (true) {
456545 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());
459548 res.connection.clear(@intCast(u16, nchecked));
460549
461550 if (res.request.parser.state.isContent()) break;
462551 }
463552
464 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
553 res.request.headers = .{ .allocator = res.allocator, .owned = true };
465554 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
478556 if (res.request.transfer_encoding) |te| {
479557 switch (te) {
480558 .chunked => {
......@@ -494,13 +572,13 @@ pub const Response = struct {
494572 if (res.request.transfer_compression) |tc| switch (tc) {
495573 .compress => return error.CompressionNotSupported,
496574 .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,
498576 },
499577 .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,
501579 },
502580 .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()),
504582 },
505583 };
506584 }
......@@ -515,6 +593,11 @@ pub const Response = struct {
515593 }
516594
517595 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
596 switch (res.state) {
597 .waited, .responded, .finished => {},
598 .first, .start => unreachable,
599 }
600
518601 const out_index = switch (res.request.compression) {
519602 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
520603 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
......@@ -528,12 +611,12 @@ pub const Response = struct {
528611 while (!res.request.parser.state.isContent()) { // read trailing headers
529612 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());
532615 res.connection.clear(@intCast(u16, nchecked));
533616 }
534617
535618 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
538621 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
539622 // This will *only* fail for a malformed trailer.
......@@ -564,6 +647,11 @@ pub const Response = struct {
564647
565648 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
566649 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
650 switch (res.state) {
651 .responded => {},
652 .first, .waited, .start, .finished => unreachable,
653 }
654
567655 switch (res.transfer_encoding) {
568656 .chunked => {
569657 try res.connection.writer().print("{x}\r\n", .{bytes.len});
......@@ -583,7 +671,7 @@ pub const Response = struct {
583671 }
584672 }
585673
586 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {
674 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {
587675 var index: usize = 0;
588676 while (index < bytes.len) {
589677 index += try write(req, bytes[index..]);
......@@ -594,11 +682,18 @@ pub const Response = struct {
594682
595683 /// Finish the body of a request. This notifies the server that you have no more data to send.
596684 pub fn finish(res: *Response) FinishError!void {
685 switch (res.state) {
686 .responded => res.state = .finished,
687 .first, .waited, .start, .finished => unreachable,
688 }
689
597690 switch (res.transfer_encoding) {
598691 .chunked => try res.connection.writeAll("0\r\n\r\n"),
599692 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
600693 .none => {},
601694 }
695
696 try res.connection.flush();
602697 }
603698};
604699
......@@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) {
635730 static: []u8,
636731};
637732
638/// Accept a new connection and allocate a Response for it.
639pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
733pub const AcceptOptions = struct {
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 {
640740 const in = try server.socket.accept();
641741
642 const res = try server.allocator.create(Response);
643 res.* = .{
644 .server = server,
742 return Response{
743 .allocator = options.allocator,
645744 .address = in.address,
646745 .connection = .{ .conn = .{
647746 .stream = in.stream,
648747 .protocol = .plain,
649748 } },
650 .headers = .{ .allocator = server.allocator },
749 .headers = .{ .allocator = options.allocator },
651750 .request = .{
652751 .version = undefined,
653752 .method = undefined,
654753 .target = undefined,
655 .parser = switch (options) {
754 .headers = .{ .allocator = options.allocator, .owned = false },
755 .parser = switch (options.header_strategy) {
656756 .dynamic => |max| proto.HeadersParser.initDynamic(max),
657757 .static => |buf| proto.HeadersParser.initStatic(buf),
658758 },
659759 },
660760 };
661
662 return res;
663761}
664762
665763test "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{
5555 .os_filter = .windows,
5656 .link_libc = true,
5757 },
58 .{
59 .src_path = "test/standalone/http.zig",
60 .all_modes = true,
61 },
5862
5963 // Ensure the development tools are buildable. Alphabetically sorted.
6064 // 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}