master
   1const builtin = @import("builtin");
   2const native_endian = builtin.cpu.arch.endian();
   3
   4const std = @import("std");
   5const http = std.http;
   6const mem = std.mem;
   7const net = std.Io.net;
   8const Io = std.Io;
   9const expect = std.testing.expect;
  10const expectEqual = std.testing.expectEqual;
  11const expectEqualStrings = std.testing.expectEqualStrings;
  12const expectError = std.testing.expectError;
  13
  14test "trailers" {
  15    const io = std.testing.io;
  16    const test_server = try createTestServer(io, struct {
  17        fn run(test_server: *TestServer) anyerror!void {
  18            const net_server = &test_server.net_server;
  19            var recv_buffer: [1024]u8 = undefined;
  20            var send_buffer: [1024]u8 = undefined;
  21            var remaining: usize = 1;
  22            while (remaining != 0) : (remaining -= 1) {
  23                var stream = try net_server.accept(io);
  24                defer stream.close(io);
  25
  26                var connection_br = stream.reader(io, &recv_buffer);
  27                var connection_bw = stream.writer(io, &send_buffer);
  28                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
  29
  30                try expectEqual(.ready, server.reader.state);
  31                var request = try server.receiveHead();
  32                try serve(&request);
  33                try expectEqual(.ready, server.reader.state);
  34            }
  35        }
  36
  37        fn serve(request: *http.Server.Request) !void {
  38            try expectEqualStrings(request.head.target, "/trailer");
  39
  40            var response = try request.respondStreaming(&.{}, .{});
  41            try response.writer.writeAll("Hello, ");
  42            try response.flush();
  43            try response.writer.writeAll("World!\n");
  44            try response.flush();
  45            try response.endChunked(.{
  46                .trailers = &.{
  47                    .{ .name = "X-Checksum", .value = "aaaa" },
  48                },
  49            });
  50        }
  51    });
  52    defer test_server.destroy();
  53
  54    const gpa = std.testing.allocator;
  55
  56    var client: http.Client = .{ .allocator = gpa, .io = io };
  57    defer client.deinit();
  58
  59    const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{
  60        test_server.port(),
  61    });
  62    defer gpa.free(location);
  63    const uri = try std.Uri.parse(location);
  64
  65    {
  66        var req = try client.request(.GET, uri, .{});
  67        defer req.deinit();
  68
  69        try req.sendBodiless();
  70        var response = try req.receiveHead(&.{});
  71
  72        {
  73            var it = response.head.iterateHeaders();
  74            const header = it.next().?;
  75            try expectEqualStrings("transfer-encoding", header.name);
  76            try expectEqualStrings("chunked", header.value);
  77            try expectEqual(null, it.next());
  78        }
  79
  80        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
  81        defer gpa.free(body);
  82
  83        try expectEqualStrings("Hello, World!\n", body);
  84
  85        {
  86            var it = response.iterateTrailers();
  87            const header = it.next().?;
  88            try expectEqualStrings("X-Checksum", header.name);
  89            try expectEqualStrings("aaaa", header.value);
  90            try expectEqual(null, it.next());
  91        }
  92    }
  93
  94    // connection has been kept alive
  95    try expect(client.connection_pool.free_len == 1);
  96}
  97
  98test "HTTP server handles a chunked transfer coding request" {
  99    const io = std.testing.io;
 100    const test_server = try createTestServer(io, struct {
 101        fn run(test_server: *TestServer) anyerror!void {
 102            const net_server = &test_server.net_server;
 103            var recv_buffer: [8192]u8 = undefined;
 104            var send_buffer: [500]u8 = undefined;
 105            var stream = try net_server.accept(io);
 106            defer stream.close(io);
 107
 108            var connection_br = stream.reader(io, &recv_buffer);
 109            var connection_bw = stream.writer(io, &send_buffer);
 110            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 111            var request = try server.receiveHead();
 112
 113            try expect(request.head.transfer_encoding == .chunked);
 114
 115            var buf: [128]u8 = undefined;
 116            var br = try request.readerExpectContinue(&.{});
 117            const n = try br.readSliceShort(&buf);
 118            try expectEqualStrings("ABCD", buf[0..n]);
 119
 120            try request.respond("message from server!\n", .{
 121                .extra_headers = &.{
 122                    .{ .name = "content-type", .value = "text/plain" },
 123                },
 124                .keep_alive = false,
 125            });
 126        }
 127    });
 128    defer test_server.destroy();
 129
 130    const request_bytes =
 131        "POST / HTTP/1.1\r\n" ++
 132        "Content-Type: text/plain\r\n" ++
 133        "Transfer-Encoding: chunked\r\n" ++
 134        "\r\n" ++
 135        "1\r\n" ++
 136        "A\r\n" ++
 137        "1\r\n" ++
 138        "B\r\n" ++
 139        "2\r\n" ++
 140        "CD\r\n" ++
 141        "0\r\n" ++
 142        "\r\n";
 143
 144    const host_name: net.HostName = try .init("127.0.0.1");
 145    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
 146    defer stream.close(io);
 147    var stream_writer = stream.writer(io, &.{});
 148    try stream_writer.interface.writeAll(request_bytes);
 149
 150    const gpa = std.testing.allocator;
 151    const expected_response =
 152        "HTTP/1.1 200 OK\r\n" ++
 153        "connection: close\r\n" ++
 154        "content-length: 21\r\n" ++
 155        "content-type: text/plain\r\n" ++
 156        "\r\n" ++
 157        "message from server!\n";
 158    var stream_reader = stream.reader(io, &.{});
 159    const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1));
 160    defer gpa.free(response);
 161    try expectEqualStrings(expected_response, response);
 162}
 163
 164test "echo content server" {
 165    const io = std.testing.io;
 166    const test_server = try createTestServer(io, struct {
 167        fn run(test_server: *TestServer) anyerror!void {
 168            const net_server = &test_server.net_server;
 169            var recv_buffer: [1024]u8 = undefined;
 170            var send_buffer: [100]u8 = undefined;
 171
 172            accept: while (!test_server.shutting_down) {
 173                var stream = try net_server.accept(io);
 174                defer stream.close(io);
 175
 176                var connection_br = stream.reader(io, &recv_buffer);
 177                var connection_bw = stream.writer(io, &send_buffer);
 178                var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 179
 180                while (http_server.reader.state == .ready) {
 181                    var request = http_server.receiveHead() catch |err| switch (err) {
 182                        error.HttpConnectionClosing => continue :accept,
 183                        else => |e| return e,
 184                    };
 185                    if (mem.eql(u8, request.head.target, "/end")) {
 186                        return request.respond("", .{ .keep_alive = false });
 187                    }
 188                    if (request.head.expect) |expect_header_value| {
 189                        if (mem.eql(u8, expect_header_value, "garbage")) {
 190                            try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{}));
 191                            request.head.expect = null;
 192                            try request.respond("", .{
 193                                .keep_alive = false,
 194                                .status = .expectation_failed,
 195                            });
 196                            continue;
 197                        }
 198                    }
 199                    handleRequest(&request) catch |err| {
 200                        // This message helps the person troubleshooting determine whether
 201                        // output comes from the server thread or the client thread.
 202                        std.debug.print("handleRequest failed with '{s}'\n", .{@errorName(err)});
 203                        return err;
 204                    };
 205                }
 206            }
 207        }
 208
 209        fn handleRequest(request: *http.Server.Request) !void {
 210            //std.debug.print("server received {s} {s} {s}\n", .{
 211            //    @tagName(request.head.method),
 212            //    @tagName(request.head.version),
 213            //    request.head.target,
 214            //});
 215
 216            try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
 217            try expectEqualStrings("text/plain", request.head.content_type.?);
 218
 219            // head strings expire here
 220            const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited);
 221            defer std.testing.allocator.free(body);
 222
 223            try expectEqualStrings("Hello, World!\n", body);
 224
 225            var response = try request.respondStreaming(&.{}, .{
 226                .content_length = switch (request.head.transfer_encoding) {
 227                    .chunked => null,
 228                    .none => len: {
 229                        try expectEqual(14, request.head.content_length.?);
 230                        break :len 14;
 231                    },
 232                },
 233            });
 234            try response.flush(); // Test an early flush to send the HTTP headers before the body.
 235            const w = &response.writer;
 236            try w.writeAll("Hello, ");
 237            try w.writeAll("World!\n");
 238            try response.end();
 239            //std.debug.print("  server finished responding\n", .{});
 240        }
 241    });
 242    defer test_server.destroy();
 243
 244    {
 245        var client: http.Client = .{ .allocator = std.testing.allocator, .io = io };
 246        defer client.deinit();
 247
 248        try echoTests(&client, test_server.port());
 249    }
 250}
 251
 252test "Server.Request.respondStreaming non-chunked, unknown content-length" {
 253    const io = std.testing.io;
 254
 255    if (builtin.os.tag == .windows) {
 256        // https://github.com/ziglang/zig/issues/21457
 257        return error.SkipZigTest;
 258    }
 259
 260    // In this case, the response is expected to stream until the connection is
 261    // closed, indicating the end of the body.
 262    const test_server = try createTestServer(io, struct {
 263        fn run(test_server: *TestServer) anyerror!void {
 264            const net_server = &test_server.net_server;
 265            var recv_buffer: [1000]u8 = undefined;
 266            var send_buffer: [500]u8 = undefined;
 267            var remaining: usize = 1;
 268            while (remaining != 0) : (remaining -= 1) {
 269                var stream = try net_server.accept(io);
 270                defer stream.close(io);
 271
 272                var connection_br = stream.reader(io, &recv_buffer);
 273                var connection_bw = stream.writer(io, &send_buffer);
 274                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 275
 276                try expectEqual(.ready, server.reader.state);
 277                var request = try server.receiveHead();
 278                try expectEqualStrings(request.head.target, "/foo");
 279                var buf: [30]u8 = undefined;
 280                var response = try request.respondStreaming(&buf, .{
 281                    .respond_options = .{
 282                        .transfer_encoding = .none,
 283                    },
 284                });
 285                const w = &response.writer;
 286                for (0..500) |i| {
 287                    try w.print("{d}, ah ha ha!\n", .{i});
 288                }
 289                try w.flush();
 290                try response.end();
 291                try expectEqual(.closing, server.reader.state);
 292            }
 293        }
 294    });
 295    defer test_server.destroy();
 296
 297    const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
 298    const host_name: net.HostName = try .init("127.0.0.1");
 299    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
 300    defer stream.close(io);
 301    var stream_writer = stream.writer(io, &.{});
 302    try stream_writer.interface.writeAll(request_bytes);
 303
 304    var stream_reader = stream.reader(io, &.{});
 305    const gpa = std.testing.allocator;
 306    const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
 307    defer gpa.free(response);
 308
 309    var expected_response = std.array_list.Managed(u8).init(gpa);
 310    defer expected_response.deinit();
 311
 312    try expected_response.appendSlice("HTTP/1.1 200 OK\r\nconnection: close\r\n\r\n");
 313
 314    {
 315        var total: usize = 0;
 316        for (0..500) |i| {
 317            var buf: [30]u8 = undefined;
 318            const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
 319            try expected_response.appendSlice(line);
 320            total += line.len;
 321        }
 322        try expectEqual(7390, total);
 323    }
 324
 325    try expectEqualStrings(expected_response.items, response);
 326}
 327
 328test "receiving arbitrary http headers from the client" {
 329    const io = std.testing.io;
 330
 331    const test_server = try createTestServer(io, struct {
 332        fn run(test_server: *TestServer) anyerror!void {
 333            const net_server = &test_server.net_server;
 334            var recv_buffer: [666]u8 = undefined;
 335            var send_buffer: [777]u8 = undefined;
 336            var remaining: usize = 1;
 337            while (remaining != 0) : (remaining -= 1) {
 338                var stream = try net_server.accept(io);
 339                defer stream.close(io);
 340
 341                var connection_br = stream.reader(io, &recv_buffer);
 342                var connection_bw = stream.writer(io, &send_buffer);
 343                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 344
 345                try expectEqual(.ready, server.reader.state);
 346                var request = try server.receiveHead();
 347                try expectEqualStrings("/bar", request.head.target);
 348                var it = request.iterateHeaders();
 349                {
 350                    const header = it.next().?;
 351                    try expectEqualStrings("CoNneCtIoN", header.name);
 352                    try expectEqualStrings("close", header.value);
 353                    try expect(!it.is_trailer);
 354                }
 355                {
 356                    const header = it.next().?;
 357                    try expectEqualStrings("aoeu", header.name);
 358                    try expectEqualStrings("asdf", header.value);
 359                    try expect(!it.is_trailer);
 360                }
 361                try request.respond("", .{});
 362            }
 363        }
 364    });
 365    defer test_server.destroy();
 366
 367    const request_bytes = "GET /bar HTTP/1.1\r\n" ++
 368        "CoNneCtIoN:close\r\n" ++
 369        "aoeu:  asdf \r\n" ++
 370        "\r\n";
 371    const host_name: net.HostName = try .init("127.0.0.1");
 372    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
 373    defer stream.close(io);
 374    var stream_writer = stream.writer(io, &.{});
 375    try stream_writer.interface.writeAll(request_bytes);
 376
 377    var stream_reader = stream.reader(io, &.{});
 378    const gpa = std.testing.allocator;
 379    const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
 380    defer gpa.free(response);
 381
 382    var expected_response = std.array_list.Managed(u8).init(gpa);
 383    defer expected_response.deinit();
 384
 385    try expected_response.appendSlice("HTTP/1.1 200 OK\r\n");
 386    try expected_response.appendSlice("connection: close\r\n");
 387    try expected_response.appendSlice("content-length: 0\r\n\r\n");
 388    try expectEqualStrings(expected_response.items, response);
 389}
 390
 391test "general client/server API coverage" {
 392    const io = std.testing.io;
 393
 394    if (builtin.os.tag == .windows) {
 395        // This test was never passing on Windows.
 396        return error.SkipZigTest;
 397    }
 398
 399    const test_server = try createTestServer(io, struct {
 400        fn run(test_server: *TestServer) anyerror!void {
 401            const net_server = &test_server.net_server;
 402            var recv_buffer: [1024]u8 = undefined;
 403            var send_buffer: [100]u8 = undefined;
 404
 405            outer: while (!test_server.shutting_down) {
 406                var stream = try net_server.accept(io);
 407                defer stream.close(io);
 408
 409                var connection_br = stream.reader(io, &recv_buffer);
 410                var connection_bw = stream.writer(io, &send_buffer);
 411                var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 412
 413                while (http_server.reader.state == .ready) {
 414                    var request = http_server.receiveHead() catch |err| switch (err) {
 415                        error.HttpConnectionClosing => continue :outer,
 416                        else => |e| return e,
 417                    };
 418
 419                    try handleRequest(&request, net_server.socket.address.getPort());
 420                }
 421            }
 422        }
 423
 424        fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
 425            const log = std.log.scoped(.server);
 426            const gpa = std.testing.allocator;
 427
 428            log.info("{t} {t} {s}", .{ request.head.method, request.head.version, request.head.target });
 429            const target = try gpa.dupe(u8, request.head.target);
 430            defer gpa.free(target);
 431
 432            const reader = (try request.readerExpectContinue(&.{}));
 433            const body = try reader.allocRemaining(gpa, .unlimited);
 434            defer gpa.free(body);
 435
 436            if (mem.startsWith(u8, target, "/get")) {
 437                var response = try request.respondStreaming(&.{}, .{
 438                    .content_length = if (mem.indexOf(u8, target, "?chunked") == null)
 439                        14
 440                    else
 441                        null,
 442                    .respond_options = .{
 443                        .extra_headers = &.{
 444                            .{ .name = "content-type", .value = "text/plain" },
 445                        },
 446                    },
 447                });
 448                const w = &response.writer;
 449                try w.writeAll("Hello, ");
 450                try w.writeAll("World!\n");
 451                try response.end();
 452                // Writing again would cause an assertion failure.
 453            } else if (mem.startsWith(u8, target, "/large")) {
 454                var response = try request.respondStreaming(&.{}, .{
 455                    .content_length = 14 * 1024 + 14 * 10,
 456                });
 457
 458                try response.flush(); // Test an early flush to send the HTTP headers before the body.
 459
 460                const w = &response.writer;
 461
 462                var i: u32 = 0;
 463                while (i < 5) : (i += 1) {
 464                    try w.writeAll("Hello, World!\n");
 465                }
 466
 467                var vec: [1][]const u8 = .{"Hello, World!\n"};
 468                try w.writeSplatAll(&vec, 1024);
 469
 470                i = 0;
 471                while (i < 5) : (i += 1) {
 472                    try w.writeAll("Hello, World!\n");
 473                }
 474
 475                try response.end();
 476            } else if (mem.eql(u8, target, "/redirect/1")) {
 477                var response = try request.respondStreaming(&.{}, .{
 478                    .respond_options = .{
 479                        .status = .found,
 480                        .extra_headers = &.{
 481                            .{ .name = "location", .value = "../../get" },
 482                        },
 483                    },
 484                });
 485
 486                const w = &response.writer;
 487                try w.writeAll("Hello, ");
 488                try w.writeAll("Redirected!\n");
 489                try response.end();
 490            } else if (mem.eql(u8, target, "/redirect/2")) {
 491                try request.respond("Hello, Redirected!\n", .{
 492                    .status = .found,
 493                    .extra_headers = &.{
 494                        .{ .name = "location", .value = "/redirect/1" },
 495                    },
 496                });
 497            } else if (mem.eql(u8, target, "/redirect/3")) {
 498                const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{
 499                    listen_port,
 500                });
 501                defer gpa.free(location);
 502
 503                try request.respond("Hello, Redirected!\n", .{
 504                    .status = .found,
 505                    .extra_headers = &.{
 506                        .{ .name = "location", .value = location },
 507                    },
 508                });
 509            } else if (mem.eql(u8, target, "/redirect/4")) {
 510                try request.respond("Hello, Redirected!\n", .{
 511                    .status = .found,
 512                    .extra_headers = &.{
 513                        .{ .name = "location", .value = "/redirect/3" },
 514                    },
 515                });
 516            } else if (mem.eql(u8, target, "/redirect/5")) {
 517                try request.respond("Hello, Redirected!\n", .{
 518                    .status = .found,
 519                    .extra_headers = &.{
 520                        .{ .name = "location", .value = "/%2525" },
 521                    },
 522                });
 523            } else if (mem.eql(u8, target, "/%2525")) {
 524                try request.respond("Encoded redirect successful!\n", .{});
 525            } else if (mem.eql(u8, target, "/redirect/invalid")) {
 526                const invalid_port = try getUnusedTcpPort();
 527                const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
 528                defer gpa.free(location);
 529
 530                try request.respond("", .{
 531                    .status = .found,
 532                    .extra_headers = &.{
 533                        .{ .name = "location", .value = location },
 534                    },
 535                });
 536            } else if (mem.eql(u8, target, "/empty")) {
 537                try request.respond("", .{
 538                    .extra_headers = &.{
 539                        .{ .name = "empty", .value = "" },
 540                    },
 541                });
 542            } else {
 543                try request.respond("", .{ .status = .not_found });
 544            }
 545        }
 546
 547        fn getUnusedTcpPort() !u16 {
 548            const addr = try net.IpAddress.parse("127.0.0.1", 0);
 549            var s = try addr.listen(io, .{});
 550            defer s.deinit(io);
 551            return s.socket.address.getPort();
 552        }
 553    });
 554    defer test_server.destroy();
 555
 556    const log = std.log.scoped(.client);
 557
 558    const gpa = std.testing.allocator;
 559    var client: http.Client = .{ .allocator = gpa, .io = io };
 560    defer client.deinit();
 561
 562    const port = test_server.port();
 563
 564    { // read content-length response
 565        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
 566        defer gpa.free(location);
 567        const uri = try std.Uri.parse(location);
 568
 569        log.info("{s}", .{location});
 570        var redirect_buffer: [1024]u8 = undefined;
 571        var req = try client.request(.GET, uri, .{});
 572        defer req.deinit();
 573
 574        try req.sendBodiless();
 575        var response = try req.receiveHead(&redirect_buffer);
 576
 577        try expectEqualStrings("text/plain", response.head.content_type.?);
 578
 579        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 580        defer gpa.free(body);
 581
 582        try expectEqualStrings("Hello, World!\n", body);
 583    }
 584
 585    // connection has been kept alive
 586    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 587
 588    { // read large content-length response
 589        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/large", .{port});
 590        defer gpa.free(location);
 591        const uri = try std.Uri.parse(location);
 592
 593        log.info("{s}", .{location});
 594        var redirect_buffer: [1024]u8 = undefined;
 595        var req = try client.request(.GET, uri, .{});
 596        defer req.deinit();
 597
 598        try req.sendBodiless();
 599        var response = try req.receiveHead(&redirect_buffer);
 600
 601        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 602        defer gpa.free(body);
 603
 604        try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
 605    }
 606
 607    // connection has been kept alive
 608    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 609
 610    { // send head request and not read chunked
 611        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
 612        defer gpa.free(location);
 613        const uri = try std.Uri.parse(location);
 614
 615        log.info("{s}", .{location});
 616        var redirect_buffer: [1024]u8 = undefined;
 617        var req = try client.request(.HEAD, uri, .{});
 618        defer req.deinit();
 619
 620        try req.sendBodiless();
 621        var response = try req.receiveHead(&redirect_buffer);
 622
 623        try expectEqualStrings("text/plain", response.head.content_type.?);
 624        try expectEqual(14, response.head.content_length.?);
 625
 626        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 627        defer gpa.free(body);
 628
 629        try expectEqualStrings("", body);
 630    }
 631
 632    // connection has been kept alive
 633    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 634
 635    { // read chunked response
 636        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
 637        defer gpa.free(location);
 638        const uri = try std.Uri.parse(location);
 639
 640        log.info("{s}", .{location});
 641        var redirect_buffer: [1024]u8 = undefined;
 642        var req = try client.request(.GET, uri, .{});
 643        defer req.deinit();
 644
 645        try req.sendBodiless();
 646        var response = try req.receiveHead(&redirect_buffer);
 647
 648        try expectEqualStrings("text/plain", response.head.content_type.?);
 649
 650        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 651        defer gpa.free(body);
 652
 653        try expectEqualStrings("Hello, World!\n", body);
 654    }
 655
 656    // connection has been kept alive
 657    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 658
 659    { // send head request and not read chunked
 660        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
 661        defer gpa.free(location);
 662        const uri = try std.Uri.parse(location);
 663
 664        log.info("{s}", .{location});
 665        var redirect_buffer: [1024]u8 = undefined;
 666        var req = try client.request(.HEAD, uri, .{});
 667        defer req.deinit();
 668
 669        try req.sendBodiless();
 670        var response = try req.receiveHead(&redirect_buffer);
 671
 672        try expectEqualStrings("text/plain", response.head.content_type.?);
 673        try expect(response.head.transfer_encoding == .chunked);
 674
 675        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 676        defer gpa.free(body);
 677
 678        try expectEqualStrings("", body);
 679    }
 680
 681    // connection has been kept alive
 682    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 683
 684    { // read content-length response with connection close
 685        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
 686        defer gpa.free(location);
 687        const uri = try std.Uri.parse(location);
 688
 689        log.info("{s}", .{location});
 690        var redirect_buffer: [1024]u8 = undefined;
 691        var req = try client.request(.GET, uri, .{
 692            .keep_alive = false,
 693        });
 694        defer req.deinit();
 695
 696        try req.sendBodiless();
 697        var response = try req.receiveHead(&redirect_buffer);
 698
 699        try expectEqualStrings("text/plain", response.head.content_type.?);
 700
 701        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 702        defer gpa.free(body);
 703
 704        try expectEqualStrings("Hello, World!\n", body);
 705    }
 706
 707    // connection has been closed
 708    try expect(client.connection_pool.free_len == 0);
 709
 710    { // handle empty header field value
 711        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/empty", .{port});
 712        defer gpa.free(location);
 713        const uri = try std.Uri.parse(location);
 714
 715        log.info("{s}", .{location});
 716        var redirect_buffer: [1024]u8 = undefined;
 717        var req = try client.request(.GET, uri, .{
 718            .extra_headers = &.{
 719                .{ .name = "empty", .value = "" },
 720            },
 721        });
 722        defer req.deinit();
 723
 724        try req.sendBodiless();
 725        var response = try req.receiveHead(&redirect_buffer);
 726
 727        try std.testing.expectEqual(.ok, response.head.status);
 728
 729        var it = response.head.iterateHeaders();
 730        {
 731            const header = it.next().?;
 732            try expect(!it.is_trailer);
 733            try expectEqualStrings("content-length", header.name);
 734            try expectEqualStrings("0", header.value);
 735        }
 736
 737        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 738        defer gpa.free(body);
 739
 740        try expectEqualStrings("", body);
 741
 742        {
 743            const header = it.next().?;
 744            try expect(!it.is_trailer);
 745            try expectEqualStrings("empty", header.name);
 746            try expectEqualStrings("", header.value);
 747        }
 748        try expectEqual(null, it.next());
 749    }
 750
 751    // connection has been kept alive
 752    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 753
 754    { // relative redirect
 755        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/1", .{port});
 756        defer gpa.free(location);
 757        const uri = try std.Uri.parse(location);
 758
 759        log.info("{s}", .{location});
 760        var redirect_buffer: [1024]u8 = undefined;
 761        var req = try client.request(.GET, uri, .{});
 762        defer req.deinit();
 763
 764        try req.sendBodiless();
 765        var response = try req.receiveHead(&redirect_buffer);
 766
 767        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 768        defer gpa.free(body);
 769
 770        try expectEqualStrings("Hello, World!\n", body);
 771    }
 772
 773    // connection has been kept alive
 774    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 775
 776    { // redirect from root
 777        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{port});
 778        defer gpa.free(location);
 779        const uri = try std.Uri.parse(location);
 780
 781        log.info("{s}", .{location});
 782        var redirect_buffer: [1024]u8 = undefined;
 783        var req = try client.request(.GET, uri, .{});
 784        defer req.deinit();
 785
 786        try req.sendBodiless();
 787        var response = try req.receiveHead(&redirect_buffer);
 788
 789        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 790        defer gpa.free(body);
 791
 792        try expectEqualStrings("Hello, World!\n", body);
 793    }
 794
 795    // connection has been kept alive
 796    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 797
 798    { // absolute redirect
 799        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/3", .{port});
 800        defer gpa.free(location);
 801        const uri = try std.Uri.parse(location);
 802
 803        log.info("{s}", .{location});
 804        var redirect_buffer: [1024]u8 = undefined;
 805        var req = try client.request(.GET, uri, .{});
 806        defer req.deinit();
 807
 808        try req.sendBodiless();
 809        var response = try req.receiveHead(&redirect_buffer);
 810
 811        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 812        defer gpa.free(body);
 813
 814        try expectEqualStrings("Hello, World!\n", body);
 815    }
 816
 817    // connection has been kept alive
 818    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 819
 820    { // too many redirects
 821        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/4", .{port});
 822        defer gpa.free(location);
 823        const uri = try std.Uri.parse(location);
 824
 825        log.info("{s}", .{location});
 826        var redirect_buffer: [1024]u8 = undefined;
 827        var req = try client.request(.GET, uri, .{});
 828        defer req.deinit();
 829
 830        try req.sendBodiless();
 831        if (req.receiveHead(&redirect_buffer)) |_| {
 832            return error.TestFailed;
 833        } else |err| switch (err) {
 834            error.TooManyHttpRedirects => {},
 835            else => return err,
 836        }
 837    }
 838
 839    { // redirect to encoded url
 840        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
 841        defer gpa.free(location);
 842        const uri = try std.Uri.parse(location);
 843
 844        log.info("{s}", .{location});
 845        var redirect_buffer: [1024]u8 = undefined;
 846        var req = try client.request(.GET, uri, .{});
 847        defer req.deinit();
 848
 849        try req.sendBodiless();
 850        var response = try req.receiveHead(&redirect_buffer);
 851
 852        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 853        defer gpa.free(body);
 854
 855        try expectEqualStrings("Encoded redirect successful!\n", body);
 856    }
 857
 858    // connection has been kept alive
 859    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 860
 861    { // check client without segfault by connection error after redirection
 862        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/invalid", .{port});
 863        defer gpa.free(location);
 864        const uri = try std.Uri.parse(location);
 865
 866        log.info("{s}", .{location});
 867        var redirect_buffer: [1024]u8 = undefined;
 868        var req = try client.request(.GET, uri, .{});
 869        defer req.deinit();
 870
 871        try req.sendBodiless();
 872        const result = req.receiveHead(&redirect_buffer);
 873
 874        // a proxy without an upstream is likely to return a 5xx status.
 875        if (client.http_proxy == null) {
 876            try expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
 877        }
 878    }
 879
 880    // connection has been kept alive
 881    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 882}
 883
 884test "Server streams both reading and writing" {
 885    const io = std.testing.io;
 886
 887    const test_server = try createTestServer(io, struct {
 888        fn run(test_server: *TestServer) anyerror!void {
 889            const net_server = &test_server.net_server;
 890            var recv_buffer: [1024]u8 = undefined;
 891            var send_buffer: [777]u8 = undefined;
 892
 893            var stream = try net_server.accept(io);
 894            defer stream.close(io);
 895
 896            var connection_br = stream.reader(io, &recv_buffer);
 897            var connection_bw = stream.writer(io, &send_buffer);
 898            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
 899            var request = try server.receiveHead();
 900            var read_buffer: [100]u8 = undefined;
 901            var br = try request.readerExpectContinue(&read_buffer);
 902            var response = try request.respondStreaming(&.{}, .{
 903                .respond_options = .{
 904                    .transfer_encoding = .none, // Causes keep_alive=false
 905                },
 906            });
 907            const w = &response.writer;
 908
 909            while (true) {
 910                try response.flush();
 911                const buf = br.peekGreedy(1) catch |err| switch (err) {
 912                    error.EndOfStream => break,
 913                    error.ReadFailed => return error.ReadFailed,
 914                };
 915                br.toss(buf.len);
 916                for (buf) |*b| b.* = std.ascii.toUpper(b.*);
 917                try w.writeAll(buf);
 918            }
 919            try response.end();
 920        }
 921    });
 922    defer test_server.destroy();
 923
 924    var client: http.Client = .{
 925        .allocator = std.testing.allocator,
 926        .io = io,
 927    };
 928    defer client.deinit();
 929
 930    var redirect_buffer: [555]u8 = undefined;
 931    var req = try client.request(.POST, .{
 932        .scheme = "http",
 933        .host = .{ .raw = "127.0.0.1" },
 934        .port = test_server.port(),
 935        .path = .{ .percent_encoded = "/" },
 936    }, .{});
 937    defer req.deinit();
 938
 939    req.transfer_encoding = .chunked;
 940    var body_writer = try req.sendBody(&.{});
 941    var response = try req.receiveHead(&redirect_buffer);
 942
 943    try body_writer.writer.writeAll("one ");
 944    try body_writer.writer.writeAll("fish");
 945    try body_writer.end();
 946
 947    const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .unlimited);
 948    defer std.testing.allocator.free(body);
 949
 950    try expectEqualStrings("ONE FISH", body);
 951}
 952
 953fn echoTests(client: *http.Client, port: u16) !void {
 954    const gpa = std.testing.allocator;
 955    var location_buffer: [100]u8 = undefined;
 956
 957    { // send content-length request
 958        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content", .{port});
 959        defer gpa.free(location);
 960        const uri = try std.Uri.parse(location);
 961
 962        var redirect_buffer: [1024]u8 = undefined;
 963        var req = try client.request(.POST, uri, .{
 964            .extra_headers = &.{
 965                .{ .name = "content-type", .value = "text/plain" },
 966            },
 967        });
 968        defer req.deinit();
 969
 970        req.transfer_encoding = .{ .content_length = 14 };
 971
 972        var body_writer = try req.sendBody(&.{});
 973        try body_writer.writer.writeAll("Hello, ");
 974        try body_writer.writer.writeAll("World!\n");
 975        try body_writer.end();
 976
 977        var response = try req.receiveHead(&redirect_buffer);
 978
 979        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
 980        defer gpa.free(body);
 981
 982        try expectEqualStrings("Hello, World!\n", body);
 983    }
 984
 985    // connection has been kept alive
 986    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
 987
 988    { // send chunked request
 989        const uri = try std.Uri.parse(try std.fmt.bufPrint(
 990            &location_buffer,
 991            "http://127.0.0.1:{d}/echo-content",
 992            .{port},
 993        ));
 994
 995        var redirect_buffer: [1024]u8 = undefined;
 996        var req = try client.request(.POST, uri, .{
 997            .extra_headers = &.{
 998                .{ .name = "content-type", .value = "text/plain" },
 999            },
1000        });
1001        defer req.deinit();
1002
1003        req.transfer_encoding = .chunked;
1004
1005        var body_writer = try req.sendBody(&.{});
1006        try body_writer.writer.writeAll("Hello, ");
1007        try body_writer.writer.writeAll("World!\n");
1008        try body_writer.end();
1009
1010        var response = try req.receiveHead(&redirect_buffer);
1011
1012        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
1013        defer gpa.free(body);
1014
1015        try expectEqualStrings("Hello, World!\n", body);
1016    }
1017
1018    // connection has been kept alive
1019    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
1020
1021    { // Client.fetch()
1022
1023        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
1024        defer gpa.free(location);
1025
1026        var body: std.Io.Writer.Allocating = .init(gpa);
1027        defer body.deinit();
1028        try body.ensureUnusedCapacity(64);
1029
1030        const res = try client.fetch(.{
1031            .location = .{ .url = location },
1032            .method = .POST,
1033            .payload = "Hello, World!\n",
1034            .extra_headers = &.{
1035                .{ .name = "content-type", .value = "text/plain" },
1036            },
1037            .response_writer = &body.writer,
1038        });
1039        try expectEqual(.ok, res.status);
1040        try expectEqualStrings("Hello, World!\n", body.written());
1041    }
1042
1043    { // expect: 100-continue
1044        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
1045        defer gpa.free(location);
1046        const uri = try std.Uri.parse(location);
1047
1048        var redirect_buffer: [1024]u8 = undefined;
1049        var req = try client.request(.POST, uri, .{
1050            .extra_headers = &.{
1051                .{ .name = "expect", .value = "100-continue" },
1052                .{ .name = "content-type", .value = "text/plain" },
1053            },
1054        });
1055        defer req.deinit();
1056
1057        req.transfer_encoding = .chunked;
1058
1059        var body_writer = try req.sendBody(&.{});
1060        try body_writer.writer.writeAll("Hello, ");
1061        try body_writer.writer.writeAll("World!\n");
1062        try body_writer.end();
1063
1064        var response = try req.receiveHead(&redirect_buffer);
1065        try expectEqual(.ok, response.head.status);
1066
1067        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
1068        defer gpa.free(body);
1069
1070        try expectEqualStrings("Hello, World!\n", body);
1071    }
1072
1073    { // expect: garbage
1074        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
1075        defer gpa.free(location);
1076        const uri = try std.Uri.parse(location);
1077
1078        var redirect_buffer: [1024]u8 = undefined;
1079        var req = try client.request(.POST, uri, .{
1080            .extra_headers = &.{
1081                .{ .name = "content-type", .value = "text/plain" },
1082                .{ .name = "expect", .value = "garbage" },
1083            },
1084        });
1085        defer req.deinit();
1086
1087        req.transfer_encoding = .chunked;
1088
1089        var body_writer = try req.sendBody(&.{});
1090        try body_writer.flush();
1091        var response = try req.receiveHead(&redirect_buffer);
1092        try expectEqual(.expectation_failed, response.head.status);
1093        _ = try response.reader(&.{}).discardRemaining();
1094    }
1095}
1096
1097const TestServer = struct {
1098    io: Io,
1099    shutting_down: bool,
1100    server_thread: std.Thread,
1101    net_server: net.Server,
1102
1103    fn destroy(self: *@This()) void {
1104        const io = self.io;
1105        self.shutting_down = true;
1106        var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch
1107            @panic("shutdown failure");
1108        stream.close(io);
1109
1110        self.server_thread.join();
1111        self.net_server.deinit(io);
1112        std.testing.allocator.destroy(self);
1113    }
1114
1115    fn port(self: @This()) u16 {
1116        return self.net_server.socket.address.getPort();
1117    }
1118};
1119
1120fn createTestServer(io: Io, S: type) !*TestServer {
1121    if (builtin.single_threaded) return error.SkipZigTest;
1122    if (builtin.zig_backend == .stage2_llvm and native_endian == .big) {
1123        // https://github.com/ziglang/zig/issues/13782
1124        return error.SkipZigTest;
1125    }
1126
1127    const address = try net.IpAddress.parse("127.0.0.1", 0);
1128    const test_server = try std.testing.allocator.create(TestServer);
1129    test_server.* = .{
1130        .io = io,
1131        .net_server = try address.listen(io, .{ .reuse_address = true }),
1132        .shutting_down = false,
1133        .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}),
1134    };
1135    return test_server;
1136}
1137
1138test "redirect to different connection" {
1139    const io = std.testing.io;
1140    const test_server_new = try createTestServer(io, struct {
1141        fn run(test_server: *TestServer) anyerror!void {
1142            const net_server = &test_server.net_server;
1143            var recv_buffer: [888]u8 = undefined;
1144            var send_buffer: [777]u8 = undefined;
1145
1146            var stream = try net_server.accept(io);
1147            defer stream.close(io);
1148
1149            var connection_br = stream.reader(io, &recv_buffer);
1150            var connection_bw = stream.writer(io, &send_buffer);
1151            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
1152            var request = try server.receiveHead();
1153            try expectEqualStrings(request.head.target, "/ok");
1154            try request.respond("good job, you pass", .{});
1155        }
1156    });
1157    defer test_server_new.destroy();
1158
1159    const global = struct {
1160        var other_port: ?u16 = null;
1161    };
1162    global.other_port = test_server_new.port();
1163
1164    const test_server_orig = try createTestServer(io, struct {
1165        fn run(test_server: *TestServer) anyerror!void {
1166            const net_server = &test_server.net_server;
1167            var recv_buffer: [999]u8 = undefined;
1168            var send_buffer: [100]u8 = undefined;
1169
1170            var stream = try net_server.accept(io);
1171            defer stream.close(io);
1172
1173            var loc_buf: [50]u8 = undefined;
1174            const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{
1175                global.other_port.?,
1176            });
1177
1178            var connection_br = stream.reader(io, &recv_buffer);
1179            var connection_bw = stream.writer(io, &send_buffer);
1180            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
1181            var request = try server.receiveHead();
1182            try expectEqualStrings(request.head.target, "/help");
1183            try request.respond("", .{
1184                .status = .found,
1185                .extra_headers = &.{
1186                    .{ .name = "location", .value = new_loc },
1187                },
1188            });
1189        }
1190    });
1191    defer test_server_orig.destroy();
1192
1193    const gpa = std.testing.allocator;
1194
1195    var client: http.Client = .{
1196        .allocator = gpa,
1197        .io = io,
1198    };
1199    defer client.deinit();
1200
1201    var loc_buf: [100]u8 = undefined;
1202    const location = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/help", .{
1203        test_server_orig.port(),
1204    });
1205    const uri = try std.Uri.parse(location);
1206
1207    {
1208        var redirect_buffer: [666]u8 = undefined;
1209        var req = try client.request(.GET, uri, .{});
1210        defer req.deinit();
1211
1212        try req.sendBodiless();
1213        var response = try req.receiveHead(&redirect_buffer);
1214        var reader = response.reader(&.{});
1215
1216        const body = try reader.allocRemaining(gpa, .unlimited);
1217        defer gpa.free(body);
1218
1219        try expectEqualStrings("good job, you pass", body);
1220    }
1221}