Commit a07218cc43

Techatrix <19954306+Techatrix@users.noreply.github.com>
2024-02-25 12:01:21
http: handle header fields with empty value
1 parent 9727931
lib/std/http/Client.zig
@@ -488,7 +488,7 @@ pub const Response = struct {
             var line_it = mem.splitSequence(u8, line, ": ");
             const header_name = line_it.next().?;
             const header_value = line_it.rest();
-            if (header_value.len == 0) return error.HttpHeadersInvalid;
+            if (header_name.len == 0) return error.HttpHeadersInvalid;
 
             if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
                 res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
@@ -774,7 +774,7 @@ pub const Request = struct {
         }
 
         for (req.extra_headers) |header| {
-            assert(header.value.len != 0);
+            assert(header.name.len != 0);
 
             try w.writeAll(header.name);
             try w.writeAll(": ");
@@ -1515,11 +1515,13 @@ pub fn open(
 ) RequestError!Request {
     if (std.debug.runtime_safety) {
         for (options.extra_headers) |header| {
+            assert(header.name.len != 0);
             assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
             assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
             assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
         }
         for (options.privileged_headers) |header| {
+            assert(header.name.len != 0);
             assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
             assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
         }
lib/std/http/HeaderIterator.zig
@@ -15,7 +15,7 @@ pub fn next(it: *HeaderIterator) ?std.http.Header {
     var kv_it = std.mem.splitSequence(u8, it.bytes[it.index..end], ": ");
     const name = kv_it.next().?;
     const value = kv_it.rest();
-    if (value.len == 0) {
+    if (name.len == 0 and value.len == 0) {
         if (it.is_trailer) return null;
         const next_end = std.mem.indexOfPosLinear(u8, it.bytes, end + 2, "\r\n") orelse
             return null;
@@ -35,7 +35,7 @@ pub fn next(it: *HeaderIterator) ?std.http.Header {
 }
 
 test next {
-    var it = HeaderIterator.init("200 OK\r\na: b\r\nc: d\r\n\r\ne: f\r\n\r\n");
+    var it = HeaderIterator.init("200 OK\r\na: b\r\nc: \r\nd: e\r\n\r\nf: g\r\n\r\n");
     try std.testing.expect(!it.is_trailer);
     {
         const header = it.next().?;
@@ -47,13 +47,19 @@ test next {
         const header = it.next().?;
         try std.testing.expect(!it.is_trailer);
         try std.testing.expectEqualStrings("c", header.name);
-        try std.testing.expectEqualStrings("d", header.value);
+        try std.testing.expectEqualStrings("", header.value);
+    }
+    {
+        const header = it.next().?;
+        try std.testing.expect(!it.is_trailer);
+        try std.testing.expectEqualStrings("d", header.name);
+        try std.testing.expectEqualStrings("e", header.value);
     }
     {
         const header = it.next().?;
         try std.testing.expect(it.is_trailer);
-        try std.testing.expectEqualStrings("e", header.name);
-        try std.testing.expectEqualStrings("f", header.value);
+        try std.testing.expectEqualStrings("f", header.name);
+        try std.testing.expectEqualStrings("g", header.value);
     }
     try std.testing.expectEqual(null, it.next());
 }
lib/std/http/Server.zig
@@ -211,7 +211,7 @@ pub const Request = struct {
                 var line_it = mem.splitSequence(u8, line, ": ");
                 const header_name = line_it.next().?;
                 const header_value = line_it.rest();
-                if (header_value.len == 0) return error.HttpHeadersInvalid;
+                if (header_name.len == 0) return error.HttpHeadersInvalid;
 
                 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
                     head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
@@ -311,6 +311,7 @@ pub const Request = struct {
         assert(options.extra_headers.len <= max_extra_headers);
         if (std.debug.runtime_safety) {
             for (options.extra_headers) |header| {
+                assert(header.name.len != 0);
                 assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
                 assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
                 assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
@@ -370,11 +371,13 @@ pub const Request = struct {
             };
             iovecs_len += 1;
 
-            iovecs[iovecs_len] = .{
-                .iov_base = header.value.ptr,
-                .iov_len = header.value.len,
-            };
-            iovecs_len += 1;
+            if (header.value.len != 0) {
+                iovecs[iovecs_len] = .{
+                    .iov_base = header.value.ptr,
+                    .iov_len = header.value.len,
+                };
+                iovecs_len += 1;
+            }
 
             iovecs[iovecs_len] = .{
                 .iov_base = "\r\n",
@@ -496,6 +499,7 @@ pub const Request = struct {
             }
 
             for (o.extra_headers) |header| {
+                assert(header.name.len != 0);
                 h.appendSliceAssumeCapacity(header.name);
                 h.appendSliceAssumeCapacity(": ");
                 h.appendSliceAssumeCapacity(header.value);
@@ -986,11 +990,13 @@ pub const Response = struct {
                 };
                 iovecs_len += 1;
 
-                iovecs[iovecs_len] = .{
-                    .iov_base = trailer.value.ptr,
-                    .iov_len = trailer.value.len,
-                };
-                iovecs_len += 1;
+                if (trailer.value.len != 0) {
+                    iovecs[iovecs_len] = .{
+                        .iov_base = trailer.value.ptr,
+                        .iov_len = trailer.value.len,
+                    };
+                    iovecs_len += 1;
+                }
 
                 iovecs[iovecs_len] = .{
                     .iov_base = "\r\n",
lib/std/http/test.zig
@@ -479,6 +479,12 @@ test "general client/server API coverage" {
                         .{ .name = "location", .value = location },
                     },
                 });
+            } else if (mem.eql(u8, request.head.target, "/empty")) {
+                try request.respond("", .{
+                    .extra_headers = &.{
+                        .{ .name = "empty", .value = "" },
+                    },
+                });
             } else {
                 try request.respond("", .{ .status = .not_found });
             }
@@ -491,7 +497,10 @@ test "general client/server API coverage" {
             return s.listen_address.in.getPort();
         }
     });
-    defer test_server.destroy();
+    defer {
+        global.handle_new_requests = false;
+        test_server.destroy();
+    }
 
     const log = std.log.scoped(.client);
 
@@ -654,6 +663,56 @@ test "general client/server API coverage" {
     // connection has been closed
     try expect(client.connection_pool.free_len == 0);
 
+    { // handle empty header field value
+        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/empty", .{port});
+        defer gpa.free(location);
+        const uri = try std.Uri.parse(location);
+
+        log.info("{s}", .{location});
+        var server_header_buffer: [1024]u8 = undefined;
+        var req = try client.open(.GET, uri, .{
+            .server_header_buffer = &server_header_buffer,
+            .extra_headers = &.{
+                .{ .name = "empty", .value = "" },
+            },
+        });
+        defer req.deinit();
+
+        try req.send(.{});
+        try req.wait();
+
+        try std.testing.expectEqual(.ok, req.response.status);
+
+        const body = try req.reader().readAllAlloc(gpa, 8192);
+        defer gpa.free(body);
+
+        try expectEqualStrings("", body);
+
+        var it = req.response.iterateHeaders();
+        {
+            const header = it.next().?;
+            try expect(!it.is_trailer);
+            try expectEqualStrings("connection", header.name);
+            try expectEqualStrings("keep-alive", header.value);
+        }
+        {
+            const header = it.next().?;
+            try expect(!it.is_trailer);
+            try expectEqualStrings("content-length", header.name);
+            try expectEqualStrings("0", header.value);
+        }
+        {
+            const header = it.next().?;
+            try expect(!it.is_trailer);
+            try expectEqualStrings("empty", header.name);
+            try expectEqualStrings("", header.value);
+        }
+        try expectEqual(null, it.next());
+    }
+
+    // connection has been kept alive
+    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
+
     { // relative redirect
         const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/1", .{port});
         defer gpa.free(location);