| author | |
| committer | |
| log | 3bb3d39fb4158ba4b811bcae7e7a897febf07e17 |
| tree | 51d408085e1e9c60003c02960e509e3498807ab5 |
| parent | 5569e6b49d9b421d35e3175df36eb9fe7e4e8084 |
| parent | 9017d758b96c1f296249670dffb774a598bc8598 |
| signature |
std.http: more http fixes, add standalone http server test8 files changed, 771 insertions(+), 183 deletions(-)
lib/std/Uri.zig+6-3| ... | @@ -216,6 +216,7 @@ pub fn format( | ... | @@ -216,6 +216,7 @@ pub fn format( |
| 216 | 216 | ||
| 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; | ||
| 219 | 220 | ||
| 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 | } |
| 255 | 256 | ||
| 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+53-27| ... | @@ -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; |
| 75 | 75 | ||
| 76 | pool.acquireUnsafe(node); | 76 | pool.acquireUnsafe(node); |
| 77 | return node; | 77 | return node; |
| ... | @@ -251,47 +251,50 @@ pub const Connection = struct { | ... | @@ -251,47 +251,50 @@ pub const Connection = struct { |
| 251 | 251 | ||
| 252 | /// A buffered (and peekable) Connection. | 252 | /// A buffered (and peekable) Connection. |
| 253 | pub const BufferedConnection = struct { | 253 | pub const BufferedConnection = struct { |
| 254 | pub const buffer_size = 0x2000; | 254 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; |
| 255 | 255 | ||
| 256 | conn: Connection, | 256 | conn: Connection, |
| 257 | buf: [buffer_size]u8 = undefined, | 257 | read_buf: [buffer_size]u8 = undefined, |
| 258 | start: u16 = 0, | 258 | read_start: u16 = 0, |
| 259 | end: u16 = 0, | 259 | read_end: u16 = 0, |
| 260 | |||
| 261 | write_buf: [buffer_size]u8 = undefined, | ||
| 262 | write_end: u16 = 0, | ||
| 260 | 263 | ||
| 261 | pub fn fill(bconn: *BufferedConnection) ReadError!void { | 264 | pub fn fill(bconn: *BufferedConnection) ReadError!void { |
| 262 | if (bconn.end != bconn.start) return; | 265 | if (bconn.read_end != bconn.read_start) return; |
| 263 | 266 | ||
| 264 | const nread = try bconn.conn.read(bconn.buf[0..]); | 267 | const nread = try bconn.conn.read(bconn.read_buf[0..]); |
| 265 | if (nread == 0) return error.EndOfStream; | 268 | if (nread == 0) return error.EndOfStream; |
| 266 | bconn.start = 0; | 269 | bconn.read_start = 0; |
| 267 | bconn.end = @truncate(u16, nread); | 270 | bconn.read_end = @intCast(u16, nread); |
| 268 | } | 271 | } |
| 269 | 272 | ||
| 270 | pub fn peek(bconn: *BufferedConnection) []const u8 { | 273 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| 271 | return bconn.buf[bconn.start..bconn.end]; | 274 | return bconn.read_buf[bconn.read_start..bconn.read_end]; |
| 272 | } | 275 | } |
| 273 | 276 | ||
| 274 | pub fn clear(bconn: *BufferedConnection, num: u16) void { | 277 | pub fn clear(bconn: *BufferedConnection, num: u16) void { |
| 275 | bconn.start += num; | 278 | bconn.read_start += num; |
| 276 | } | 279 | } |
| 277 | 280 | ||
| 278 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { | 281 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { |
| 279 | var out_index: u16 = 0; | 282 | var out_index: u16 = 0; |
| 280 | while (out_index < len) { | 283 | while (out_index < len) { |
| 281 | const available = bconn.end - bconn.start; | 284 | const available = bconn.read_end - bconn.read_start; |
| 282 | const left = buffer.len - out_index; | 285 | const left = buffer.len - out_index; |
| 283 | 286 | ||
| 284 | if (available > 0) { | 287 | if (available > 0) { |
| 285 | const can_read = @truncate(u16, @min(available, left)); | 288 | const can_read = @intCast(u16, @min(available, left)); |
| 286 | 289 | ||
| 287 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); | 290 | @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]); |
| 288 | out_index += can_read; | 291 | out_index += can_read; |
| 289 | bconn.start += can_read; | 292 | bconn.read_start += can_read; |
| 290 | 293 | ||
| 291 | continue; | 294 | continue; |
| 292 | } | 295 | } |
| 293 | 296 | ||
| 294 | if (left > bconn.buf.len) { | 297 | if (left > bconn.read_buf.len) { |
| 295 | // skip the buffer if the output is large enough | 298 | // skip the buffer if the output is large enough |
| 296 | return bconn.conn.read(buffer[out_index..]); | 299 | return bconn.conn.read(buffer[out_index..]); |
| 297 | } | 300 | } |
| ... | @@ -314,11 +317,30 @@ pub const BufferedConnection = struct { | ... | @@ -314,11 +317,30 @@ pub const BufferedConnection = struct { |
| 314 | } | 317 | } |
| 315 | 318 | ||
| 316 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { | 319 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { |
| 317 | return bconn.conn.writeAll(buffer); | 320 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { |
| 321 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | ||
| 322 | bconn.write_end += @intCast(u16, buffer.len); | ||
| 323 | } else { | ||
| 324 | try bconn.flush(); | ||
| 325 | try bconn.conn.writeAll(buffer); | ||
| 326 | } | ||
| 318 | } | 327 | } |
| 319 | 328 | ||
| 320 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { | 329 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { |
| 321 | return bconn.conn.write(buffer); | 330 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { |
| 331 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | ||
| 332 | bconn.write_end += @intCast(u16, buffer.len); | ||
| 333 | |||
| 334 | return buffer.len; | ||
| 335 | } else { | ||
| 336 | try bconn.flush(); | ||
| 337 | return try bconn.conn.write(buffer); | ||
| 338 | } | ||
| 339 | } | ||
| 340 | |||
| 341 | pub fn flush(bconn: *BufferedConnection) WriteError!void { | ||
| 342 | defer bconn.write_end = 0; | ||
| 343 | return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]); | ||
| 322 | } | 344 | } |
| 323 | 345 | ||
| 324 | pub const WriteError = Connection.WriteError; | 346 | pub const WriteError = Connection.WriteError; |
| ... | @@ -355,8 +377,6 @@ pub const Compression = union(enum) { | ... | @@ -355,8 +377,6 @@ pub const Compression = union(enum) { |
| 355 | /// A HTTP response originating from a server. | 377 | /// A HTTP response originating from a server. |
| 356 | pub const Response = struct { | 378 | pub const Response = struct { |
| 357 | pub const ParseError = Allocator.Error || error{ | 379 | pub const ParseError = Allocator.Error || error{ |
| 358 | ShortHttpStatusLine, | ||
| 359 | BadHttpVersion, | ||
| 360 | HttpHeadersInvalid, | 380 | HttpHeadersInvalid, |
| 361 | HttpHeaderContinuationsUnsupported, | 381 | HttpHeaderContinuationsUnsupported, |
| 362 | HttpTransferEncodingUnsupported, | 382 | HttpTransferEncodingUnsupported, |
| ... | @@ -370,12 +390,12 @@ pub const Response = struct { | ... | @@ -370,12 +390,12 @@ pub const Response = struct { |
| 370 | 390 | ||
| 371 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | 391 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 372 | if (first_line.len < 12) | 392 | if (first_line.len < 12) |
| 373 | return error.ShortHttpStatusLine; | 393 | return error.HttpHeadersInvalid; |
| 374 | 394 | ||
| 375 | const version: http.Version = switch (int64(first_line[0..8])) { | 395 | const version: http.Version = switch (int64(first_line[0..8])) { |
| 376 | int64("HTTP/1.0") => .@"HTTP/1.0", | 396 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 377 | int64("HTTP/1.1") => .@"HTTP/1.1", | 397 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 378 | else => return error.BadHttpVersion, | 398 | else => return error.HttpHeadersInvalid, |
| 379 | }; | 399 | }; |
| 380 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | 400 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; |
| 381 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | 401 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); |
| ... | @@ -569,8 +589,7 @@ pub const Request = struct { | ... | @@ -569,8 +589,7 @@ pub const Request = struct { |
| 569 | 589 | ||
| 570 | /// Send the request to the server. | 590 | /// Send the request to the server. |
| 571 | pub fn start(req: *Request) StartError!void { | 591 | pub fn start(req: *Request) StartError!void { |
| 572 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); | 592 | const w = req.connection.data.buffered.writer(); |
| 573 | const w = buffered.writer(); | ||
| 574 | 593 | ||
| 575 | try w.writeAll(@tagName(req.method)); | 594 | try w.writeAll(@tagName(req.method)); |
| 576 | try w.writeByte(' '); | 595 | try w.writeByte(' '); |
| ... | @@ -644,7 +663,7 @@ pub const Request = struct { | ... | @@ -644,7 +663,7 @@ pub const Request = struct { |
| 644 | 663 | ||
| 645 | try w.writeAll("\r\n"); | 664 | try w.writeAll("\r\n"); |
| 646 | 665 | ||
| 647 | try buffered.flush(); | 666 | try req.connection.data.buffered.flush(); |
| 648 | } | 667 | } |
| 649 | 668 | ||
| 650 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; | 669 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; |
| ... | @@ -695,16 +714,16 @@ pub const Request = struct { | ... | @@ -695,16 +714,16 @@ pub const Request = struct { |
| 695 | 714 | ||
| 696 | if (req.method == .CONNECT and req.response.status == .ok) { | 715 | if (req.method == .CONNECT and req.response.status == .ok) { |
| 697 | req.connection.data.closing = false; | 716 | req.connection.data.closing = false; |
| 698 | req.connection.data.proxied = true; | ||
| 699 | req.response.parser.done = true; | 717 | req.response.parser.done = true; |
| 700 | } | 718 | } |
| 701 | 719 | ||
| 720 | // we default to using keep-alive if not provided | ||
| 702 | const req_connection = req.headers.getFirstValue("connection"); | 721 | const req_connection = req.headers.getFirstValue("connection"); |
| 703 | 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.?); |
| 704 | 723 | ||
| 705 | const res_connection = req.response.headers.getFirstValue("connection"); | 724 | const res_connection = req.response.headers.getFirstValue("connection"); |
| 706 | 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.?); |
| 707 | if (req_keepalive and res_keepalive) { | 726 | if (res_keepalive and (req_keepalive or req_connection == null)) { |
| 708 | req.connection.data.closing = false; | 727 | req.connection.data.closing = false; |
| 709 | } else { | 728 | } else { |
| 710 | req.connection.data.closing = true; | 729 | req.connection.data.closing = true; |
| ... | @@ -725,6 +744,11 @@ pub const Request = struct { | ... | @@ -725,6 +744,11 @@ pub const Request = struct { |
| 725 | req.response.parser.done = true; | 744 | req.response.parser.done = true; |
| 726 | } | 745 | } |
| 727 | 746 | ||
| 747 | // HEAD requests have no body | ||
| 748 | if (req.method == .HEAD) { | ||
| 749 | req.response.parser.done = true; | ||
| 750 | } | ||
| 751 | |||
| 728 | if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) { | 752 | if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) { |
| 729 | req.response.skip = true; | 753 | req.response.skip = true; |
| 730 | 754 | ||
| ... | @@ -866,6 +890,8 @@ pub const Request = struct { | ... | @@ -866,6 +890,8 @@ pub const Request = struct { |
| 866 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | 890 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 867 | .none => {}, | 891 | .none => {}, |
| 868 | } | 892 | } |
| 893 | |||
| 894 | try req.connection.data.buffered.flush(); | ||
| 869 | } | 895 | } |
| 870 | }; | 896 | }; |
| 871 | 897 |
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, |
| 38 | 38 | ||
| 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+167-69| ... | @@ -95,47 +95,50 @@ pub const Connection = struct { | ... | @@ -95,47 +95,50 @@ pub const Connection = struct { |
| 95 | 95 | ||
| 96 | /// A buffered (and peekable) Connection. | 96 | /// A buffered (and peekable) Connection. |
| 97 | pub const BufferedConnection = struct { | 97 | pub const BufferedConnection = struct { |
| 98 | pub const buffer_size = 0x2000; | 98 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; |
| 99 | 99 | ||
| 100 | conn: Connection, | 100 | conn: Connection, |
| 101 | buf: [buffer_size]u8 = undefined, | 101 | read_buf: [buffer_size]u8 = undefined, |
| 102 | start: u16 = 0, | 102 | read_start: u16 = 0, |
| 103 | end: u16 = 0, | 103 | read_end: u16 = 0, |
| 104 | |||
| 105 | write_buf: [buffer_size]u8 = undefined, | ||
| 106 | write_end: u16 = 0, | ||
| 104 | 107 | ||
| 105 | pub fn fill(bconn: *BufferedConnection) ReadError!void { | 108 | pub fn fill(bconn: *BufferedConnection) ReadError!void { |
| 106 | if (bconn.end != bconn.start) return; | 109 | if (bconn.read_end != bconn.read_start) return; |
| 107 | 110 | ||
| 108 | const nread = try bconn.conn.read(bconn.buf[0..]); | 111 | const nread = try bconn.conn.read(bconn.read_buf[0..]); |
| 109 | if (nread == 0) return error.EndOfStream; | 112 | if (nread == 0) return error.EndOfStream; |
| 110 | bconn.start = 0; | 113 | bconn.read_start = 0; |
| 111 | bconn.end = @truncate(u16, nread); | 114 | bconn.read_end = @intCast(u16, nread); |
| 112 | } | 115 | } |
| 113 | 116 | ||
| 114 | pub fn peek(bconn: *BufferedConnection) []const u8 { | 117 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| 115 | return bconn.buf[bconn.start..bconn.end]; | 118 | return bconn.read_buf[bconn.read_start..bconn.read_end]; |
| 116 | } | 119 | } |
| 117 | 120 | ||
| 118 | pub fn clear(bconn: *BufferedConnection, num: u16) void { | 121 | pub fn clear(bconn: *BufferedConnection, num: u16) void { |
| 119 | bconn.start += num; | 122 | bconn.read_start += num; |
| 120 | } | 123 | } |
| 121 | 124 | ||
| 122 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { | 125 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { |
| 123 | var out_index: u16 = 0; | 126 | var out_index: u16 = 0; |
| 124 | while (out_index < len) { | 127 | while (out_index < len) { |
| 125 | const available = bconn.end - bconn.start; | 128 | const available = bconn.read_end - bconn.read_start; |
| 126 | const left = buffer.len - out_index; | 129 | const left = buffer.len - out_index; |
| 127 | 130 | ||
| 128 | if (available > 0) { | 131 | if (available > 0) { |
| 129 | const can_read = @truncate(u16, @min(available, left)); | 132 | const can_read = @intCast(u16, @min(available, left)); |
| 130 | 133 | ||
| 131 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); | 134 | @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]); |
| 132 | out_index += can_read; | 135 | out_index += can_read; |
| 133 | bconn.start += can_read; | 136 | bconn.read_start += can_read; |
| 134 | 137 | ||
| 135 | continue; | 138 | continue; |
| 136 | } | 139 | } |
| 137 | 140 | ||
| 138 | if (left > bconn.buf.len) { | 141 | if (left > bconn.read_buf.len) { |
| 139 | // skip the buffer if the output is large enough | 142 | // skip the buffer if the output is large enough |
| 140 | return bconn.conn.read(buffer[out_index..]); | 143 | return bconn.conn.read(buffer[out_index..]); |
| 141 | } | 144 | } |
| ... | @@ -158,11 +161,30 @@ pub const BufferedConnection = struct { | ... | @@ -158,11 +161,30 @@ pub const BufferedConnection = struct { |
| 158 | } | 161 | } |
| 159 | 162 | ||
| 160 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { | 163 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { |
| 161 | return bconn.conn.writeAll(buffer); | 164 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { |
| 165 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | ||
| 166 | bconn.write_end += @intCast(u16, buffer.len); | ||
| 167 | } else { | ||
| 168 | try bconn.flush(); | ||
| 169 | try bconn.conn.writeAll(buffer); | ||
| 170 | } | ||
| 162 | } | 171 | } |
| 163 | 172 | ||
| 164 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { | 173 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { |
| 165 | return bconn.conn.write(buffer); | 174 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { |
| 175 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | ||
| 176 | bconn.write_end += @intCast(u16, buffer.len); | ||
| 177 | |||
| 178 | return buffer.len; | ||
| 179 | } else { | ||
| 180 | try bconn.flush(); | ||
| 181 | return try bconn.conn.write(buffer); | ||
| 182 | } | ||
| 183 | } | ||
| 184 | |||
| 185 | pub fn flush(bconn: *BufferedConnection) WriteError!void { | ||
| 186 | defer bconn.write_end = 0; | ||
| 187 | return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]); | ||
| 166 | } | 188 | } |
| 167 | 189 | ||
| 168 | pub const WriteError = Connection.WriteError; | 190 | pub const WriteError = Connection.WriteError; |
| ... | @@ -199,8 +221,6 @@ pub const Compression = union(enum) { | ... | @@ -199,8 +221,6 @@ pub const Compression = union(enum) { |
| 199 | /// A HTTP request originating from a client. | 221 | /// A HTTP request originating from a client. |
| 200 | pub const Request = struct { | 222 | pub const Request = struct { |
| 201 | pub const ParseError = Allocator.Error || error{ | 223 | pub const ParseError = Allocator.Error || error{ |
| 202 | ShortHttpStatusLine, | ||
| 203 | BadHttpVersion, | ||
| 204 | UnknownHttpMethod, | 224 | UnknownHttpMethod, |
| 205 | HttpHeadersInvalid, | 225 | HttpHeadersInvalid, |
| 206 | HttpHeaderContinuationsUnsupported, | 226 | HttpHeaderContinuationsUnsupported, |
| ... | @@ -215,7 +235,7 @@ pub const Request = struct { | ... | @@ -215,7 +235,7 @@ pub const Request = struct { |
| 215 | 235 | ||
| 216 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | 236 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 217 | if (first_line.len < 10) | 237 | if (first_line.len < 10) |
| 218 | return error.ShortHttpStatusLine; | 238 | return error.HttpHeadersInvalid; |
| 219 | 239 | ||
| 220 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | 240 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; |
| 221 | const method_str = first_line[0..method_end]; | 241 | const method_str = first_line[0..method_end]; |
| ... | @@ -229,7 +249,7 @@ pub const Request = struct { | ... | @@ -229,7 +249,7 @@ pub const Request = struct { |
| 229 | const version: http.Version = switch (int64(version_str[0..8])) { | 249 | const version: http.Version = switch (int64(version_str[0..8])) { |
| 230 | int64("HTTP/1.0") => .@"HTTP/1.0", | 250 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 231 | int64("HTTP/1.1") => .@"HTTP/1.1", | 251 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 232 | else => return error.BadHttpVersion, | 252 | else => return error.HttpHeadersInvalid, |
| 233 | }; | 253 | }; |
| 234 | 254 | ||
| 235 | const target = first_line[method_end + 1 .. version_start]; | 255 | const target = first_line[method_end + 1 .. version_start]; |
| ... | @@ -312,7 +332,7 @@ pub const Request = struct { | ... | @@ -312,7 +332,7 @@ pub const Request = struct { |
| 312 | transfer_encoding: ?http.TransferEncoding = null, | 332 | transfer_encoding: ?http.TransferEncoding = null, |
| 313 | transfer_compression: ?http.ContentEncoding = null, | 333 | transfer_compression: ?http.ContentEncoding = null, |
| 314 | 334 | ||
| 315 | headers: http.Headers = undefined, | 335 | headers: http.Headers, |
| 316 | parser: proto.HeadersParser, | 336 | parser: proto.HeadersParser, |
| 317 | compression: Compression = .none, | 337 | compression: Compression = .none, |
| 318 | }; | 338 | }; |
| ... | @@ -329,21 +349,63 @@ pub const Response = struct { | ... | @@ -329,21 +349,63 @@ pub const Response = struct { |
| 329 | 349 | ||
| 330 | transfer_encoding: ResponseTransfer = .none, | 350 | transfer_encoding: ResponseTransfer = .none, |
| 331 | 351 | ||
| 332 | server: *Server, | 352 | allocator: Allocator, |
| 333 | address: net.Address, | 353 | address: net.Address, |
| 334 | connection: BufferedConnection, | 354 | connection: BufferedConnection, |
| 335 | 355 | ||
| 336 | headers: http.Headers, | 356 | headers: http.Headers, |
| 337 | request: Request, | 357 | request: Request, |
| 338 | 358 | ||
| 359 | state: State = .first, | ||
| 360 | |||
| 361 | const State = enum { | ||
| 362 | first, | ||
| 363 | start, | ||
| 364 | waited, | ||
| 365 | responded, | ||
| 366 | finished, | ||
| 367 | }; | ||
| 368 | |||
| 339 | pub fn deinit(res: *Response) void { | 369 | pub fn deinit(res: *Response) void { |
| 340 | res.server.allocator.destroy(res); | 370 | res.connection.close(); |
| 371 | |||
| 372 | res.headers.deinit(); | ||
| 373 | res.request.headers.deinit(); | ||
| 374 | |||
| 375 | if (res.request.parser.header_bytes_owned) { | ||
| 376 | res.request.parser.header_bytes.deinit(res.allocator); | ||
| 377 | } | ||
| 341 | } | 378 | } |
| 342 | 379 | ||
| 380 | pub const ResetState = enum { reset, closing }; | ||
| 381 | |||
| 343 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. | 382 | /// 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 { | 383 | pub fn reset(res: *Response) ResetState { |
| 345 | res.request.headers.deinit(); | 384 | if (res.state == .first) { |
| 346 | res.headers.deinit(); | 385 | res.state = .start; |
| 386 | return .reset; | ||
| 387 | } | ||
| 388 | |||
| 389 | if (!res.request.parser.done) { | ||
| 390 | // If the response wasn't fully read, then we need to close the connection. | ||
| 391 | res.connection.conn.closing = true; | ||
| 392 | return .closing; | ||
| 393 | } | ||
| 394 | |||
| 395 | // A connection is only keep-alive if the Connection header is present and it's value is not "close". | ||
| 396 | // The server and client must both agree | ||
| 397 | // | ||
| 398 | // do() defaults to using keep-alive if the client requests it. | ||
| 399 | const res_connection = res.headers.getFirstValue("connection"); | ||
| 400 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | ||
| 401 | |||
| 402 | const req_connection = res.request.headers.getFirstValue("connection"); | ||
| 403 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | ||
| 404 | if (req_keepalive and (res_keepalive or res_connection == null)) { | ||
| 405 | res.connection.conn.closing = false; | ||
| 406 | } else { | ||
| 407 | res.connection.conn.closing = true; | ||
| 408 | } | ||
| 347 | 409 | ||
| 348 | switch (res.request.compression) { | 410 | switch (res.request.compression) { |
| 349 | .none => {}, | 411 | .none => {}, |
| ... | @@ -352,19 +414,30 @@ pub const Response = struct { | ... | @@ -352,19 +414,30 @@ pub const Response = struct { |
| 352 | .zstd => |*zstd| zstd.deinit(), | 414 | .zstd => |*zstd| zstd.deinit(), |
| 353 | } | 415 | } |
| 354 | 416 | ||
| 355 | if (!res.request.parser.done) { | 417 | res.state = .start; |
| 356 | // If the response wasn't fully read, then we need to close the connection. | 418 | res.version = .@"HTTP/1.1"; |
| 357 | res.connection.conn.closing = true; | 419 | res.status = .ok; |
| 358 | } | 420 | res.reason = null; |
| 359 | 421 | ||
| 360 | if (res.connection.conn.closing) { | 422 | res.transfer_encoding = .none; |
| 361 | res.connection.close(); | ||
| 362 | 423 | ||
| 363 | if (res.request.parser.header_bytes_owned) { | 424 | res.headers.clearRetainingCapacity(); |
| 364 | res.request.parser.header_bytes.deinit(res.server.allocator); | 425 | |
| 365 | } | 426 | res.request.headers.clearAndFree(); // FIXME: figure out why `clearRetainingCapacity` causes a leak in hash_map here |
| 427 | res.request.parser.reset(); | ||
| 428 | |||
| 429 | res.request = Request{ | ||
| 430 | .version = undefined, | ||
| 431 | .method = undefined, | ||
| 432 | .target = undefined, | ||
| 433 | .headers = res.request.headers, | ||
| 434 | .parser = res.request.parser, | ||
| 435 | }; | ||
| 436 | |||
| 437 | if (res.connection.conn.closing) { | ||
| 438 | return .closing; | ||
| 366 | } else { | 439 | } else { |
| 367 | res.request.parser.reset(); | 440 | return .reset; |
| 368 | } | 441 | } |
| 369 | } | 442 | } |
| 370 | 443 | ||
| ... | @@ -372,8 +445,12 @@ pub const Response = struct { | ... | @@ -372,8 +445,12 @@ pub const Response = struct { |
| 372 | 445 | ||
| 373 | /// Send the response headers. | 446 | /// Send the response headers. |
| 374 | pub fn do(res: *Response) !void { | 447 | pub fn do(res: *Response) !void { |
| 375 | var buffered = std.io.bufferedWriter(res.connection.writer()); | 448 | switch (res.state) { |
| 376 | const w = buffered.writer(); | 449 | .waited => res.state = .responded, |
| 450 | .first, .start, .responded, .finished => unreachable, | ||
| 451 | } | ||
| 452 | |||
| 453 | const w = res.connection.writer(); | ||
| 377 | 454 | ||
| 378 | try w.writeAll(@tagName(res.version)); | 455 | try w.writeAll(@tagName(res.version)); |
| 379 | try w.writeByte(' '); | 456 | try w.writeByte(' '); |
| ... | @@ -391,7 +468,14 @@ pub const Response = struct { | ... | @@ -391,7 +468,14 @@ pub const Response = struct { |
| 391 | } | 468 | } |
| 392 | 469 | ||
| 393 | if (!res.headers.contains("connection")) { | 470 | if (!res.headers.contains("connection")) { |
| 394 | try w.writeAll("Connection: keep-alive\r\n"); | 471 | const req_connection = res.request.headers.getFirstValue("connection"); |
| 472 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | ||
| 473 | |||
| 474 | if (req_keepalive) { | ||
| 475 | try w.writeAll("Connection: keep-alive\r\n"); | ||
| 476 | } else { | ||
| 477 | try w.writeAll("Connection: close\r\n"); | ||
| 478 | } | ||
| 395 | } | 479 | } |
| 396 | 480 | ||
| 397 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); | 481 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); |
| ... | @@ -424,7 +508,7 @@ pub const Response = struct { | ... | @@ -424,7 +508,7 @@ pub const Response = struct { |
| 424 | 508 | ||
| 425 | try w.writeAll("\r\n"); | 509 | try w.writeAll("\r\n"); |
| 426 | 510 | ||
| 427 | try buffered.flush(); | 511 | try res.connection.flush(); |
| 428 | } | 512 | } |
| 429 | 513 | ||
| 430 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; | 514 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; |
| ... | @@ -452,29 +536,23 @@ pub const Response = struct { | ... | @@ -452,29 +536,23 @@ pub const Response = struct { |
| 452 | 536 | ||
| 453 | /// Wait for the client to send a complete request head. | 537 | /// Wait for the client to send a complete request head. |
| 454 | pub fn wait(res: *Response) WaitError!void { | 538 | pub fn wait(res: *Response) WaitError!void { |
| 539 | switch (res.state) { | ||
| 540 | .first, .start => res.state = .waited, | ||
| 541 | .waited, .responded, .finished => unreachable, | ||
| 542 | } | ||
| 543 | |||
| 455 | while (true) { | 544 | while (true) { |
| 456 | try res.connection.fill(); | 545 | try res.connection.fill(); |
| 457 | 546 | ||
| 458 | const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek()); | 547 | const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek()); |
| 459 | res.connection.clear(@intCast(u16, nchecked)); | 548 | res.connection.clear(@intCast(u16, nchecked)); |
| 460 | 549 | ||
| 461 | if (res.request.parser.state.isContent()) break; | 550 | if (res.request.parser.state.isContent()) break; |
| 462 | } | 551 | } |
| 463 | 552 | ||
| 464 | res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; | 553 | res.request.headers = .{ .allocator = res.allocator, .owned = true }; |
| 465 | try res.request.parse(res.request.parser.header_bytes.items); | 554 | try res.request.parse(res.request.parser.header_bytes.items); |
| 466 | 555 | ||
| 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| { | 556 | if (res.request.transfer_encoding) |te| { |
| 479 | switch (te) { | 557 | switch (te) { |
| 480 | .chunked => { | 558 | .chunked => { |
| ... | @@ -494,13 +572,13 @@ pub const Response = struct { | ... | @@ -494,13 +572,13 @@ pub const Response = struct { |
| 494 | if (res.request.transfer_compression) |tc| switch (tc) { | 572 | if (res.request.transfer_compression) |tc| switch (tc) { |
| 495 | .compress => return error.CompressionNotSupported, | 573 | .compress => return error.CompressionNotSupported, |
| 496 | .deflate => res.request.compression = .{ | 574 | .deflate => res.request.compression = .{ |
| 497 | .deflate = std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | 575 | .deflate = std.compress.zlib.zlibStream(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, |
| 498 | }, | 576 | }, |
| 499 | .gzip => res.request.compression = .{ | 577 | .gzip => res.request.compression = .{ |
| 500 | .gzip = std.compress.gzip.decompress(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | 578 | .gzip = std.compress.gzip.decompress(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, |
| 501 | }, | 579 | }, |
| 502 | .zstd => res.request.compression = .{ | 580 | .zstd => res.request.compression = .{ |
| 503 | .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()), | 581 | .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()), |
| 504 | }, | 582 | }, |
| 505 | }; | 583 | }; |
| 506 | } | 584 | } |
| ... | @@ -515,6 +593,11 @@ pub const Response = struct { | ... | @@ -515,6 +593,11 @@ pub const Response = struct { |
| 515 | } | 593 | } |
| 516 | 594 | ||
| 517 | pub fn read(res: *Response, buffer: []u8) ReadError!usize { | 595 | pub fn read(res: *Response, buffer: []u8) ReadError!usize { |
| 596 | switch (res.state) { | ||
| 597 | .waited, .responded, .finished => {}, | ||
| 598 | .first, .start => unreachable, | ||
| 599 | } | ||
| 600 | |||
| 518 | const out_index = switch (res.request.compression) { | 601 | const out_index = switch (res.request.compression) { |
| 519 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, | 602 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| 520 | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, | 603 | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, |
| ... | @@ -528,12 +611,12 @@ pub const Response = struct { | ... | @@ -528,12 +611,12 @@ pub const Response = struct { |
| 528 | while (!res.request.parser.state.isContent()) { // read trailing headers | 611 | while (!res.request.parser.state.isContent()) { // read trailing headers |
| 529 | try res.connection.fill(); | 612 | try res.connection.fill(); |
| 530 | 613 | ||
| 531 | const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek()); | 614 | const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek()); |
| 532 | res.connection.clear(@intCast(u16, nchecked)); | 615 | res.connection.clear(@intCast(u16, nchecked)); |
| 533 | } | 616 | } |
| 534 | 617 | ||
| 535 | if (has_trail) { | 618 | if (has_trail) { |
| 536 | res.request.headers = http.Headers{ .allocator = res.server.allocator, .owned = false }; | 619 | res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false }; |
| 537 | 620 | ||
| 538 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. | 621 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. |
| 539 | // This will *only* fail for a malformed trailer. | 622 | // This will *only* fail for a malformed trailer. |
| ... | @@ -564,6 +647,11 @@ pub const Response = struct { | ... | @@ -564,6 +647,11 @@ pub const Response = struct { |
| 564 | 647 | ||
| 565 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | 648 | /// 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 { | 649 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 650 | switch (res.state) { | ||
| 651 | .responded => {}, | ||
| 652 | .first, .waited, .start, .finished => unreachable, | ||
| 653 | } | ||
| 654 | |||
| 567 | switch (res.transfer_encoding) { | 655 | switch (res.transfer_encoding) { |
| 568 | .chunked => { | 656 | .chunked => { |
| 569 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); | 657 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| ... | @@ -583,7 +671,7 @@ pub const Response = struct { | ... | @@ -583,7 +671,7 @@ pub const Response = struct { |
| 583 | } | 671 | } |
| 584 | } | 672 | } |
| 585 | 673 | ||
| 586 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | 674 | pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void { |
| 587 | var index: usize = 0; | 675 | var index: usize = 0; |
| 588 | while (index < bytes.len) { | 676 | while (index < bytes.len) { |
| 589 | index += try write(req, bytes[index..]); | 677 | index += try write(req, bytes[index..]); |
| ... | @@ -594,11 +682,18 @@ pub const Response = struct { | ... | @@ -594,11 +682,18 @@ pub const Response = struct { |
| 594 | 682 | ||
| 595 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 683 | /// 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 { | 684 | pub fn finish(res: *Response) FinishError!void { |
| 685 | switch (res.state) { | ||
| 686 | .responded => res.state = .finished, | ||
| 687 | .first, .waited, .start, .finished => unreachable, | ||
| 688 | } | ||
| 689 | |||
| 597 | switch (res.transfer_encoding) { | 690 | switch (res.transfer_encoding) { |
| 598 | .chunked => try res.connection.writeAll("0\r\n\r\n"), | 691 | .chunked => try res.connection.writeAll("0\r\n\r\n"), |
| 599 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | 692 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 600 | .none => {}, | 693 | .none => {}, |
| 601 | } | 694 | } |
| 695 | |||
| 696 | try res.connection.flush(); | ||
| 602 | } | 697 | } |
| 603 | }; | 698 | }; |
| 604 | 699 | ||
| ... | @@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) { | ... | @@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) { |
| 635 | static: []u8, | 730 | static: []u8, |
| 636 | }; | 731 | }; |
| 637 | 732 | ||
| 638 | /// Accept a new connection and allocate a Response for it. | 733 | pub const AcceptOptions = struct { |
| 639 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { | 734 | allocator: Allocator, |
| 735 | header_strategy: HeaderStrategy = .{ .dynamic = 8192 }, | ||
| 736 | }; | ||
| 737 | |||
| 738 | /// Accept a new connection. | ||
| 739 | pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response { | ||
| 640 | const in = try server.socket.accept(); | 740 | const in = try server.socket.accept(); |
| 641 | 741 | ||
| 642 | const res = try server.allocator.create(Response); | 742 | return Response{ |
| 643 | res.* = .{ | 743 | .allocator = options.allocator, |
| 644 | .server = server, | ||
| 645 | .address = in.address, | 744 | .address = in.address, |
| 646 | .connection = .{ .conn = .{ | 745 | .connection = .{ .conn = .{ |
| 647 | .stream = in.stream, | 746 | .stream = in.stream, |
| 648 | .protocol = .plain, | 747 | .protocol = .plain, |
| 649 | } }, | 748 | } }, |
| 650 | .headers = .{ .allocator = server.allocator }, | 749 | .headers = .{ .allocator = options.allocator }, |
| 651 | .request = .{ | 750 | .request = .{ |
| 652 | .version = undefined, | 751 | .version = undefined, |
| 653 | .method = undefined, | 752 | .method = undefined, |
| 654 | .target = undefined, | 753 | .target = undefined, |
| 655 | .parser = switch (options) { | 754 | .headers = .{ .allocator = options.allocator, .owned = false }, |
| 755 | .parser = switch (options.header_strategy) { | ||
| 656 | .dynamic => |max| proto.HeadersParser.initDynamic(max), | 756 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 657 | .static => |buf| proto.HeadersParser.initStatic(buf), | 757 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| 658 | }, | 758 | }, |
| 659 | }, | 759 | }, |
| 660 | }; | 760 | }; |
| 661 | |||
| 662 | return res; | ||
| 663 | } | 761 | } |
| 664 | 762 | ||
| 665 | test "HTTP server handles a chunked transfer coding request" { | 763 | test "HTTP server handles a chunked transfer coding request" { |
lib/std/http/test.zig deleted-72| ... | @@ -1,72 +0,0 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const expect = std.testing.expect; | ||
| 3 | |||
| 4 | test "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 | }, | ||
| 58 | 62 | ||
| 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+541| ... | @@ -0,0 +1,541 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const http = std.http; | ||
| 4 | const Server = http.Server; | ||
| 5 | const Client = http.Client; | ||
| 6 | |||
| 7 | const mem = std.mem; | ||
| 8 | const testing = std.testing; | ||
| 9 | |||
| 10 | const max_header_size = 8192; | ||
| 11 | |||
| 12 | var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; | ||
| 13 | var gpa_client = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; | ||
| 14 | |||
| 15 | const salloc = gpa_server.allocator(); | ||
| 16 | const calloc = gpa_client.allocator(); | ||
| 17 | |||
| 18 | var server: Server = undefined; | ||
| 19 | |||
| 20 | fn handleRequest(res: *Server.Response) !void { | ||
| 21 | const log = std.log.scoped(.server); | ||
| 22 | |||
| 23 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); | ||
| 24 | |||
| 25 | const body = try res.reader().readAllAlloc(salloc, 8192); | ||
| 26 | defer salloc.free(body); | ||
| 27 | |||
| 28 | if (res.request.headers.contains("connection")) { | ||
| 29 | try res.headers.append("connection", "keep-alive"); | ||
| 30 | } | ||
| 31 | |||
| 32 | if (mem.startsWith(u8, res.request.target, "/get")) { | ||
| 33 | if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) { | ||
| 34 | res.transfer_encoding = .chunked; | ||
| 35 | } else { | ||
| 36 | res.transfer_encoding = .{ .content_length = 14 }; | ||
| 37 | } | ||
| 38 | |||
| 39 | try res.headers.append("content-type", "text/plain"); | ||
| 40 | |||
| 41 | try res.do(); | ||
| 42 | if (res.request.method != .HEAD) { | ||
| 43 | try res.writeAll("Hello, "); | ||
| 44 | try res.writeAll("World!\n"); | ||
| 45 | try res.finish(); | ||
| 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(); | ||
| 65 | } else if (mem.eql(u8, res.request.target, "/echo-content")) { | ||
| 66 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 67 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); | ||
| 68 | |||
| 69 | if (res.request.headers.contains("transfer-encoding")) { | ||
| 70 | try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?); | ||
| 71 | res.transfer_encoding = .chunked; | ||
| 72 | } else { | ||
| 73 | res.transfer_encoding = .{ .content_length = 14 }; | ||
| 74 | try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?); | ||
| 75 | } | ||
| 76 | |||
| 77 | try res.do(); | ||
| 78 | try res.writeAll("Hello, "); | ||
| 79 | try res.writeAll("World!\n"); | ||
| 80 | try res.finish(); | ||
| 81 | } else if (mem.eql(u8, res.request.target, "/trailer")) { | ||
| 82 | res.transfer_encoding = .chunked; | ||
| 83 | |||
| 84 | try res.do(); | ||
| 85 | try res.writeAll("Hello, "); | ||
| 86 | try res.writeAll("World!\n"); | ||
| 87 | // try res.finish(); | ||
| 88 | try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n"); | ||
| 89 | try res.connection.flush(); | ||
| 90 | } else if (mem.eql(u8, res.request.target, "/redirect/1")) { | ||
| 91 | res.transfer_encoding = .chunked; | ||
| 92 | |||
| 93 | res.status = .found; | ||
| 94 | try res.headers.append("location", "../../get"); | ||
| 95 | |||
| 96 | try res.do(); | ||
| 97 | try res.writeAll("Hello, "); | ||
| 98 | try res.writeAll("Redirected!\n"); | ||
| 99 | try res.finish(); | ||
| 100 | } else if (mem.eql(u8, res.request.target, "/redirect/2")) { | ||
| 101 | res.transfer_encoding = .chunked; | ||
| 102 | |||
| 103 | res.status = .found; | ||
| 104 | try res.headers.append("location", "/redirect/1"); | ||
| 105 | |||
| 106 | try res.do(); | ||
| 107 | try res.writeAll("Hello, "); | ||
| 108 | try res.writeAll("Redirected!\n"); | ||
| 109 | try res.finish(); | ||
| 110 | } else if (mem.eql(u8, res.request.target, "/redirect/3")) { | ||
| 111 | res.transfer_encoding = .chunked; | ||
| 112 | |||
| 113 | const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{server.socket.listen_address.getPort()}); | ||
| 114 | defer salloc.free(location); | ||
| 115 | |||
| 116 | res.status = .found; | ||
| 117 | try res.headers.append("location", location); | ||
| 118 | |||
| 119 | try res.do(); | ||
| 120 | try res.writeAll("Hello, "); | ||
| 121 | try res.writeAll("Redirected!\n"); | ||
| 122 | try res.finish(); | ||
| 123 | } else if (mem.eql(u8, res.request.target, "/redirect/4")) { | ||
| 124 | res.transfer_encoding = .chunked; | ||
| 125 | |||
| 126 | res.status = .found; | ||
| 127 | try res.headers.append("location", "/redirect/3"); | ||
| 128 | |||
| 129 | try res.do(); | ||
| 130 | try res.writeAll("Hello, "); | ||
| 131 | try res.writeAll("Redirected!\n"); | ||
| 132 | try res.finish(); | ||
| 133 | } else { | ||
| 134 | res.status = .not_found; | ||
| 135 | try res.do(); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | var handle_new_requests = true; | ||
| 140 | |||
| 141 | fn runServer(srv: *Server) !void { | ||
| 142 | outer: while (handle_new_requests) { | ||
| 143 | var res = try srv.accept(.{ | ||
| 144 | .allocator = salloc, | ||
| 145 | .header_strategy = .{ .dynamic = max_header_size }, | ||
| 146 | }); | ||
| 147 | defer res.deinit(); | ||
| 148 | |||
| 149 | while (res.reset() != .closing) { | ||
| 150 | res.wait() catch |err| switch (err) { | ||
| 151 | error.HttpHeadersInvalid => continue :outer, | ||
| 152 | error.EndOfStream => continue, | ||
| 153 | else => return err, | ||
| 154 | }; | ||
| 155 | |||
| 156 | try handleRequest(&res); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | fn serverThread(srv: *Server) void { | ||
| 162 | defer srv.deinit(); | ||
| 163 | defer _ = gpa_server.deinit(); | ||
| 164 | |||
| 165 | runServer(srv) catch |err| { | ||
| 166 | std.debug.print("server error: {}\n", .{err}); | ||
| 167 | |||
| 168 | if (@errorReturnTrace()) |trace| { | ||
| 169 | std.debug.dumpStackTrace(trace.*); | ||
| 170 | } | ||
| 171 | |||
| 172 | _ = gpa_server.deinit(); | ||
| 173 | std.os.exit(1); | ||
| 174 | }; | ||
| 175 | } | ||
| 176 | |||
| 177 | fn killServer(addr: std.net.Address) void { | ||
| 178 | handle_new_requests = false; | ||
| 179 | |||
| 180 | const conn = std.net.tcpConnectToAddress(addr) catch return; | ||
| 181 | conn.close(); | ||
| 182 | } | ||
| 183 | |||
| 184 | pub fn main() !void { | ||
| 185 | const log = std.log.scoped(.client); | ||
| 186 | |||
| 187 | defer _ = gpa_client.deinit(); | ||
| 188 | |||
| 189 | server = Server.init(salloc, .{ .reuse_address = true }); | ||
| 190 | |||
| 191 | const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; | ||
| 192 | try server.listen(addr); | ||
| 193 | |||
| 194 | const port = server.socket.listen_address.getPort(); | ||
| 195 | |||
| 196 | const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server}); | ||
| 197 | |||
| 198 | var client = Client{ .allocator = calloc }; | ||
| 199 | // defer client.deinit(); handled below | ||
| 200 | |||
| 201 | { // read content-length response | ||
| 202 | var h = http.Headers{ .allocator = calloc }; | ||
| 203 | defer h.deinit(); | ||
| 204 | |||
| 205 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | ||
| 206 | defer calloc.free(location); | ||
| 207 | const uri = try std.Uri.parse(location); | ||
| 208 | |||
| 209 | log.info("{s}", .{location}); | ||
| 210 | var req = try client.request(.GET, uri, h, .{}); | ||
| 211 | defer req.deinit(); | ||
| 212 | |||
| 213 | try req.start(); | ||
| 214 | try req.wait(); | ||
| 215 | |||
| 216 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 217 | defer calloc.free(body); | ||
| 218 | |||
| 219 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 220 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | ||
| 221 | } | ||
| 222 | |||
| 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 | |||
| 250 | { // send head request and not read chunked | ||
| 251 | var h = http.Headers{ .allocator = calloc }; | ||
| 252 | defer h.deinit(); | ||
| 253 | |||
| 254 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | ||
| 255 | defer calloc.free(location); | ||
| 256 | const uri = try std.Uri.parse(location); | ||
| 257 | |||
| 258 | log.info("{s}", .{location}); | ||
| 259 | var req = try client.request(.HEAD, uri, h, .{}); | ||
| 260 | defer req.deinit(); | ||
| 261 | |||
| 262 | try req.start(); | ||
| 263 | try req.wait(); | ||
| 264 | |||
| 265 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 266 | defer calloc.free(body); | ||
| 267 | |||
| 268 | try testing.expectEqualStrings("", body); | ||
| 269 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | ||
| 270 | try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?); | ||
| 271 | } | ||
| 272 | |||
| 273 | // connection has been kept alive | ||
| 274 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 275 | |||
| 276 | { // read chunked response | ||
| 277 | var h = http.Headers{ .allocator = calloc }; | ||
| 278 | defer h.deinit(); | ||
| 279 | |||
| 280 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | ||
| 281 | defer calloc.free(location); | ||
| 282 | const uri = try std.Uri.parse(location); | ||
| 283 | |||
| 284 | log.info("{s}", .{location}); | ||
| 285 | var req = try client.request(.GET, uri, h, .{}); | ||
| 286 | defer req.deinit(); | ||
| 287 | |||
| 288 | try req.start(); | ||
| 289 | try req.wait(); | ||
| 290 | |||
| 291 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 292 | defer calloc.free(body); | ||
| 293 | |||
| 294 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 295 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | ||
| 296 | } | ||
| 297 | |||
| 298 | // connection has been kept alive | ||
| 299 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 300 | |||
| 301 | { // send head request and not read chunked | ||
| 302 | var h = http.Headers{ .allocator = calloc }; | ||
| 303 | defer h.deinit(); | ||
| 304 | |||
| 305 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | ||
| 306 | defer calloc.free(location); | ||
| 307 | const uri = try std.Uri.parse(location); | ||
| 308 | |||
| 309 | log.info("{s}", .{location}); | ||
| 310 | var req = try client.request(.HEAD, uri, h, .{}); | ||
| 311 | defer req.deinit(); | ||
| 312 | |||
| 313 | try req.start(); | ||
| 314 | try req.wait(); | ||
| 315 | |||
| 316 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 317 | defer calloc.free(body); | ||
| 318 | |||
| 319 | try testing.expectEqualStrings("", body); | ||
| 320 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | ||
| 321 | try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?); | ||
| 322 | } | ||
| 323 | |||
| 324 | // connection has been kept alive | ||
| 325 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 326 | |||
| 327 | { // check trailing headers | ||
| 328 | var h = http.Headers{ .allocator = calloc }; | ||
| 329 | defer h.deinit(); | ||
| 330 | |||
| 331 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port}); | ||
| 332 | defer calloc.free(location); | ||
| 333 | const uri = try std.Uri.parse(location); | ||
| 334 | |||
| 335 | log.info("{s}", .{location}); | ||
| 336 | var req = try client.request(.GET, uri, h, .{}); | ||
| 337 | defer req.deinit(); | ||
| 338 | |||
| 339 | try req.start(); | ||
| 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 | try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?); | ||
| 347 | } | ||
| 348 | |||
| 349 | // connection has been kept alive | ||
| 350 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 351 | |||
| 352 | { // send content-length request | ||
| 353 | var h = http.Headers{ .allocator = calloc }; | ||
| 354 | defer h.deinit(); | ||
| 355 | |||
| 356 | try h.append("content-type", "text/plain"); | ||
| 357 | |||
| 358 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | ||
| 359 | defer calloc.free(location); | ||
| 360 | const uri = try std.Uri.parse(location); | ||
| 361 | |||
| 362 | log.info("{s}", .{location}); | ||
| 363 | var req = try client.request(.POST, uri, h, .{}); | ||
| 364 | defer req.deinit(); | ||
| 365 | |||
| 366 | req.transfer_encoding = .{ .content_length = 14 }; | ||
| 367 | |||
| 368 | try req.start(); | ||
| 369 | try req.writeAll("Hello, "); | ||
| 370 | try req.writeAll("World!\n"); | ||
| 371 | try req.finish(); | ||
| 372 | |||
| 373 | try req.wait(); | ||
| 374 | |||
| 375 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 376 | defer calloc.free(body); | ||
| 377 | |||
| 378 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 379 | } | ||
| 380 | |||
| 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 | |||
| 411 | { // send chunked request | ||
| 412 | var h = http.Headers{ .allocator = calloc }; | ||
| 413 | defer h.deinit(); | ||
| 414 | |||
| 415 | try h.append("content-type", "text/plain"); | ||
| 416 | |||
| 417 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | ||
| 418 | defer calloc.free(location); | ||
| 419 | const uri = try std.Uri.parse(location); | ||
| 420 | |||
| 421 | log.info("{s}", .{location}); | ||
| 422 | var req = try client.request(.POST, uri, h, .{}); | ||
| 423 | defer req.deinit(); | ||
| 424 | |||
| 425 | req.transfer_encoding = .chunked; | ||
| 426 | |||
| 427 | try req.start(); | ||
| 428 | try req.writeAll("Hello, "); | ||
| 429 | try req.writeAll("World!\n"); | ||
| 430 | try req.finish(); | ||
| 431 | |||
| 432 | try req.wait(); | ||
| 433 | |||
| 434 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 435 | defer calloc.free(body); | ||
| 436 | |||
| 437 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 438 | } | ||
| 439 | |||
| 440 | // connection has been kept alive | ||
| 441 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 442 | |||
| 443 | { // relative redirect | ||
| 444 | var h = http.Headers{ .allocator = calloc }; | ||
| 445 | defer h.deinit(); | ||
| 446 | |||
| 447 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port}); | ||
| 448 | defer calloc.free(location); | ||
| 449 | const uri = try std.Uri.parse(location); | ||
| 450 | |||
| 451 | log.info("{s}", .{location}); | ||
| 452 | var req = try client.request(.GET, uri, h, .{}); | ||
| 453 | defer req.deinit(); | ||
| 454 | |||
| 455 | try req.start(); | ||
| 456 | try req.wait(); | ||
| 457 | |||
| 458 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 459 | defer calloc.free(body); | ||
| 460 | |||
| 461 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 462 | } | ||
| 463 | |||
| 464 | // connection has been kept alive | ||
| 465 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 466 | |||
| 467 | { // redirect from root | ||
| 468 | var h = http.Headers{ .allocator = calloc }; | ||
| 469 | defer h.deinit(); | ||
| 470 | |||
| 471 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port}); | ||
| 472 | defer calloc.free(location); | ||
| 473 | const uri = try std.Uri.parse(location); | ||
| 474 | |||
| 475 | log.info("{s}", .{location}); | ||
| 476 | var req = try client.request(.GET, uri, h, .{}); | ||
| 477 | defer req.deinit(); | ||
| 478 | |||
| 479 | try req.start(); | ||
| 480 | try req.wait(); | ||
| 481 | |||
| 482 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 483 | defer calloc.free(body); | ||
| 484 | |||
| 485 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 486 | } | ||
| 487 | |||
| 488 | // connection has been kept alive | ||
| 489 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 490 | |||
| 491 | { // absolute redirect | ||
| 492 | var h = http.Headers{ .allocator = calloc }; | ||
| 493 | defer h.deinit(); | ||
| 494 | |||
| 495 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port}); | ||
| 496 | defer calloc.free(location); | ||
| 497 | const uri = try std.Uri.parse(location); | ||
| 498 | |||
| 499 | log.info("{s}", .{location}); | ||
| 500 | var req = try client.request(.GET, uri, h, .{}); | ||
| 501 | defer req.deinit(); | ||
| 502 | |||
| 503 | try req.start(); | ||
| 504 | try req.wait(); | ||
| 505 | |||
| 506 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 507 | defer calloc.free(body); | ||
| 508 | |||
| 509 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 510 | } | ||
| 511 | |||
| 512 | // connection has been kept alive | ||
| 513 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 514 | |||
| 515 | { // too many redirects | ||
| 516 | var h = http.Headers{ .allocator = calloc }; | ||
| 517 | defer h.deinit(); | ||
| 518 | |||
| 519 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port}); | ||
| 520 | defer calloc.free(location); | ||
| 521 | const uri = try std.Uri.parse(location); | ||
| 522 | |||
| 523 | log.info("{s}", .{location}); | ||
| 524 | var req = try client.request(.GET, uri, h, .{}); | ||
| 525 | defer req.deinit(); | ||
| 526 | |||
| 527 | try req.start(); | ||
| 528 | req.wait() catch |err| switch (err) { | ||
| 529 | error.TooManyHttpRedirects => {}, | ||
| 530 | else => return err, | ||
| 531 | }; | ||
| 532 | } | ||
| 533 | |||
| 534 | // connection has been kept alive | ||
| 535 | try testing.expect(client.connection_pool.free_len == 1); | ||
| 536 | |||
| 537 | client.deinit(); | ||
| 538 | |||
| 539 | killServer(server.socket.listen_address); | ||
| 540 | server_thread.join(); | ||
| 541 | } | ||