| author | |
| committer | |
| log | 380916c0f8883746e4d84d5334f68d0569d76f38 |
| tree | d308cd79fd6eeeb1027d82c4da0de5e88a487727 |
| parent | 40ed3c4d2493835cd0a9217b947b115780a9ff27 |
Before I mistakenly thought that missing content-length meant zero when
it actually means to stream until the connection is closed.
Now the respond() function accepts transfer_encoding which can be left
as default (use content.len for content-length), set to none which makes
it omit the content-length, or chunked, which makes it format the
response as a chunked transfer even though the server has the entire
contents already buffered.
The echo-content tests are moved from test/standalone/http.zig to the
standard library where they are actually run.3 files changed, 322 insertions(+), 199 deletions(-)
lib/std/http/Server.zig+81-35| ... | ... | @@ -279,13 +279,15 @@ pub const Request = struct { |
| 279 | 279 | reason: ?[]const u8 = null, |
| 280 | 280 | keep_alive: bool = true, |
| 281 | 281 | extra_headers: []const http.Header = &.{}, |
| 282 | transfer_encoding: ?http.TransferEncoding = null, | |
| 282 | 283 | }; |
| 283 | 284 | |
| 284 | 285 | /// Send an entire HTTP response to the client, including headers and body. |
| 285 | 286 | /// |
| 286 | 287 | /// Automatically handles HEAD requests by omitting the body. |
| 287 | /// Uses the "content-length" header unless `content` is empty in which | |
| 288 | /// case it omits the content-length header. | |
| 288 | /// | |
| 289 | /// Unless `transfer_encoding` is specified, uses the "content-length" | |
| 290 | /// header. | |
| 289 | 291 | /// |
| 290 | 292 | /// If the request contains a body and the connection is to be reused, |
| 291 | 293 | /// discards the request body, leaving the Server in the `ready` state. If |
| ... | ... | @@ -303,7 +305,9 @@ pub const Request = struct { |
| 303 | 305 | assert(options.status != .@"continue"); |
| 304 | 306 | assert(options.extra_headers.len <= max_extra_headers); |
| 305 | 307 | |
| 306 | const keep_alive = request.discardBody(options.keep_alive); | |
| 308 | const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none; | |
| 309 | const server_keep_alive = !transfer_encoding_none and options.keep_alive; | |
| 310 | const keep_alive = request.discardBody(server_keep_alive); | |
| 307 | 311 | |
| 308 | 312 | const phrase = options.reason orelse options.status.phrase() orelse ""; |
| 309 | 313 | |
| ... | ... | @@ -314,9 +318,15 @@ pub const Request = struct { |
| 314 | 318 | }) catch unreachable; |
| 315 | 319 | if (keep_alive) |
| 316 | 320 | h.appendSliceAssumeCapacity("connection: keep-alive\r\n"); |
| 317 | if (content.len > 0) | |
| 321 | ||
| 322 | if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { | |
| 323 | .none => {}, | |
| 324 | .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"), | |
| 325 | } else { | |
| 318 | 326 | h.fixedWriter().print("content-length: {d}\r\n", .{content.len}) catch unreachable; |
| 327 | } | |
| 319 | 328 | |
| 329 | var chunk_header_buffer: [18]u8 = undefined; | |
| 320 | 330 | var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined; |
| 321 | 331 | var iovecs_len: usize = 0; |
| 322 | 332 | |
| ... | ... | @@ -358,12 +368,47 @@ pub const Request = struct { |
| 358 | 368 | }; |
| 359 | 369 | iovecs_len += 1; |
| 360 | 370 | |
| 361 | if (request.head.method != .HEAD and content.len > 0) { | |
| 362 | iovecs[iovecs_len] = .{ | |
| 363 | .iov_base = content.ptr, | |
| 364 | .iov_len = content.len, | |
| 365 | }; | |
| 366 | iovecs_len += 1; | |
| 371 | if (request.head.method != .HEAD) { | |
| 372 | const is_chunked = (options.transfer_encoding orelse .none) == .chunked; | |
| 373 | if (is_chunked) { | |
| 374 | if (content.len > 0) { | |
| 375 | const chunk_header = std.fmt.bufPrint( | |
| 376 | &chunk_header_buffer, | |
| 377 | "{x}\r\n", | |
| 378 | .{content.len}, | |
| 379 | ) catch unreachable; | |
| 380 | ||
| 381 | iovecs[iovecs_len] = .{ | |
| 382 | .iov_base = chunk_header.ptr, | |
| 383 | .iov_len = chunk_header.len, | |
| 384 | }; | |
| 385 | iovecs_len += 1; | |
| 386 | ||
| 387 | iovecs[iovecs_len] = .{ | |
| 388 | .iov_base = content.ptr, | |
| 389 | .iov_len = content.len, | |
| 390 | }; | |
| 391 | iovecs_len += 1; | |
| 392 | ||
| 393 | iovecs[iovecs_len] = .{ | |
| 394 | .iov_base = "\r\n", | |
| 395 | .iov_len = 2, | |
| 396 | }; | |
| 397 | iovecs_len += 1; | |
| 398 | } | |
| 399 | ||
| 400 | iovecs[iovecs_len] = .{ | |
| 401 | .iov_base = "0\r\n\r\n", | |
| 402 | .iov_len = 5, | |
| 403 | }; | |
| 404 | iovecs_len += 1; | |
| 405 | } else if (content.len > 0) { | |
| 406 | iovecs[iovecs_len] = .{ | |
| 407 | .iov_base = content.ptr, | |
| 408 | .iov_len = content.len, | |
| 409 | }; | |
| 410 | iovecs_len += 1; | |
| 411 | } | |
| 367 | 412 | } |
| 368 | 413 | |
| 369 | 414 | try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]); |
| ... | ... | @@ -400,8 +445,9 @@ pub const Request = struct { |
| 400 | 445 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response { |
| 401 | 446 | const o = options.respond_options; |
| 402 | 447 | assert(o.status != .@"continue"); |
| 403 | ||
| 404 | const keep_alive = request.discardBody(o.keep_alive); | |
| 448 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; | |
| 449 | const server_keep_alive = !transfer_encoding_none and o.keep_alive; | |
| 450 | const keep_alive = request.discardBody(server_keep_alive); | |
| 405 | 451 | const phrase = o.reason orelse o.status.phrase() orelse ""; |
| 406 | 452 | |
| 407 | 453 | var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer); |
| ... | ... | @@ -815,26 +861,32 @@ pub const Response = struct { |
| 815 | 861 | }; |
| 816 | 862 | iovecs_len += 1; |
| 817 | 863 | |
| 818 | iovecs[iovecs_len] = .{ | |
| 819 | .iov_base = chunk_header.ptr, | |
| 820 | .iov_len = chunk_header.len, | |
| 821 | }; | |
| 822 | iovecs_len += 1; | |
| 864 | if (r.chunk_len > 0) { | |
| 865 | iovecs[iovecs_len] = .{ | |
| 866 | .iov_base = chunk_header.ptr, | |
| 867 | .iov_len = chunk_header.len, | |
| 868 | }; | |
| 869 | iovecs_len += 1; | |
| 823 | 870 | |
| 824 | iovecs[iovecs_len] = .{ | |
| 825 | .iov_base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len, | |
| 826 | .iov_len = r.chunk_len, | |
| 827 | }; | |
| 828 | iovecs_len += 1; | |
| 871 | iovecs[iovecs_len] = .{ | |
| 872 | .iov_base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len, | |
| 873 | .iov_len = r.chunk_len, | |
| 874 | }; | |
| 875 | iovecs_len += 1; | |
| 876 | ||
| 877 | iovecs[iovecs_len] = .{ | |
| 878 | .iov_base = "\r\n", | |
| 879 | .iov_len = 2, | |
| 880 | }; | |
| 881 | iovecs_len += 1; | |
| 882 | } | |
| 829 | 883 | |
| 830 | 884 | if (end_trailers) |trailers| { |
| 831 | if (r.chunk_len > 0) { | |
| 832 | iovecs[iovecs_len] = .{ | |
| 833 | .iov_base = "\r\n0\r\n", | |
| 834 | .iov_len = 5, | |
| 835 | }; | |
| 836 | iovecs_len += 1; | |
| 837 | } | |
| 885 | iovecs[iovecs_len] = .{ | |
| 886 | .iov_base = "0\r\n", | |
| 887 | .iov_len = 3, | |
| 888 | }; | |
| 889 | iovecs_len += 1; | |
| 838 | 890 | |
| 839 | 891 | for (trailers) |trailer| { |
| 840 | 892 | iovecs[iovecs_len] = .{ |
| ... | ... | @@ -862,12 +914,6 @@ pub const Response = struct { |
| 862 | 914 | iovecs_len += 1; |
| 863 | 915 | } |
| 864 | 916 | |
| 865 | iovecs[iovecs_len] = .{ | |
| 866 | .iov_base = "\r\n", | |
| 867 | .iov_len = 2, | |
| 868 | }; | |
| 869 | iovecs_len += 1; | |
| 870 | } else if (r.chunk_len > 0) { | |
| 871 | 917 | iovecs[iovecs_len] = .{ |
| 872 | 918 | .iov_base = "\r\n", |
| 873 | 919 | .iov_len = 2, |
lib/std/http/test.zig+241-1| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | const builtin = @import("builtin"); |
| 2 | 2 | const std = @import("std"); |
| 3 | 3 | const testing = std.testing; |
| 4 | const native_endian = builtin.cpu.arch.endian(); | |
| 4 | 5 | |
| 5 | 6 | test "trailers" { |
| 6 | 7 | if (builtin.single_threaded) return error.SkipZigTest; |
| ... | ... | @@ -106,7 +107,6 @@ test "HTTP server handles a chunked transfer coding request" { |
| 106 | 107 | return error.SkipZigTest; |
| 107 | 108 | } |
| 108 | 109 | |
| 109 | const native_endian = comptime builtin.cpu.arch.endian(); | |
| 110 | 110 | if (builtin.zig_backend == .stage2_llvm and native_endian == .big) { |
| 111 | 111 | // https://github.com/ziglang/zig/issues/13782 |
| 112 | 112 | return error.SkipZigTest; |
| ... | ... | @@ -168,3 +168,243 @@ test "HTTP server handles a chunked transfer coding request" { |
| 168 | 168 | |
| 169 | 169 | server_thread.join(); |
| 170 | 170 | } |
| 171 | ||
| 172 | test "echo content server" { | |
| 173 | if (builtin.single_threaded) return error.SkipZigTest; | |
| 174 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | |
| 175 | ||
| 176 | if (builtin.zig_backend == .stage2_llvm and native_endian == .big) { | |
| 177 | // https://github.com/ziglang/zig/issues/13782 | |
| 178 | return error.SkipZigTest; | |
| 179 | } | |
| 180 | ||
| 181 | const gpa = std.testing.allocator; | |
| 182 | ||
| 183 | const address = try std.net.Address.parseIp("127.0.0.1", 0); | |
| 184 | var socket_server = try address.listen(.{ .reuse_address = true }); | |
| 185 | defer socket_server.deinit(); | |
| 186 | const port = socket_server.listen_address.in.getPort(); | |
| 187 | ||
| 188 | const server_thread = try std.Thread.spawn(.{}, (struct { | |
| 189 | fn handleRequest(request: *std.http.Server.Request) !void { | |
| 190 | std.debug.print("server received {s} {s} {s}\n", .{ | |
| 191 | @tagName(request.head.method), | |
| 192 | @tagName(request.head.version), | |
| 193 | request.head.target, | |
| 194 | }); | |
| 195 | ||
| 196 | const body = try request.reader().readAllAlloc(std.testing.allocator, 8192); | |
| 197 | defer std.testing.allocator.free(body); | |
| 198 | ||
| 199 | try testing.expect(std.mem.startsWith(u8, request.head.target, "/echo-content")); | |
| 200 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 201 | try testing.expectEqualStrings("text/plain", request.head.content_type.?); | |
| 202 | ||
| 203 | var send_buffer: [100]u8 = undefined; | |
| 204 | var response = request.respondStreaming(.{ | |
| 205 | .send_buffer = &send_buffer, | |
| 206 | .content_length = switch (request.head.transfer_encoding) { | |
| 207 | .chunked => null, | |
| 208 | .none => len: { | |
| 209 | try testing.expectEqual(14, request.head.content_length.?); | |
| 210 | break :len 14; | |
| 211 | }, | |
| 212 | }, | |
| 213 | }); | |
| 214 | ||
| 215 | try response.flush(); // Test an early flush to send the HTTP headers before the body. | |
| 216 | const w = response.writer(); | |
| 217 | try w.writeAll("Hello, "); | |
| 218 | try w.writeAll("World!\n"); | |
| 219 | try response.end(); | |
| 220 | std.debug.print(" server finished responding\n", .{}); | |
| 221 | } | |
| 222 | ||
| 223 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 224 | var read_buffer: [1024]u8 = undefined; | |
| 225 | ||
| 226 | accept: while (true) { | |
| 227 | const conn = try net_server.accept(); | |
| 228 | defer conn.stream.close(); | |
| 229 | ||
| 230 | var http_server = std.http.Server.init(conn, &read_buffer); | |
| 231 | ||
| 232 | while (http_server.state == .ready) { | |
| 233 | var request = http_server.receiveHead() catch |err| switch (err) { | |
| 234 | error.HttpConnectionClosing => continue :accept, | |
| 235 | else => |e| return e, | |
| 236 | }; | |
| 237 | if (std.mem.eql(u8, request.head.target, "/end")) { | |
| 238 | return request.respond("", .{ .keep_alive = false }); | |
| 239 | } | |
| 240 | handleRequest(&request) catch |err| { | |
| 241 | // This message helps the person troubleshooting determine whether | |
| 242 | // output comes from the server thread or the client thread. | |
| 243 | std.debug.print("handleRequest failed with '{s}'\n", .{@errorName(err)}); | |
| 244 | return err; | |
| 245 | }; | |
| 246 | } | |
| 247 | } | |
| 248 | } | |
| 249 | }).run, .{&socket_server}); | |
| 250 | ||
| 251 | defer server_thread.join(); | |
| 252 | ||
| 253 | { | |
| 254 | var client: std.http.Client = .{ .allocator = gpa }; | |
| 255 | defer client.deinit(); | |
| 256 | ||
| 257 | try echoTests(&client, port); | |
| 258 | } | |
| 259 | } | |
| 260 | ||
| 261 | fn echoTests(client: *std.http.Client, port: u16) !void { | |
| 262 | const gpa = testing.allocator; | |
| 263 | var location_buffer: [100]u8 = undefined; | |
| 264 | ||
| 265 | { // send content-length request | |
| 266 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 267 | defer gpa.free(location); | |
| 268 | const uri = try std.Uri.parse(location); | |
| 269 | ||
| 270 | var server_header_buffer: [1024]u8 = undefined; | |
| 271 | var req = try client.open(.POST, uri, .{ | |
| 272 | .server_header_buffer = &server_header_buffer, | |
| 273 | .extra_headers = &.{ | |
| 274 | .{ .name = "content-type", .value = "text/plain" }, | |
| 275 | }, | |
| 276 | }); | |
| 277 | defer req.deinit(); | |
| 278 | ||
| 279 | req.transfer_encoding = .{ .content_length = 14 }; | |
| 280 | ||
| 281 | try req.send(.{}); | |
| 282 | try req.writeAll("Hello, "); | |
| 283 | try req.writeAll("World!\n"); | |
| 284 | try req.finish(); | |
| 285 | ||
| 286 | try req.wait(); | |
| 287 | ||
| 288 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 289 | defer gpa.free(body); | |
| 290 | ||
| 291 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 292 | } | |
| 293 | ||
| 294 | // connection has been kept alive | |
| 295 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); | |
| 296 | ||
| 297 | { // send chunked request | |
| 298 | const uri = try std.Uri.parse(try std.fmt.bufPrint( | |
| 299 | &location_buffer, | |
| 300 | "http://127.0.0.1:{d}/echo-content", | |
| 301 | .{port}, | |
| 302 | )); | |
| 303 | ||
| 304 | var server_header_buffer: [1024]u8 = undefined; | |
| 305 | var req = try client.open(.POST, uri, .{ | |
| 306 | .server_header_buffer = &server_header_buffer, | |
| 307 | .extra_headers = &.{ | |
| 308 | .{ .name = "content-type", .value = "text/plain" }, | |
| 309 | }, | |
| 310 | }); | |
| 311 | defer req.deinit(); | |
| 312 | ||
| 313 | req.transfer_encoding = .chunked; | |
| 314 | ||
| 315 | try req.send(.{}); | |
| 316 | try req.writeAll("Hello, "); | |
| 317 | try req.writeAll("World!\n"); | |
| 318 | try req.finish(); | |
| 319 | ||
| 320 | try req.wait(); | |
| 321 | ||
| 322 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 323 | defer gpa.free(body); | |
| 324 | ||
| 325 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 326 | } | |
| 327 | ||
| 328 | // connection has been kept alive | |
| 329 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); | |
| 330 | ||
| 331 | { // Client.fetch() | |
| 332 | ||
| 333 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port}); | |
| 334 | defer gpa.free(location); | |
| 335 | ||
| 336 | var body = std.ArrayList(u8).init(gpa); | |
| 337 | defer body.deinit(); | |
| 338 | ||
| 339 | const res = try client.fetch(.{ | |
| 340 | .location = .{ .url = location }, | |
| 341 | .method = .POST, | |
| 342 | .payload = "Hello, World!\n", | |
| 343 | .extra_headers = &.{ | |
| 344 | .{ .name = "content-type", .value = "text/plain" }, | |
| 345 | }, | |
| 346 | .response_storage = .{ .dynamic = &body }, | |
| 347 | }); | |
| 348 | try testing.expectEqual(.ok, res.status); | |
| 349 | try testing.expectEqualStrings("Hello, World!\n", body.items); | |
| 350 | } | |
| 351 | ||
| 352 | { // expect: 100-continue | |
| 353 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-100", .{port}); | |
| 354 | defer gpa.free(location); | |
| 355 | const uri = try std.Uri.parse(location); | |
| 356 | ||
| 357 | var server_header_buffer: [1024]u8 = undefined; | |
| 358 | var req = try client.open(.POST, uri, .{ | |
| 359 | .server_header_buffer = &server_header_buffer, | |
| 360 | .extra_headers = &.{ | |
| 361 | .{ .name = "expect", .value = "100-continue" }, | |
| 362 | .{ .name = "content-type", .value = "text/plain" }, | |
| 363 | }, | |
| 364 | }); | |
| 365 | defer req.deinit(); | |
| 366 | ||
| 367 | req.transfer_encoding = .chunked; | |
| 368 | ||
| 369 | try req.send(.{}); | |
| 370 | try req.writeAll("Hello, "); | |
| 371 | try req.writeAll("World!\n"); | |
| 372 | try req.finish(); | |
| 373 | ||
| 374 | try req.wait(); | |
| 375 | try testing.expectEqual(.ok, req.response.status); | |
| 376 | ||
| 377 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 378 | defer gpa.free(body); | |
| 379 | ||
| 380 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 381 | } | |
| 382 | ||
| 383 | { // expect: garbage | |
| 384 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port}); | |
| 385 | defer gpa.free(location); | |
| 386 | const uri = try std.Uri.parse(location); | |
| 387 | ||
| 388 | var server_header_buffer: [1024]u8 = undefined; | |
| 389 | var req = try client.open(.POST, uri, .{ | |
| 390 | .server_header_buffer = &server_header_buffer, | |
| 391 | .extra_headers = &.{ | |
| 392 | .{ .name = "content-type", .value = "text/plain" }, | |
| 393 | .{ .name = "expect", .value = "garbage" }, | |
| 394 | }, | |
| 395 | }); | |
| 396 | defer req.deinit(); | |
| 397 | ||
| 398 | req.transfer_encoding = .chunked; | |
| 399 | ||
| 400 | try req.send(.{}); | |
| 401 | try req.wait(); | |
| 402 | try testing.expectEqual(.expectation_failed, req.response.status); | |
| 403 | } | |
| 404 | ||
| 405 | _ = try client.fetch(.{ | |
| 406 | .location = .{ | |
| 407 | .url = try std.fmt.bufPrint(&location_buffer, "http://127.0.0.1:{d}/end", .{port}), | |
| 408 | }, | |
| 409 | }); | |
| 410 | } |
test/standalone/http.zig-163| ... | ... | @@ -81,26 +81,6 @@ fn handleRequest(request: *http.Server.Request, listen_port: u16) !void { |
| 81 | 81 | try w.writeAll("Hello, World!\n"); |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | try response.end(); | |
| 85 | } else if (mem.startsWith(u8, request.head.target, "/echo-content")) { | |
| 86 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 87 | try testing.expectEqualStrings("text/plain", request.head.content_type.?); | |
| 88 | ||
| 89 | var response = request.respondStreaming(.{ | |
| 90 | .send_buffer = &send_buffer, | |
| 91 | .content_length = switch (request.head.transfer_encoding) { | |
| 92 | .chunked => null, | |
| 93 | .none => len: { | |
| 94 | try testing.expectEqual(14, request.head.content_length.?); | |
| 95 | break :len 14; | |
| 96 | }, | |
| 97 | }, | |
| 98 | }); | |
| 99 | ||
| 100 | try response.flush(); // Test an early flush to send the HTTP headers before the body. | |
| 101 | const w = response.writer(); | |
| 102 | try w.writeAll("Hello, "); | |
| 103 | try w.writeAll("World!\n"); | |
| 104 | 84 | try response.end(); |
| 105 | 85 | } else if (mem.eql(u8, request.head.target, "/redirect/1")) { |
| 106 | 86 | var response = request.respondStreaming(.{ |
| ... | ... | @@ -351,39 +331,6 @@ pub fn main() !void { |
| 351 | 331 | // connection has been kept alive |
| 352 | 332 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); |
| 353 | 333 | |
| 354 | { // send content-length request | |
| 355 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 356 | defer calloc.free(location); | |
| 357 | const uri = try std.Uri.parse(location); | |
| 358 | ||
| 359 | log.info("{s}", .{location}); | |
| 360 | var server_header_buffer: [1024]u8 = undefined; | |
| 361 | var req = try client.open(.POST, uri, .{ | |
| 362 | .server_header_buffer = &server_header_buffer, | |
| 363 | .extra_headers = &.{ | |
| 364 | .{ .name = "content-type", .value = "text/plain" }, | |
| 365 | }, | |
| 366 | }); | |
| 367 | defer req.deinit(); | |
| 368 | ||
| 369 | req.transfer_encoding = .{ .content_length = 14 }; | |
| 370 | ||
| 371 | try req.send(.{}); | |
| 372 | try req.writeAll("Hello, "); | |
| 373 | try req.writeAll("World!\n"); | |
| 374 | try req.finish(); | |
| 375 | ||
| 376 | try req.wait(); | |
| 377 | ||
| 378 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 379 | defer calloc.free(body); | |
| 380 | ||
| 381 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 382 | } | |
| 383 | ||
| 384 | // connection has been kept alive | |
| 385 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); | |
| 386 | ||
| 387 | 334 | { // read content-length response with connection close |
| 388 | 335 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); |
| 389 | 336 | defer calloc.free(location); |
| ... | ... | @@ -410,39 +357,6 @@ pub fn main() !void { |
| 410 | 357 | // connection has been closed |
| 411 | 358 | try testing.expect(client.connection_pool.free_len == 0); |
| 412 | 359 | |
| 413 | { // send chunked request | |
| 414 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 415 | defer calloc.free(location); | |
| 416 | const uri = try std.Uri.parse(location); | |
| 417 | ||
| 418 | log.info("{s}", .{location}); | |
| 419 | var server_header_buffer: [1024]u8 = undefined; | |
| 420 | var req = try client.open(.POST, uri, .{ | |
| 421 | .server_header_buffer = &server_header_buffer, | |
| 422 | .extra_headers = &.{ | |
| 423 | .{ .name = "content-type", .value = "text/plain" }, | |
| 424 | }, | |
| 425 | }); | |
| 426 | defer req.deinit(); | |
| 427 | ||
| 428 | req.transfer_encoding = .chunked; | |
| 429 | ||
| 430 | try req.send(.{}); | |
| 431 | try req.writeAll("Hello, "); | |
| 432 | try req.writeAll("World!\n"); | |
| 433 | try req.finish(); | |
| 434 | ||
| 435 | try req.wait(); | |
| 436 | ||
| 437 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 438 | defer calloc.free(body); | |
| 439 | ||
| 440 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 441 | } | |
| 442 | ||
| 443 | // connection has been kept alive | |
| 444 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); | |
| 445 | ||
| 446 | 360 | { // relative redirect |
| 447 | 361 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port}); |
| 448 | 362 | defer calloc.free(location); |
| ... | ... | @@ -561,83 +475,6 @@ pub fn main() !void { |
| 561 | 475 | // connection has been kept alive |
| 562 | 476 | try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); |
| 563 | 477 | |
| 564 | { // Client.fetch() | |
| 565 | ||
| 566 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#fetch", .{port}); | |
| 567 | defer calloc.free(location); | |
| 568 | ||
| 569 | log.info("{s}", .{location}); | |
| 570 | var body = std.ArrayList(u8).init(calloc); | |
| 571 | defer body.deinit(); | |
| 572 | ||
| 573 | const res = try client.fetch(.{ | |
| 574 | .location = .{ .url = location }, | |
| 575 | .method = .POST, | |
| 576 | .payload = "Hello, World!\n", | |
| 577 | .extra_headers = &.{ | |
| 578 | .{ .name = "content-type", .value = "text/plain" }, | |
| 579 | }, | |
| 580 | .response_storage = .{ .dynamic = &body }, | |
| 581 | }); | |
| 582 | try testing.expectEqual(.ok, res.status); | |
| 583 | try testing.expectEqualStrings("Hello, World!\n", body.items); | |
| 584 | } | |
| 585 | ||
| 586 | { // expect: 100-continue | |
| 587 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port}); | |
| 588 | defer calloc.free(location); | |
| 589 | const uri = try std.Uri.parse(location); | |
| 590 | ||
| 591 | log.info("{s}", .{location}); | |
| 592 | var server_header_buffer: [1024]u8 = undefined; | |
| 593 | var req = try client.open(.POST, uri, .{ | |
| 594 | .server_header_buffer = &server_header_buffer, | |
| 595 | .extra_headers = &.{ | |
| 596 | .{ .name = "expect", .value = "100-continue" }, | |
| 597 | .{ .name = "content-type", .value = "text/plain" }, | |
| 598 | }, | |
| 599 | }); | |
| 600 | defer req.deinit(); | |
| 601 | ||
| 602 | req.transfer_encoding = .chunked; | |
| 603 | ||
| 604 | try req.send(.{}); | |
| 605 | try req.writeAll("Hello, "); | |
| 606 | try req.writeAll("World!\n"); | |
| 607 | try req.finish(); | |
| 608 | ||
| 609 | try req.wait(); | |
| 610 | try testing.expectEqual(http.Status.ok, req.response.status); | |
| 611 | ||
| 612 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 613 | defer calloc.free(body); | |
| 614 | ||
| 615 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 616 | } | |
| 617 | ||
| 618 | { // expect: garbage | |
| 619 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port}); | |
| 620 | defer calloc.free(location); | |
| 621 | const uri = try std.Uri.parse(location); | |
| 622 | ||
| 623 | log.info("{s}", .{location}); | |
| 624 | var server_header_buffer: [1024]u8 = undefined; | |
| 625 | var req = try client.open(.POST, uri, .{ | |
| 626 | .server_header_buffer = &server_header_buffer, | |
| 627 | .extra_headers = &.{ | |
| 628 | .{ .name = "content-type", .value = "text/plain" }, | |
| 629 | .{ .name = "expect", .value = "garbage" }, | |
| 630 | }, | |
| 631 | }); | |
| 632 | defer req.deinit(); | |
| 633 | ||
| 634 | req.transfer_encoding = .chunked; | |
| 635 | ||
| 636 | try req.send(.{}); | |
| 637 | try req.wait(); | |
| 638 | try testing.expectEqual(http.Status.expectation_failed, req.response.status); | |
| 639 | } | |
| 640 | ||
| 641 | 478 | { // issue 16282 *** This test leaves the client in an invalid state, it must be last *** |
| 642 | 479 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); |
| 643 | 480 | defer calloc.free(location); |