authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-25 09:22:20-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-05-06 21:35:15-05:00
log71c228fe6572b9f3b30e82035bf8fd7e2b1dd29d
treebc260df4dd4ac6befa2f05d54a39dd8b43548907
parentd71a43ec2c28a53a3e1d9bcb538707eca00a6fc0
signaturelock-open Commit is signed but in an unrecognized format.

std.http: add simple standalone http tests, add state check for http server


8 files changed, 546 insertions(+), 134 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+9-7
...@@ -264,7 +264,7 @@ pub const BufferedConnection = struct {...@@ -264,7 +264,7 @@ pub const BufferedConnection = struct {
264 const nread = try bconn.conn.read(bconn.buf[0..]);264 const nread = try bconn.conn.read(bconn.buf[0..]);
265 if (nread == 0) return error.EndOfStream;265 if (nread == 0) return error.EndOfStream;
266 bconn.start = 0;266 bconn.start = 0;
267 bconn.end = @truncate(u16, nread);267 bconn.end = @intCast(u16, nread);
268 }268 }
269269
270 pub fn peek(bconn: *BufferedConnection) []const u8 {270 pub fn peek(bconn: *BufferedConnection) []const u8 {
...@@ -282,7 +282,7 @@ pub const BufferedConnection = struct {...@@ -282,7 +282,7 @@ pub const BufferedConnection = struct {
282 const left = buffer.len - out_index;282 const left = buffer.len - out_index;
283283
284 if (available > 0) {284 if (available > 0) {
285 const can_read = @truncate(u16, @min(available, left));285 const can_read = @intCast(u16, @min(available, left));
286286
287 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);287 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);
288 out_index += can_read;288 out_index += can_read;
...@@ -355,8 +355,6 @@ pub const Compression = union(enum) {...@@ -355,8 +355,6 @@ pub const Compression = union(enum) {
355/// A HTTP response originating from a server.355/// A HTTP response originating from a server.
356pub const Response = struct {356pub const Response = struct {
357 pub const ParseError = Allocator.Error || error{357 pub const ParseError = Allocator.Error || error{
358 ShortHttpStatusLine,
359 BadHttpVersion,
360 HttpHeadersInvalid,358 HttpHeadersInvalid,
361 HttpHeaderContinuationsUnsupported,359 HttpHeaderContinuationsUnsupported,
362 HttpTransferEncodingUnsupported,360 HttpTransferEncodingUnsupported,
...@@ -370,12 +368,12 @@ pub const Response = struct {...@@ -370,12 +368,12 @@ pub const Response = struct {
370368
371 const first_line = it.next() orelse return error.HttpHeadersInvalid;369 const first_line = it.next() orelse return error.HttpHeadersInvalid;
372 if (first_line.len < 12)370 if (first_line.len < 12)
373 return error.ShortHttpStatusLine;371 return error.HttpHeadersInvalid;
374372
375 const version: http.Version = switch (int64(first_line[0..8])) {373 const version: http.Version = switch (int64(first_line[0..8])) {
376 int64("HTTP/1.0") => .@"HTTP/1.0",374 int64("HTTP/1.0") => .@"HTTP/1.0",
377 int64("HTTP/1.1") => .@"HTTP/1.1",375 int64("HTTP/1.1") => .@"HTTP/1.1",
378 else => return error.BadHttpVersion,376 else => return error.HttpHeadersInvalid,
379 };377 };
380 if (first_line[8] != ' ') return error.HttpHeadersInvalid;378 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
381 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));379 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
...@@ -695,7 +693,6 @@ pub const Request = struct {...@@ -695,7 +693,6 @@ pub const Request = struct {
695693
696 if (req.method == .CONNECT and req.response.status == .ok) {694 if (req.method == .CONNECT and req.response.status == .ok) {
697 req.connection.data.closing = false;695 req.connection.data.closing = false;
698 req.connection.data.proxied = true;
699 req.response.parser.done = true;696 req.response.parser.done = true;
700 }697 }
701698
...@@ -725,6 +722,11 @@ pub const Request = struct {...@@ -725,6 +722,11 @@ pub const Request = struct {
725 req.response.parser.done = true;722 req.response.parser.done = true;
726 }723 }
727724
725 // HEAD requests have no body
726 if (req.method == .HEAD) {
727 req.response.parser.done = true;
728 }
729
728 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {730 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {
729 req.response.skip = true;731 req.response.skip = true;
730732
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+95-40
...@@ -108,7 +108,7 @@ pub const BufferedConnection = struct {...@@ -108,7 +108,7 @@ pub const BufferedConnection = struct {
108 const nread = try bconn.conn.read(bconn.buf[0..]);108 const nread = try bconn.conn.read(bconn.buf[0..]);
109 if (nread == 0) return error.EndOfStream;109 if (nread == 0) return error.EndOfStream;
110 bconn.start = 0;110 bconn.start = 0;
111 bconn.end = @truncate(u16, nread);111 bconn.end = @intCast(u16, nread);
112 }112 }
113113
114 pub fn peek(bconn: *BufferedConnection) []const u8 {114 pub fn peek(bconn: *BufferedConnection) []const u8 {
...@@ -126,7 +126,7 @@ pub const BufferedConnection = struct {...@@ -126,7 +126,7 @@ pub const BufferedConnection = struct {
126 const left = buffer.len - out_index;126 const left = buffer.len - out_index;
127127
128 if (available > 0) {128 if (available > 0) {
129 const can_read = @truncate(u16, @min(available, left));129 const can_read = @intCast(u16, @min(available, left));
130130
131 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);131 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);
132 out_index += can_read;132 out_index += can_read;
...@@ -199,8 +199,6 @@ pub const Compression = union(enum) {...@@ -199,8 +199,6 @@ pub const Compression = union(enum) {
199/// A HTTP request originating from a client.199/// A HTTP request originating from a client.
200pub const Request = struct {200pub const Request = struct {
201 pub const ParseError = Allocator.Error || error{201 pub const ParseError = Allocator.Error || error{
202 ShortHttpStatusLine,
203 BadHttpVersion,
204 UnknownHttpMethod,202 UnknownHttpMethod,
205 HttpHeadersInvalid,203 HttpHeadersInvalid,
206 HttpHeaderContinuationsUnsupported,204 HttpHeaderContinuationsUnsupported,
...@@ -215,7 +213,7 @@ pub const Request = struct {...@@ -215,7 +213,7 @@ pub const Request = struct {
215213
216 const first_line = it.next() orelse return error.HttpHeadersInvalid;214 const first_line = it.next() orelse return error.HttpHeadersInvalid;
217 if (first_line.len < 10)215 if (first_line.len < 10)
218 return error.ShortHttpStatusLine;216 return error.HttpHeadersInvalid;
219217
220 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;218 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
221 const method_str = first_line[0..method_end];219 const method_str = first_line[0..method_end];
...@@ -229,7 +227,7 @@ pub const Request = struct {...@@ -229,7 +227,7 @@ pub const Request = struct {
229 const version: http.Version = switch (int64(version_str[0..8])) {227 const version: http.Version = switch (int64(version_str[0..8])) {
230 int64("HTTP/1.0") => .@"HTTP/1.0",228 int64("HTTP/1.0") => .@"HTTP/1.0",
231 int64("HTTP/1.1") => .@"HTTP/1.1",229 int64("HTTP/1.1") => .@"HTTP/1.1",
232 else => return error.BadHttpVersion,230 else => return error.HttpHeadersInvalid,
233 };231 };
234232
235 const target = first_line[method_end + 1 .. version_start];233 const target = first_line[method_end + 1 .. version_start];
...@@ -312,7 +310,7 @@ pub const Request = struct {...@@ -312,7 +310,7 @@ pub const Request = struct {
312 transfer_encoding: ?http.TransferEncoding = null,310 transfer_encoding: ?http.TransferEncoding = null,
313 transfer_compression: ?http.ContentEncoding = null,311 transfer_compression: ?http.ContentEncoding = null,
314312
315 headers: http.Headers = undefined,313 headers: http.Headers,
316 parser: proto.HeadersParser,314 parser: proto.HeadersParser,
317 compression: Compression = .none,315 compression: Compression = .none,
318};316};
...@@ -336,42 +334,92 @@ pub const Response = struct {...@@ -336,42 +334,92 @@ pub const Response = struct {
336 headers: http.Headers,334 headers: http.Headers,
337 request: Request,335 request: Request,
338336
337 state: State = .first,
338
339 const State = enum {
340 first,
341 start,
342 waited,
343 responded,
344 finished,
345 };
346
339 pub fn deinit(res: *Response) void {347 pub fn deinit(res: *Response) void {
340 res.server.allocator.destroy(res);348 res.connection.close();
341 }
342349
343 /// 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();350 res.headers.deinit();
351 res.request.headers.deinit();
347352
348 switch (res.request.compression) {353 if (res.request.parser.header_bytes_owned) {
349 .none => {},354 res.request.parser.header_bytes.deinit(res.server.allocator);
350 .deflate => |*deflate| deflate.deinit(),355 }
351 .gzip => |*gzip| gzip.deinit(),356 }
352 .zstd => |*zstd| zstd.deinit(),357
358 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
359 pub fn reset(res: *Response) bool {
360 if (res.state == .first) {
361 res.state = .start;
362 return true;
353 }363 }
354364
355 if (!res.request.parser.done) {365 if (!res.request.parser.done) {
356 // If the response wasn't fully read, then we need to close the connection.366 // If the response wasn't fully read, then we need to close the connection.
357 res.connection.conn.closing = true;367 res.connection.conn.closing = true;
368 return false;
358 }369 }
359370
360 if (res.connection.conn.closing) {371 // A connection is only keep-alive if the Connection header is present and it's value is not "close".
361 res.connection.close();372 // The server and client must both agree
373 const res_connection = res.headers.getFirstValue("connection");
374 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
362375
363 if (res.request.parser.header_bytes_owned) {376 const req_connection = res.request.headers.getFirstValue("connection");
364 res.request.parser.header_bytes.deinit(res.server.allocator);377 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
365 }378 if (res_keepalive and req_keepalive) {
379 res.connection.conn.closing = false;
366 } else {380 } else {
367 res.request.parser.reset();381 res.connection.conn.closing = true;
382 }
383
384 switch (res.request.compression) {
385 .none => {},
386 .deflate => |*deflate| deflate.deinit(),
387 .gzip => |*gzip| gzip.deinit(),
388 .zstd => |*zstd| zstd.deinit(),
368 }389 }
390
391 res.state = .start;
392 res.version = .@"HTTP/1.1";
393 res.status = .ok;
394 res.reason = null;
395
396 res.transfer_encoding = .none;
397
398 res.headers.clearRetainingCapacity();
399
400 res.request.headers.clearRetainingCapacity();
401 res.request.parser.reset();
402
403 res.request = Request{
404 .version = undefined,
405 .method = undefined,
406 .target = undefined,
407 .headers = res.request.headers,
408 .parser = res.request.parser,
409 };
410
411 return !res.connection.conn.closing;
369 }412 }
370413
371 pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };414 pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
372415
373 /// Send the response headers.416 /// Send the response headers.
374 pub fn do(res: *Response) !void {417 pub fn do(res: *Response) !void {
418 switch (res.state) {
419 .waited => res.state = .responded,
420 .first, .start, .responded, .finished => unreachable,
421 }
422
375 var buffered = std.io.bufferedWriter(res.connection.writer());423 var buffered = std.io.bufferedWriter(res.connection.writer());
376 const w = buffered.writer();424 const w = buffered.writer();
377425
...@@ -452,6 +500,11 @@ pub const Response = struct {...@@ -452,6 +500,11 @@ pub const Response = struct {
452500
453 /// Wait for the client to send a complete request head.501 /// Wait for the client to send a complete request head.
454 pub fn wait(res: *Response) WaitError!void {502 pub fn wait(res: *Response) WaitError!void {
503 switch (res.state) {
504 .first, .start => res.state = .waited,
505 .waited, .responded, .finished => unreachable,
506 }
507
455 while (true) {508 while (true) {
456 try res.connection.fill();509 try res.connection.fill();
457510
...@@ -464,17 +517,6 @@ pub const Response = struct {...@@ -464,17 +517,6 @@ pub const Response = struct {
464 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };517 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
465 try res.request.parse(res.request.parser.header_bytes.items);518 try res.request.parse(res.request.parser.header_bytes.items);
466519
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| {520 if (res.request.transfer_encoding) |te| {
479 switch (te) {521 switch (te) {
480 .chunked => {522 .chunked => {
...@@ -515,6 +557,11 @@ pub const Response = struct {...@@ -515,6 +557,11 @@ pub const Response = struct {
515 }557 }
516558
517 pub fn read(res: *Response, buffer: []u8) ReadError!usize {559 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
560 switch (res.state) {
561 .waited, .responded, .finished => {},
562 .first, .start => unreachable,
563 }
564
518 const out_index = switch (res.request.compression) {565 const out_index = switch (res.request.compression) {
519 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,566 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
520 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,567 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
...@@ -564,6 +611,11 @@ pub const Response = struct {...@@ -564,6 +611,11 @@ pub const Response = struct {
564611
565 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.612 /// 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 {613 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
614 switch (res.state) {
615 .responded => {},
616 .first, .waited, .start, .finished => unreachable,
617 }
618
567 switch (res.transfer_encoding) {619 switch (res.transfer_encoding) {
568 .chunked => {620 .chunked => {
569 try res.connection.writer().print("{x}\r\n", .{bytes.len});621 try res.connection.writer().print("{x}\r\n", .{bytes.len});
...@@ -583,7 +635,7 @@ pub const Response = struct {...@@ -583,7 +635,7 @@ pub const Response = struct {
583 }635 }
584 }636 }
585637
586 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {638 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {
587 var index: usize = 0;639 var index: usize = 0;
588 while (index < bytes.len) {640 while (index < bytes.len) {
589 index += try write(req, bytes[index..]);641 index += try write(req, bytes[index..]);
...@@ -594,6 +646,11 @@ pub const Response = struct {...@@ -594,6 +646,11 @@ pub const Response = struct {
594646
595 /// Finish the body of a request. This notifies the server that you have no more data to send.647 /// 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 {648 pub fn finish(res: *Response) FinishError!void {
649 switch (res.state) {
650 .responded => res.state = .finished,
651 .first, .waited, .start, .finished => unreachable,
652 }
653
597 switch (res.transfer_encoding) {654 switch (res.transfer_encoding) {
598 .chunked => try res.connection.writeAll("0\r\n\r\n"),655 .chunked => try res.connection.writeAll("0\r\n\r\n"),
599 .content_length => |len| if (len != 0) return error.MessageNotCompleted,656 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
...@@ -636,11 +693,10 @@ pub const HeaderStrategy = union(enum) {...@@ -636,11 +693,10 @@ pub const HeaderStrategy = union(enum) {
636};693};
637694
638/// Accept a new connection and allocate a Response for it.695/// Accept a new connection and allocate a Response for it.
639pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {696pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!Response {
640 const in = try server.socket.accept();697 const in = try server.socket.accept();
641698
642 const res = try server.allocator.create(Response);699 return Response{
643 res.* = .{
644 .server = server,700 .server = server,
645 .address = in.address,701 .address = in.address,
646 .connection = .{ .conn = .{702 .connection = .{ .conn = .{
...@@ -652,14 +708,13 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {...@@ -652,14 +708,13 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
652 .version = undefined,708 .version = undefined,
653 .method = undefined,709 .method = undefined,
654 .target = undefined,710 .target = undefined,
711 .headers = .{ .allocator = server.allocator, .owned = false },
655 .parser = switch (options) {712 .parser = switch (options) {
656 .dynamic => |max| proto.HeadersParser.initDynamic(max),713 .dynamic => |max| proto.HeadersParser.initDynamic(max),
657 .static => |buf| proto.HeadersParser.initStatic(buf),714 .static => |buf| proto.HeadersParser.initStatic(buf),
658 },715 },
659 },716 },
660 };717 };
661
662 return res;
663}718}
664719
665test "HTTP server handles a chunked transfer coding request" {720test "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+432
...@@ -0,0 +1,432 @@
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(.{}){};
13var gpa_client = std.heap.GeneralPurposeAllocator(.{}){};
14
15const salloc = gpa_server.allocator();
16const calloc = gpa_client.allocator();
17
18fn handleRequest(res: *Server.Response) !void {
19 const log = std.log.scoped(.server);
20
21 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });
22
23 const body = try res.reader().readAllAlloc(salloc, 8192);
24 defer salloc.free(body);
25
26 if (res.request.headers.contains("connection")) {
27 try res.headers.append("connection", "keep-alive");
28 }
29
30 if (mem.startsWith(u8, res.request.target, "/get")) {
31 if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) {
32 res.transfer_encoding = .chunked;
33 } else {
34 res.transfer_encoding = .{ .content_length = 14 };
35 }
36
37 try res.headers.append("content-type", "text/plain");
38
39 try res.do();
40 if (res.request.method != .HEAD) {
41 try res.writeAll("Hello, ");
42 try res.writeAll("World!\n");
43 try res.finish();
44 }
45 } else if (mem.eql(u8, res.request.target, "/echo-content")) {
46 try testing.expectEqualStrings("Hello, World!\n", body);
47 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
48
49 if (res.request.headers.contains("transfer-encoding")) {
50 try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?);
51 res.transfer_encoding = .chunked;
52 } else {
53 res.transfer_encoding = .{ .content_length = 14 };
54 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);
55 }
56
57 try res.do();
58 try res.writeAll("Hello, ");
59 try res.writeAll("World!\n");
60 try res.finish();
61 } else if (mem.eql(u8, res.request.target, "/trailer")) {
62 res.transfer_encoding = .chunked;
63
64 try res.do();
65 try res.writeAll("Hello, ");
66 try res.writeAll("World!\n");
67 // try res.finish();
68 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");
69 } else if (mem.eql(u8, res.request.target, "/redirect/1")) {
70 res.transfer_encoding = .chunked;
71
72 res.status = .found;
73 try res.headers.append("location", "../../get");
74
75 try res.do();
76 try res.writeAll("Hello, ");
77 try res.writeAll("Redirected!\n");
78 try res.finish();
79 } else if (mem.eql(u8, res.request.target, "/redirect/2")) {
80 res.transfer_encoding = .chunked;
81
82 res.status = .found;
83 try res.headers.append("location", "/redirect/1");
84
85 try res.do();
86 try res.writeAll("Hello, ");
87 try res.writeAll("Redirected!\n");
88 try res.finish();
89 } else if (mem.eql(u8, res.request.target, "/redirect/3")) {
90 res.transfer_encoding = .chunked;
91
92 const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{res.server.socket.listen_address.getPort()});
93 defer salloc.free(location);
94
95 res.status = .found;
96 try res.headers.append("location", location);
97
98 try res.do();
99 try res.writeAll("Hello, ");
100 try res.writeAll("Redirected!\n");
101 try res.finish();
102 } else if (mem.eql(u8, res.request.target, "/redirect/4")) {
103 res.transfer_encoding = .chunked;
104
105 res.status = .found;
106 try res.headers.append("location", "/redirect/3");
107
108 try res.do();
109 try res.writeAll("Hello, ");
110 try res.writeAll("Redirected!\n");
111 try res.finish();
112 } else {
113 res.status = .not_found;
114 try res.do();
115 }
116}
117
118var handle_new_requests = true;
119
120fn runServer(srv: *Server) !void {
121 outer: while (handle_new_requests) {
122 var res = try srv.accept(.{ .dynamic = max_header_size });
123 defer res.deinit();
124
125 while (res.reset()) {
126 res.wait() catch |err| switch (err) {
127 error.HttpHeadersInvalid => continue :outer,
128 error.EndOfStream => continue,
129 else => return err,
130 };
131
132 try handleRequest(&res);
133 }
134 }
135}
136
137fn serverThread(srv: *Server) void {
138 defer srv.deinit();
139 defer _ = gpa_server.deinit();
140
141 runServer(srv) catch |err| {
142 std.debug.print("server error: {}\n", .{err});
143
144 if (@errorReturnTrace()) |trace| {
145 std.debug.dumpStackTrace(trace.*);
146 }
147
148 _ = gpa_server.deinit();
149 std.os.exit(1);
150 };
151}
152
153fn killServer(addr: std.net.Address) void {
154 handle_new_requests = false;
155
156 const conn = std.net.tcpConnectToAddress(addr) catch return;
157 conn.close();
158}
159
160pub fn main() !void {
161 const log = std.log.scoped(.client);
162
163 defer _ = gpa_client.deinit();
164
165 var server = Server.init(salloc, .{ .reuse_address = true });
166
167 const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable;
168 try server.listen(addr);
169
170 const port = server.socket.listen_address.getPort();
171
172 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
173
174 var client = Client{ .allocator = calloc };
175
176 defer client.deinit();
177
178 { // read content-length response
179 var h = http.Headers{ .allocator = calloc };
180 defer h.deinit();
181
182 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
183 defer calloc.free(location);
184 const uri = try std.Uri.parse(location);
185
186 log.info("{s}", .{location});
187 var req = try client.request(.GET, uri, h, .{});
188 defer req.deinit();
189
190 try req.start();
191 try req.wait();
192
193 const body = try req.reader().readAllAlloc(calloc, 8192);
194 defer calloc.free(body);
195
196 try testing.expectEqualStrings("Hello, World!\n", body);
197 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
198 }
199
200 { // send head request and not read chunked
201 var h = http.Headers{ .allocator = calloc };
202 defer h.deinit();
203
204 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
205 defer calloc.free(location);
206 const uri = try std.Uri.parse(location);
207
208 log.info("{s}", .{location});
209 var req = try client.request(.HEAD, uri, h, .{});
210 defer req.deinit();
211
212 try req.start();
213 try req.wait();
214
215 const body = try req.reader().readAllAlloc(calloc, 8192);
216 defer calloc.free(body);
217
218 try testing.expectEqualStrings("", body);
219 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
220 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);
221 }
222
223 { // read chunked response
224 var h = http.Headers{ .allocator = calloc };
225 defer h.deinit();
226
227 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
228 defer calloc.free(location);
229 const uri = try std.Uri.parse(location);
230
231 log.info("{s}", .{location});
232 var req = try client.request(.GET, uri, h, .{});
233 defer req.deinit();
234
235 try req.start();
236 try req.wait();
237
238 const body = try req.reader().readAllAlloc(calloc, 8192);
239 defer calloc.free(body);
240
241 try testing.expectEqualStrings("Hello, World!\n", body);
242 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
243 }
244
245 { // send head request and not read chunked
246 var h = http.Headers{ .allocator = calloc };
247 defer h.deinit();
248
249 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
250 defer calloc.free(location);
251 const uri = try std.Uri.parse(location);
252
253 log.info("{s}", .{location});
254 var req = try client.request(.HEAD, uri, h, .{});
255 defer req.deinit();
256
257 try req.start();
258 try req.wait();
259
260 const body = try req.reader().readAllAlloc(calloc, 8192);
261 defer calloc.free(body);
262
263 try testing.expectEqualStrings("", body);
264 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
265 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);
266 }
267
268 { // check trailing headers
269 var h = http.Headers{ .allocator = calloc };
270 defer h.deinit();
271
272 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});
273 defer calloc.free(location);
274 const uri = try std.Uri.parse(location);
275
276 log.info("{s}", .{location});
277 var req = try client.request(.GET, uri, h, .{});
278 defer req.deinit();
279
280 try req.start();
281 try req.wait();
282
283 const body = try req.reader().readAllAlloc(calloc, 8192);
284 defer calloc.free(body);
285
286 try testing.expectEqualStrings("Hello, World!\n", body);
287 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
288 }
289
290 { // send content-length request
291 var h = http.Headers{ .allocator = calloc };
292 defer h.deinit();
293
294 try h.append("content-type", "text/plain");
295
296 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
297 defer calloc.free(location);
298 const uri = try std.Uri.parse(location);
299
300 log.info("{s}", .{location});
301 var req = try client.request(.POST, uri, h, .{});
302 defer req.deinit();
303
304 req.transfer_encoding = .{ .content_length = 14 };
305
306 try req.start();
307 try req.writeAll("Hello, ");
308 try req.writeAll("World!\n");
309 try req.finish();
310
311 try req.wait();
312
313 const body = try req.reader().readAllAlloc(calloc, 8192);
314 defer calloc.free(body);
315
316 try testing.expectEqualStrings("Hello, World!\n", body);
317 }
318
319 { // send chunked request
320 var h = http.Headers{ .allocator = calloc };
321 defer h.deinit();
322
323 try h.append("content-type", "text/plain");
324
325 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
326 defer calloc.free(location);
327 const uri = try std.Uri.parse(location);
328
329 log.info("{s}", .{location});
330 var req = try client.request(.POST, uri, h, .{});
331 defer req.deinit();
332
333 req.transfer_encoding = .chunked;
334
335 try req.start();
336 try req.writeAll("Hello, ");
337 try req.writeAll("World!\n");
338 try req.finish();
339
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 }
347
348 { // relative redirect
349 var h = http.Headers{ .allocator = calloc };
350 defer h.deinit();
351
352 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});
353 defer calloc.free(location);
354 const uri = try std.Uri.parse(location);
355
356 log.info("{s}", .{location});
357 var req = try client.request(.GET, uri, h, .{});
358 defer req.deinit();
359
360 try req.start();
361 try req.wait();
362
363 const body = try req.reader().readAllAlloc(calloc, 8192);
364 defer calloc.free(body);
365
366 try testing.expectEqualStrings("Hello, World!\n", body);
367 }
368
369 { // redirect from root
370 var h = http.Headers{ .allocator = calloc };
371 defer h.deinit();
372
373 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});
374 defer calloc.free(location);
375 const uri = try std.Uri.parse(location);
376
377 log.info("{s}", .{location});
378 var req = try client.request(.GET, uri, h, .{});
379 defer req.deinit();
380
381 try req.start();
382 try req.wait();
383
384 const body = try req.reader().readAllAlloc(calloc, 8192);
385 defer calloc.free(body);
386
387 try testing.expectEqualStrings("Hello, World!\n", body);
388 }
389
390 { // absolute redirect
391 var h = http.Headers{ .allocator = calloc };
392 defer h.deinit();
393
394 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});
395 defer calloc.free(location);
396 const uri = try std.Uri.parse(location);
397
398 log.info("{s}", .{location});
399 var req = try client.request(.GET, uri, h, .{});
400 defer req.deinit();
401
402 try req.start();
403 try req.wait();
404
405 const body = try req.reader().readAllAlloc(calloc, 8192);
406 defer calloc.free(body);
407
408 try testing.expectEqualStrings("Hello, World!\n", body);
409 }
410
411 { // too many redirects
412 var h = http.Headers{ .allocator = calloc };
413 defer h.deinit();
414
415 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});
416 defer calloc.free(location);
417 const uri = try std.Uri.parse(location);
418
419 log.info("{s}", .{location});
420 var req = try client.request(.GET, uri, h, .{});
421 defer req.deinit();
422
423 try req.start();
424 req.wait() catch |err| switch (err) {
425 error.TooManyHttpRedirects => {},
426 else => return err,
427 };
428 }
429
430 killServer(server.socket.listen_address);
431 server_thread.join();
432}