authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-05-03 14:34:10-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-05-06 21:35:16-05:00
log1b3ebfefd8d8fd05152d55c431f741880d1ce2a7
tree65e23769c1a247340b4a7f9ab163741018411a4e
parent5f219a2d118cac1410888fb2c0abc0cc91d092de
signaturelock-open Commit is signed but in an unrecognized format.

fix keepalive and large buffered writes


3 files changed, 129 insertions(+), 28 deletions(-)

lib/std/http/Client.zig+10-12
...@@ -71,7 +71,7 @@ pub const ConnectionPool = struct {...@@ -71,7 +71,7 @@ pub const ConnectionPool = struct {
71 while (next) |node| : (next = node.prev) {71 while (next) |node| : (next = node.prev) {
72 if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue;72 if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue;
73 if (node.data.port != criteria.port) continue;73 if (node.data.port != criteria.port) continue;
74 if (mem.eql(u8, node.data.host, criteria.host)) continue;74 if (!mem.eql(u8, node.data.host, criteria.host)) continue;
7575
76 pool.acquireUnsafe(node);76 pool.acquireUnsafe(node);
77 return node;77 return node;
...@@ -317,32 +317,29 @@ pub const BufferedConnection = struct {...@@ -317,32 +317,29 @@ pub const BufferedConnection = struct {
317 }317 }
318318
319 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {319 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {
320 if (bconn.write_buf.len - bconn.write_end <= buffer.len) {320 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
321 @memcpy(bconn.write_buf[bconn.write_end..], buffer);321 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
322 bconn.write_end += @intCast(u16, buffer.len);322 bconn.write_end += @intCast(u16, buffer.len);
323 } else {323 } else {
324 try bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);324 try bconn.flush();
325 bconn.write_end = 0;
326
327 try bconn.conn.writeAll(buffer);325 try bconn.conn.writeAll(buffer);
328 }326 }
329 }327 }
330328
331 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {329 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {
332 if (bconn.write_buf.len - bconn.write_end <= buffer.len) {330 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
333 @memcpy(bconn.write_buf[bconn.write_end..], buffer);331 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
334 bconn.write_end += @intCast(u16, buffer.len);332 bconn.write_end += @intCast(u16, buffer.len);
335333
336 return buffer.len;334 return buffer.len;
337 } else {335 } else {
338 try bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);336 try bconn.flush();
339 bconn.write_end = 0;
340
341 return try bconn.conn.write(buffer);337 return try bconn.conn.write(buffer);
342 }338 }
343 }339 }
344340
345 pub fn flush(bconn: *BufferedConnection) WriteError!void {341 pub fn flush(bconn: *BufferedConnection) WriteError!void {
342 defer bconn.write_end = 0;
346 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);343 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);
347 }344 }
348345
...@@ -720,12 +717,13 @@ pub const Request = struct {...@@ -720,12 +717,13 @@ pub const Request = struct {
720 req.response.parser.done = true;717 req.response.parser.done = true;
721 }718 }
722719
720 // we default to using keep-alive if not provided
723 const req_connection = req.headers.getFirstValue("connection");721 const req_connection = req.headers.getFirstValue("connection");
724 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);722 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
725723
726 const res_connection = req.response.headers.getFirstValue("connection");724 const res_connection = req.response.headers.getFirstValue("connection");
727 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);725 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
728 if (req_keepalive and res_keepalive) {726 if (res_keepalive and (req_keepalive or req_connection == null)) {
729 req.connection.data.closing = false;727 req.connection.data.closing = false;
730 } else {728 } else {
731 req.connection.data.closing = true;729 req.connection.data.closing = true;
lib/std/http/Server.zig+11-12
...@@ -161,32 +161,29 @@ pub const BufferedConnection = struct {...@@ -161,32 +161,29 @@ pub const BufferedConnection = struct {
161 }161 }
162162
163 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {163 pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void {
164 if (bconn.write_buf.len - bconn.write_end <= buffer.len) {164 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
165 @memcpy(bconn.write_buf[bconn.write_end..], buffer);165 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
166 bconn.write_end += @intCast(u16, buffer.len);166 bconn.write_end += @intCast(u16, buffer.len);
167 } else {167 } else {
168 try bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);168 try bconn.flush();
169 bconn.write_end = 0;
170
171 try bconn.conn.writeAll(buffer);169 try bconn.conn.writeAll(buffer);
172 }170 }
173 }171 }
174172
175 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {173 pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize {
176 if (bconn.write_buf.len - bconn.write_end <= buffer.len) {174 if (bconn.write_buf.len - bconn.write_end >= buffer.len) {
177 @memcpy(bconn.write_buf[bconn.write_end..], buffer);175 @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer);
178 bconn.write_end += @intCast(u16, buffer.len);176 bconn.write_end += @intCast(u16, buffer.len);
179177
180 return buffer.len;178 return buffer.len;
181 } else {179 } else {
182 try bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);180 try bconn.flush();
183 bconn.write_end = 0;
184
185 return try bconn.conn.write(buffer);181 return try bconn.conn.write(buffer);
186 }182 }
187 }183 }
188184
189 pub fn flush(bconn: *BufferedConnection) WriteError!void {185 pub fn flush(bconn: *BufferedConnection) WriteError!void {
186 defer bconn.write_end = 0;
190 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);187 return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]);
191 }188 }
192189
...@@ -397,12 +394,14 @@ pub const Response = struct {...@@ -397,12 +394,14 @@ pub const Response = struct {
397394
398 // A connection is only keep-alive if the Connection header is present and it's value is not "close".395 // A connection is only keep-alive if the Connection header is present and it's value is not "close".
399 // The server and client must both agree396 // The server and client must both agree
397 //
398 // do() defaults to using keep-alive if the client requests it.
400 const res_connection = res.headers.getFirstValue("connection");399 const res_connection = res.headers.getFirstValue("connection");
401 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);400 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
402401
403 const req_connection = res.request.headers.getFirstValue("connection");402 const req_connection = res.request.headers.getFirstValue("connection");
404 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);403 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
405 if (res_keepalive and req_keepalive) {404 if (req_keepalive and (res_keepalive or res_connection == null)) {
406 res.connection.conn.closing = false;405 res.connection.conn.closing = false;
407 } else {406 } else {
408 res.connection.conn.closing = true;407 res.connection.conn.closing = true;
...@@ -424,7 +423,7 @@ pub const Response = struct {...@@ -424,7 +423,7 @@ pub const Response = struct {
424423
425 res.headers.clearRetainingCapacity();424 res.headers.clearRetainingCapacity();
426425
427 res.request.headers.clearRetainingCapacity();426 res.request.headers.clearAndFree(); // FIXME: figure out why `clearRetainingCapacity` causes a leak in hash_map here
428 res.request.parser.reset();427 res.request.parser.reset();
429428
430 res.request = Request{429 res.request = Request{
test/standalone/http.zig+108-4
...@@ -9,8 +9,8 @@ const testing = std.testing;...@@ -9,8 +9,8 @@ const testing = std.testing;
99
10const max_header_size = 8192;10const max_header_size = 8192;
1111
12var gpa_server = std.heap.GeneralPurposeAllocator(.{}){};12var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
13var gpa_client = std.heap.GeneralPurposeAllocator(.{}){};13var gpa_client = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
1414
15const salloc = gpa_server.allocator();15const salloc = gpa_server.allocator();
16const calloc = gpa_client.allocator();16const calloc = gpa_client.allocator();
...@@ -44,6 +44,24 @@ fn handleRequest(res: *Server.Response) !void {...@@ -44,6 +44,24 @@ fn handleRequest(res: *Server.Response) !void {
44 try res.writeAll("World!\n");44 try res.writeAll("World!\n");
45 try res.finish();45 try res.finish();
46 }46 }
47 } else if (mem.startsWith(u8, res.request.target, "/large")) {
48 res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 };
49
50 try res.do();
51
52 var i: u32 = 0;
53 while (i < 5) : (i += 1) {
54 try res.writeAll("Hello, World!\n");
55 }
56
57 try res.writeAll("Hello, World!\n" ** 1024);
58
59 i = 0;
60 while (i < 5) : (i += 1) {
61 try res.writeAll("Hello, World!\n");
62 }
63
64 try res.finish();
47 } else if (mem.eql(u8, res.request.target, "/echo-content")) {65 } else if (mem.eql(u8, res.request.target, "/echo-content")) {
48 try testing.expectEqualStrings("Hello, World!\n", body);66 try testing.expectEqualStrings("Hello, World!\n", body);
49 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);67 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
...@@ -68,6 +86,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -68,6 +86,7 @@ fn handleRequest(res: *Server.Response) !void {
68 try res.writeAll("World!\n");86 try res.writeAll("World!\n");
69 // try res.finish();87 // try res.finish();
70 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");88 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");
89 try res.connection.flush();
71 } else if (mem.eql(u8, res.request.target, "/redirect/1")) {90 } else if (mem.eql(u8, res.request.target, "/redirect/1")) {
72 res.transfer_encoding = .chunked;91 res.transfer_encoding = .chunked;
7392
...@@ -177,8 +196,7 @@ pub fn main() !void {...@@ -177,8 +196,7 @@ pub fn main() !void {
177 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});196 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
178197
179 var client = Client{ .allocator = calloc };198 var client = Client{ .allocator = calloc };
180199 // defer client.deinit(); handled below
181 defer client.deinit();
182200
183 { // read content-length response201 { // read content-length response
184 var h = http.Headers{ .allocator = calloc };202 var h = http.Headers{ .allocator = calloc };
...@@ -202,6 +220,33 @@ pub fn main() !void {...@@ -202,6 +220,33 @@ pub fn main() !void {
202 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);220 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
203 }221 }
204222
223 // connection has been kept alive
224 try testing.expect(client.connection_pool.free_len == 1);
225
226 { // read large content-length response
227 var h = http.Headers{ .allocator = calloc };
228 defer h.deinit();
229
230 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port});
231 defer calloc.free(location);
232 const uri = try std.Uri.parse(location);
233
234 log.info("{s}", .{location});
235 var req = try client.request(.GET, uri, h, .{});
236 defer req.deinit();
237
238 try req.start();
239 try req.wait();
240
241 const body = try req.reader().readAllAlloc(calloc, 8192 * 1024);
242 defer calloc.free(body);
243
244 try testing.expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
245 }
246
247 // connection has been kept alive
248 try testing.expect(client.connection_pool.free_len == 1);
249
205 { // send head request and not read chunked250 { // send head request and not read chunked
206 var h = http.Headers{ .allocator = calloc };251 var h = http.Headers{ .allocator = calloc };
207 defer h.deinit();252 defer h.deinit();
...@@ -225,6 +270,9 @@ pub fn main() !void {...@@ -225,6 +270,9 @@ pub fn main() !void {
225 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);270 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);
226 }271 }
227272
273 // connection has been kept alive
274 try testing.expect(client.connection_pool.free_len == 1);
275
228 { // read chunked response276 { // read chunked response
229 var h = http.Headers{ .allocator = calloc };277 var h = http.Headers{ .allocator = calloc };
230 defer h.deinit();278 defer h.deinit();
...@@ -247,6 +295,9 @@ pub fn main() !void {...@@ -247,6 +295,9 @@ pub fn main() !void {
247 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);295 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
248 }296 }
249297
298 // connection has been kept alive
299 try testing.expect(client.connection_pool.free_len == 1);
300
250 { // send head request and not read chunked301 { // send head request and not read chunked
251 var h = http.Headers{ .allocator = calloc };302 var h = http.Headers{ .allocator = calloc };
252 defer h.deinit();303 defer h.deinit();
...@@ -270,6 +321,9 @@ pub fn main() !void {...@@ -270,6 +321,9 @@ pub fn main() !void {
270 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);321 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);
271 }322 }
272323
324 // connection has been kept alive
325 try testing.expect(client.connection_pool.free_len == 1);
326
273 { // check trailing headers327 { // check trailing headers
274 var h = http.Headers{ .allocator = calloc };328 var h = http.Headers{ .allocator = calloc };
275 defer h.deinit();329 defer h.deinit();
...@@ -292,6 +346,9 @@ pub fn main() !void {...@@ -292,6 +346,9 @@ pub fn main() !void {
292 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);346 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
293 }347 }
294348
349 // connection has been kept alive
350 try testing.expect(client.connection_pool.free_len == 1);
351
295 { // send content-length request352 { // send content-length request
296 var h = http.Headers{ .allocator = calloc };353 var h = http.Headers{ .allocator = calloc };
297 defer h.deinit();354 defer h.deinit();
...@@ -321,6 +378,36 @@ pub fn main() !void {...@@ -321,6 +378,36 @@ pub fn main() !void {
321 try testing.expectEqualStrings("Hello, World!\n", body);378 try testing.expectEqualStrings("Hello, World!\n", body);
322 }379 }
323380
381 // connection has been kept alive
382 try testing.expect(client.connection_pool.free_len == 1);
383
384 { // read content-length response with connection close
385 var h = http.Headers{ .allocator = calloc };
386 defer h.deinit();
387
388 try h.append("connection", "close");
389
390 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
391 defer calloc.free(location);
392 const uri = try std.Uri.parse(location);
393
394 log.info("{s}", .{location});
395 var req = try client.request(.GET, uri, h, .{});
396 defer req.deinit();
397
398 try req.start();
399 try req.wait();
400
401 const body = try req.reader().readAllAlloc(calloc, 8192);
402 defer calloc.free(body);
403
404 try testing.expectEqualStrings("Hello, World!\n", body);
405 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
406 }
407
408 // connection has been closed
409 try testing.expect(client.connection_pool.free_len == 0);
410
324 { // send chunked request411 { // send chunked request
325 var h = http.Headers{ .allocator = calloc };412 var h = http.Headers{ .allocator = calloc };
326 defer h.deinit();413 defer h.deinit();
...@@ -350,6 +437,9 @@ pub fn main() !void {...@@ -350,6 +437,9 @@ pub fn main() !void {
350 try testing.expectEqualStrings("Hello, World!\n", body);437 try testing.expectEqualStrings("Hello, World!\n", body);
351 }438 }
352439
440 // connection has been kept alive
441 try testing.expect(client.connection_pool.free_len == 1);
442
353 { // relative redirect443 { // relative redirect
354 var h = http.Headers{ .allocator = calloc };444 var h = http.Headers{ .allocator = calloc };
355 defer h.deinit();445 defer h.deinit();
...@@ -371,6 +461,9 @@ pub fn main() !void {...@@ -371,6 +461,9 @@ pub fn main() !void {
371 try testing.expectEqualStrings("Hello, World!\n", body);461 try testing.expectEqualStrings("Hello, World!\n", body);
372 }462 }
373463
464 // connection has been kept alive
465 try testing.expect(client.connection_pool.free_len == 1);
466
374 { // redirect from root467 { // redirect from root
375 var h = http.Headers{ .allocator = calloc };468 var h = http.Headers{ .allocator = calloc };
376 defer h.deinit();469 defer h.deinit();
...@@ -392,6 +485,9 @@ pub fn main() !void {...@@ -392,6 +485,9 @@ pub fn main() !void {
392 try testing.expectEqualStrings("Hello, World!\n", body);485 try testing.expectEqualStrings("Hello, World!\n", body);
393 }486 }
394487
488 // connection has been kept alive
489 try testing.expect(client.connection_pool.free_len == 1);
490
395 { // absolute redirect491 { // absolute redirect
396 var h = http.Headers{ .allocator = calloc };492 var h = http.Headers{ .allocator = calloc };
397 defer h.deinit();493 defer h.deinit();
...@@ -413,6 +509,9 @@ pub fn main() !void {...@@ -413,6 +509,9 @@ pub fn main() !void {
413 try testing.expectEqualStrings("Hello, World!\n", body);509 try testing.expectEqualStrings("Hello, World!\n", body);
414 }510 }
415511
512 // connection has been kept alive
513 try testing.expect(client.connection_pool.free_len == 1);
514
416 { // too many redirects515 { // too many redirects
417 var h = http.Headers{ .allocator = calloc };516 var h = http.Headers{ .allocator = calloc };
418 defer h.deinit();517 defer h.deinit();
...@@ -432,6 +531,11 @@ pub fn main() !void {...@@ -432,6 +531,11 @@ pub fn main() !void {
432 };531 };
433 }532 }
434533
534 // connection has been kept alive
535 try testing.expect(client.connection_pool.free_len == 1);
536
537 client.deinit();
538
435 killServer(server.socket.listen_address);539 killServer(server.socket.listen_address);
436 server_thread.join();540 server_thread.join();
437}541}