# A Tiny HTTP Client

### A Tiny HTTP Client

An HTTP client opens a network connection, sends a request, receives a response, and writes the response body.

We will build a small program named `get`.

```text
get example.com /
```

It will send this HTTP request:

```text
GET / HTTP/1.1
Host: example.com
Connection: close
```

Here is the first version.

```zig
const std = @import("std");

pub fn main() !void {
    var args = std.process.args();

    _ = args.next();

    const host = args.next() orelse {
        std.debug.print("missing host\n", .{});
        return;
    };

    const path = args.next() orelse "/";

    const address_list = try std.net.getAddressList(
        std.heap.page_allocator,
        host,
        80,
    );
    defer address_list.deinit();

    const address = address_list.addrs[0];

    const stream = try std.net.tcpConnectToAddress(address);
    defer stream.close();

    var writer = stream.writer();
    var reader = stream.reader();

    try writer.print(
        "GET {s} HTTP/1.1\r\n" ++
            "Host: {s}\r\n" ++
            "Connection: close\r\n" ++
            "\r\n",
        .{ path, host },
    );

    var out = std.io.getStdOut().writer();

    var buffer: [4096]u8 = undefined;

    while (true) {
        const n = try reader.read(&buffer);
        if (n == 0) break;

        try out.writeAll(buffer[0..n]);
    }
}
```

Run it:

```sh
zig run main.zig -- example.com /
```

The program prints the whole HTTP response: status line, headers, blank line, and body.

The first new operation is name lookup.

```zig
const address_list = try std.net.getAddressList(
    std.heap.page_allocator,
    host,
    80,
);
defer address_list.deinit();
```

A host name can resolve to more than one network address. `getAddressList` returns a list. The list owns memory, so it must be freed with `deinit`.

This program chooses the first address.

```zig
const address = address_list.addrs[0];
```

A stronger program would try each address until one connection succeeds.

The connection is opened with:

```zig
const stream = try std.net.tcpConnectToAddress(address);
defer stream.close();
```

A TCP stream is a byte stream. It has no concept of HTTP requests or responses. HTTP is just text and bytes sent over the stream.

The request is written with `print`.

```zig
try writer.print(
    "GET {s} HTTP/1.1\r\n" ++
        "Host: {s}\r\n" ++
        "Connection: close\r\n" ++
        "\r\n",
    .{ path, host },
);
```

HTTP uses `\r\n` at the end of each header line. The empty line ends the header section.

Then the program copies bytes from the network stream to standard output.

```zig
while (true) {
    const n = try reader.read(&buffer);
    if (n == 0) break;

    try out.writeAll(buffer[0..n]);
}
```

This is the same shape as the file copier. The source is now a socket instead of a file. The destination is standard output.

Network programs should be strict about ownership. In this example:

| Value | Resource | Cleanup |
|---|---|---|
| `address_list` | allocated address list | `deinit` |
| `stream` | TCP connection | `close` |
| `buffer` | stack memory | none |

This client only speaks plain HTTP on port 80. It does not handle HTTPS. It does not parse headers. It does not follow redirects. It does not decompress content. Those are separate layers.

A small improvement is to put request writing in a function.

```zig
fn sendRequest(writer: anytype, host: []const u8, path: []const u8) !void {
    try writer.print(
        "GET {s} HTTP/1.1\r\n" ++
            "Host: {s}\r\n" ++
            "Connection: close\r\n" ++
            "\r\n",
        .{ path, host },
    );
}
```

Now `main` can show the main sequence more clearly.

```zig
const std = @import("std");

fn sendRequest(writer: anytype, host: []const u8, path: []const u8) !void {
    try writer.print(
        "GET {s} HTTP/1.1\r\n" ++
            "Host: {s}\r\n" ++
            "Connection: close\r\n" ++
            "\r\n",
        .{ path, host },
    );
}

pub fn main() !void {
    var args = std.process.args();

    _ = args.next();

    const host = args.next() orelse return error.MissingHost;
    const path = args.next() orelse "/";

    const address_list = try std.net.getAddressList(
        std.heap.page_allocator,
        host,
        80,
    );
    defer address_list.deinit();

    const stream = try std.net.tcpConnectToAddress(address_list.addrs[0]);
    defer stream.close();

    var writer = stream.writer();
    var reader = stream.reader();
    var out = std.io.getStdOut().writer();

    try sendRequest(writer, host, path);

    var buffer: [4096]u8 = undefined;

    while (true) {
        const n = try reader.read(&buffer);
        if (n == 0) break;

        try out.writeAll(buffer[0..n]);
    }
}
```

The type of `writer` is written as `anytype` in `sendRequest`.

```zig
fn sendRequest(writer: anytype, host: []const u8, path: []const u8) !void
```

This makes the function generic. Any value that has a compatible `print` method can be passed. The compiler checks the use at compile time.

This is common Zig style. Do not build an interface first. Write the function against the operations it needs.

Exercise 20-16. Print only the response headers.

Exercise 20-17. Print only the response body.

Exercise 20-18. Try every address returned by `getAddressList`.

Exercise 20-19. Add a `--port` option.

Exercise 20-20. Reject paths that do not begin with `/`.

