| author | |
| committer | |
| log | 134294230a08d531afd0a6d823ae3046b1699b0f |
| tree | 771bbcec0a45f293bba1ae3dd8b8fea4ddeb0df9 |
| parent | 96533b1289f210b415e12d4cf5bbac466279c2e5 |
| signature | Commit is signed but in an unrecognized format. |
5 files changed, 733 insertions(+), 318 deletions(-)
lib/std/http.zig+4-5| ... | ... | @@ -1,6 +1,10 @@ |
| 1 | 1 | pub const Client = @import("http/Client.zig"); |
| 2 | 2 | pub const Server = @import("http/Server.zig"); |
| 3 | 3 | pub const protocol = @import("http/protocol.zig"); |
| 4 | const headers = @import("http/Headers.zig"); | |
| 5 | ||
| 6 | pub const Headers = headers.Headers; | |
| 7 | pub const Header = headers.HeaderEntry; | |
| 4 | 8 | |
| 5 | 9 | pub const Version = enum { |
| 6 | 10 | @"HTTP/1.0", |
| ... | ... | @@ -265,11 +269,6 @@ pub const Connection = enum { |
| 265 | 269 | close, |
| 266 | 270 | }; |
| 267 | 271 | |
| 268 | pub const Header = struct { | |
| 269 | name: []const u8, | |
| 270 | value: []const u8, | |
| 271 | }; | |
| 272 | ||
| 273 | 272 | const std = @import("std.zig"); |
| 274 | 273 | |
| 275 | 274 | test { |
lib/std/http/Client.zig+190-166| ... | ... | @@ -348,140 +348,125 @@ pub const Compression = union(enum) { |
| 348 | 348 | |
| 349 | 349 | /// A HTTP response originating from a server. |
| 350 | 350 | pub const Response = struct { |
| 351 | pub const Headers = struct { | |
| 352 | status: http.Status, | |
| 353 | version: http.Version, | |
| 354 | location: ?[]const u8 = null, | |
| 355 | content_length: ?u64 = null, | |
| 356 | transfer_encoding: ?http.TransferEncoding = null, | |
| 357 | transfer_compression: ?http.ContentEncoding = null, | |
| 358 | connection: http.Connection = .close, | |
| 359 | upgrade: ?[]const u8 = null, | |
| 360 | ||
| 361 | pub const ParseError = error{ | |
| 362 | ShortHttpStatusLine, | |
| 363 | BadHttpVersion, | |
| 364 | HttpHeadersInvalid, | |
| 365 | HttpHeaderContinuationsUnsupported, | |
| 366 | HttpTransferEncodingUnsupported, | |
| 367 | HttpConnectionHeaderUnsupported, | |
| 368 | InvalidContentLength, | |
| 369 | CompressionNotSupported, | |
| 370 | }; | |
| 371 | ||
| 372 | pub fn parse(bytes: []const u8) ParseError!Headers { | |
| 373 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 374 | ||
| 375 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 376 | if (first_line.len < 12) | |
| 377 | return error.ShortHttpStatusLine; | |
| 378 | ||
| 379 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 380 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 381 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 382 | else => return error.BadHttpVersion, | |
| 383 | }; | |
| 384 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 385 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | |
| 386 | ||
| 387 | var headers: Headers = .{ | |
| 388 | .version = version, | |
| 389 | .status = status, | |
| 390 | }; | |
| 391 | ||
| 392 | while (it.next()) |line| { | |
| 393 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 394 | switch (line[0]) { | |
| 395 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 396 | else => {}, | |
| 397 | } | |
| 351 | pub const ParseError = Allocator.Error || error{ | |
| 352 | ShortHttpStatusLine, | |
| 353 | BadHttpVersion, | |
| 354 | HttpHeadersInvalid, | |
| 355 | HttpHeaderContinuationsUnsupported, | |
| 356 | HttpTransferEncodingUnsupported, | |
| 357 | HttpConnectionHeaderUnsupported, | |
| 358 | InvalidContentLength, | |
| 359 | CompressionNotSupported, | |
| 360 | }; | |
| 398 | 361 | |
| 399 | var line_it = mem.tokenize(u8, line, ": "); | |
| 400 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 401 | const header_value = line_it.rest(); | |
| 402 | if (std.ascii.eqlIgnoreCase(header_name, "location")) { | |
| 403 | if (headers.location != null) return error.HttpHeadersInvalid; | |
| 404 | headers.location = header_value; | |
| 405 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 406 | if (headers.content_length != null) return error.HttpHeadersInvalid; | |
| 407 | headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | |
| 408 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 409 | // Transfer-Encoding: second, first | |
| 410 | // Transfer-Encoding: deflate, chunked | |
| 411 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 412 | ||
| 413 | if (iter.next()) |first| { | |
| 414 | const trimmed = mem.trim(u8, first, " "); | |
| 415 | ||
| 416 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 417 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 418 | headers.transfer_encoding = te; | |
| 419 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 420 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 421 | headers.transfer_compression = ce; | |
| 422 | } else { | |
| 423 | return error.HttpTransferEncodingUnsupported; | |
| 424 | } | |
| 425 | } | |
| 362 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { | |
| 363 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 426 | 364 | |
| 427 | if (iter.next()) |second| { | |
| 428 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 365 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 366 | if (first_line.len < 12) | |
| 367 | return error.ShortHttpStatusLine; | |
| 429 | 368 | |
| 430 | const trimmed = mem.trim(u8, second, " "); | |
| 369 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 370 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 371 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 372 | else => return error.BadHttpVersion, | |
| 373 | }; | |
| 374 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 375 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | |
| 376 | const reason = mem.trimLeft(u8, first_line[12..], " "); | |
| 377 | ||
| 378 | res.version = version; | |
| 379 | res.status = status; | |
| 380 | res.reason = reason; | |
| 381 | ||
| 382 | while (it.next()) |line| { | |
| 383 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 384 | switch (line[0]) { | |
| 385 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 386 | else => {}, | |
| 387 | } | |
| 431 | 388 | |
| 432 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 433 | headers.transfer_compression = ce; | |
| 434 | } else { | |
| 435 | return error.HttpTransferEncodingUnsupported; | |
| 436 | } | |
| 389 | var line_it = mem.tokenize(u8, line, ": "); | |
| 390 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 391 | const header_value = line_it.rest(); | |
| 392 | ||
| 393 | try res.headers.append(header_name, header_value); | |
| 394 | ||
| 395 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 396 | if (res.content_length != null) return error.HttpHeadersInvalid; | |
| 397 | res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | |
| 398 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 399 | // Transfer-Encoding: second, first | |
| 400 | // Transfer-Encoding: deflate, chunked | |
| 401 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 402 | ||
| 403 | if (iter.next()) |first| { | |
| 404 | const trimmed = mem.trim(u8, first, " "); | |
| 405 | ||
| 406 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 407 | if (res.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 408 | res.transfer_encoding = te; | |
| 409 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 410 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 411 | res.transfer_compression = ce; | |
| 412 | } else { | |
| 413 | return error.HttpTransferEncodingUnsupported; | |
| 437 | 414 | } |
| 415 | } | |
| 438 | 416 | |
| 439 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 440 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 441 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 417 | if (iter.next()) |second| { | |
| 418 | if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 442 | 419 | |
| 443 | const trimmed = mem.trim(u8, header_value, " "); | |
| 420 | const trimmed = mem.trim(u8, second, " "); | |
| 444 | 421 | |
| 445 | 422 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 446 | headers.transfer_compression = ce; | |
| 423 | res.transfer_compression = ce; | |
| 447 | 424 | } else { |
| 448 | 425 | return error.HttpTransferEncodingUnsupported; |
| 449 | 426 | } |
| 450 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 451 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 452 | headers.connection = .keep_alive; | |
| 453 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 454 | headers.connection = .close; | |
| 455 | } else { | |
| 456 | return error.HttpConnectionHeaderUnsupported; | |
| 457 | } | |
| 458 | } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) { | |
| 459 | headers.upgrade = header_value; | |
| 460 | 427 | } |
| 461 | } | |
| 462 | 428 | |
| 463 | return headers; | |
| 464 | } | |
| 429 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 430 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 431 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 465 | 432 | |
| 466 | inline fn int64(array: *const [8]u8) u64 { | |
| 467 | return @bitCast(u64, array.*); | |
| 468 | } | |
| 433 | const trimmed = mem.trim(u8, header_value, " "); | |
| 469 | 434 | |
| 470 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | |
| 471 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 472 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 473 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | |
| 435 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 436 | res.transfer_compression = ce; | |
| 437 | } else { | |
| 438 | return error.HttpTransferEncodingUnsupported; | |
| 439 | } | |
| 440 | } | |
| 474 | 441 | } |
| 442 | } | |
| 475 | 443 | |
| 476 | test parseInt3 { | |
| 477 | const expectEqual = testing.expectEqual; | |
| 478 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | |
| 479 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | |
| 480 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | |
| 481 | } | |
| 482 | }; | |
| 444 | inline fn int64(array: *const [8]u8) u64 { | |
| 445 | return @bitCast(u64, array.*); | |
| 446 | } | |
| 483 | 447 | |
| 484 | headers: Headers = undefined, | |
| 448 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | |
| 449 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 450 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 451 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | |
| 452 | } | |
| 453 | ||
| 454 | test parseInt3 { | |
| 455 | const expectEqual = testing.expectEqual; | |
| 456 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | |
| 457 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | |
| 458 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | |
| 459 | } | |
| 460 | ||
| 461 | version: http.Version, | |
| 462 | status: http.Status, | |
| 463 | reason: []const u8, | |
| 464 | ||
| 465 | content_length: ?u64 = null, | |
| 466 | transfer_encoding: ?http.TransferEncoding = null, | |
| 467 | transfer_compression: ?http.ContentEncoding = null, | |
| 468 | ||
| 469 | headers: http.Headers, | |
| 485 | 470 | parser: proto.HeadersParser, |
| 486 | 471 | compression: Compression = .none, |
| 487 | 472 | skip: bool = false, |
| ... | ... | @@ -491,22 +476,14 @@ pub const Response = struct { |
| 491 | 476 | /// |
| 492 | 477 | /// Order of operations: request[ -> write -> finish] -> do -> read |
| 493 | 478 | pub const Request = struct { |
| 494 | pub const Headers = struct { | |
| 495 | version: http.Version = .@"HTTP/1.1", | |
| 496 | method: http.Method = .GET, | |
| 497 | user_agent: []const u8 = "zig (std.http)", | |
| 498 | connection: http.Connection = .keep_alive, | |
| 499 | transfer_encoding: RequestTransfer = .none, | |
| 500 | ||
| 501 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 502 | }; | |
| 503 | ||
| 504 | 479 | uri: Uri, |
| 505 | 480 | client: *Client, |
| 506 | 481 | connection: *ConnectionPool.Node, |
| 507 | /// These are stored in Request so that they are available when following | |
| 508 | /// redirects. | |
| 509 | headers: Headers, | |
| 482 | ||
| 483 | method: http.Method, | |
| 484 | version: http.Version = .@"HTTP/1.1", | |
| 485 | headers: http.Headers, | |
| 486 | transfer_encoding: RequestTransfer = .none, | |
| 510 | 487 | |
| 511 | 488 | redirects_left: u32, |
| 512 | 489 | handle_redirects: bool, |
| ... | ... | @@ -526,6 +503,7 @@ pub const Request = struct { |
| 526 | 503 | } |
| 527 | 504 | |
| 528 | 505 | if (req.response.parser.header_bytes_owned) { |
| 506 | req.response.headers.deinit(); | |
| 529 | 507 | req.response.parser.header_bytes.deinit(req.client.allocator); |
| 530 | 508 | } |
| 531 | 509 | |
| ... | ... | @@ -540,14 +518,14 @@ pub const Request = struct { |
| 540 | 518 | req.* = undefined; |
| 541 | 519 | } |
| 542 | 520 | |
| 543 | pub fn start(req: *Request, uri: Uri, headers: Headers) !void { | |
| 521 | pub fn start(req: *Request, uri: Uri) !void { | |
| 544 | 522 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 545 | 523 | const w = buffered.writer(); |
| 546 | 524 | |
| 547 | try w.writeAll(@tagName(headers.method)); | |
| 525 | try w.writeAll(@tagName(req.method)); | |
| 548 | 526 | try w.writeByte(' '); |
| 549 | 527 | |
| 550 | if (req.headers.method == .CONNECT) { | |
| 528 | if (req.method == .CONNECT) { | |
| 551 | 529 | try w.writeAll(uri.host.?); |
| 552 | 530 | try w.writeByte(':'); |
| 553 | 531 | try w.print("{}", .{uri.port.?}); |
| ... | ... | @@ -559,33 +537,62 @@ pub const Request = struct { |
| 559 | 537 | } |
| 560 | 538 | |
| 561 | 539 | try w.writeByte(' '); |
| 562 | try w.writeAll(@tagName(headers.version)); | |
| 563 | try w.writeAll("\r\nHost: "); | |
| 564 | try w.writeAll(uri.host.?); | |
| 565 | try w.writeAll("\r\nUser-Agent: "); | |
| 566 | try w.writeAll(headers.user_agent); | |
| 567 | if (headers.connection == .close) { | |
| 568 | try w.writeAll("\r\nConnection: close"); | |
| 569 | } else { | |
| 570 | try w.writeAll("\r\nConnection: keep-alive"); | |
| 540 | try w.writeAll(@tagName(req.version)); | |
| 541 | try w.writeAll("\r\n"); | |
| 542 | ||
| 543 | if (!req.headers.contains("host")) { | |
| 544 | try w.writeAll("Host: "); | |
| 545 | try w.writeAll(uri.host.?); | |
| 546 | try w.writeAll("\r\n"); | |
| 571 | 547 | } |
| 572 | try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); | |
| 573 | try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers. | |
| 574 | 548 | |
| 575 | switch (headers.transfer_encoding) { | |
| 576 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), | |
| 577 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), | |
| 578 | .none => {}, | |
| 549 | if (!req.headers.contains("user-agent")) { | |
| 550 | try w.writeAll("User-Agent: zig/"); | |
| 551 | try w.writeAll(@import("builtin").zig_version_string); | |
| 552 | try w.writeAll(" (std.http)\r\n"); | |
| 579 | 553 | } |
| 580 | 554 | |
| 581 | for (headers.custom) |header| { | |
| 582 | try w.writeAll("\r\n"); | |
| 583 | try w.writeAll(header.name); | |
| 584 | try w.writeAll(": "); | |
| 585 | try w.writeAll(header.value); | |
| 555 | if (!req.headers.contains("connection")) { | |
| 556 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 586 | 557 | } |
| 587 | 558 | |
| 588 | try w.writeAll("\r\n\r\n"); | |
| 559 | if (!req.headers.contains("accept-encoding")) { | |
| 560 | try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n"); | |
| 561 | } | |
| 562 | ||
| 563 | if (!req.headers.contains("te")) { | |
| 564 | try w.writeAll("TE: gzip, deflate, trailers\r\n"); | |
| 565 | } | |
| 566 | ||
| 567 | const has_transfer_encoding = req.headers.contains("transfer-encoding"); | |
| 568 | const has_content_length = req.headers.contains("content-length"); | |
| 569 | ||
| 570 | if (!has_transfer_encoding and !has_content_length) { | |
| 571 | switch (req.transfer_encoding) { | |
| 572 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | |
| 573 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | |
| 574 | .none => {}, | |
| 575 | } | |
| 576 | } else { | |
| 577 | if (has_content_length) { | |
| 578 | const content_length = try std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10); | |
| 579 | ||
| 580 | req.transfer_encoding = .{ .content_length = content_length }; | |
| 581 | } else if (has_transfer_encoding) { | |
| 582 | const transfer_encoding = req.headers.getFirstValue("content-length").?; | |
| 583 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | |
| 584 | req.transfer_encoding = .chunked; | |
| 585 | } else { | |
| 586 | return error.UnsupportedTransferEncoding; | |
| 587 | } | |
| 588 | } else { | |
| 589 | req.transfer_encoding = .none; | |
| 590 | } | |
| 591 | } | |
| 592 | ||
| 593 | try w.print("{}", .{req.headers}); | |
| 594 | ||
| 595 | try w.writeAll("\r\n"); | |
| 589 | 596 | |
| 590 | 597 | try buffered.flush(); |
| 591 | 598 | } |
| ... | ... | @@ -611,7 +618,7 @@ pub const Request = struct { |
| 611 | 618 | return index; |
| 612 | 619 | } |
| 613 | 620 | |
| 614 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; | |
| 621 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; | |
| 615 | 622 | |
| 616 | 623 | /// Waits for a response from the server and parses any headers that are sent. |
| 617 | 624 | /// This function will block until the final response is received. |
| ... | ... | @@ -629,33 +636,39 @@ pub const Request = struct { |
| 629 | 636 | if (req.response.parser.state.isContent()) break; |
| 630 | 637 | } |
| 631 | 638 | |
| 632 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); | |
| 639 | req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false }; | |
| 640 | try req.response.parse(req.response.parser.header_bytes.items); | |
| 633 | 641 | |
| 634 | if (req.response.headers.status == .switching_protocols) { | |
| 642 | if (req.response.status == .switching_protocols) { | |
| 635 | 643 | req.connection.data.closing = false; |
| 636 | 644 | req.response.parser.done = true; |
| 637 | 645 | } |
| 638 | 646 | |
| 639 | if (req.headers.method == .CONNECT and req.response.headers.status == .ok) { | |
| 647 | if (req.method == .CONNECT and req.response.status == .ok) { | |
| 640 | 648 | req.connection.data.closing = false; |
| 641 | 649 | req.connection.data.proxied = true; |
| 642 | 650 | req.response.parser.done = true; |
| 643 | 651 | } |
| 644 | 652 | |
| 645 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | |
| 653 | const req_connection = req.headers.getFirstValue("connection"); | |
| 654 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 655 | ||
| 656 | const res_connection = req.response.headers.getFirstValue("connection"); | |
| 657 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 658 | if (req_keepalive and res_keepalive) { | |
| 646 | 659 | req.connection.data.closing = false; |
| 647 | 660 | } else { |
| 648 | 661 | req.connection.data.closing = true; |
| 649 | 662 | } |
| 650 | 663 | |
| 651 | if (req.response.headers.transfer_encoding) |te| { | |
| 664 | if (req.response.transfer_encoding) |te| { | |
| 652 | 665 | switch (te) { |
| 653 | 666 | .chunked => { |
| 654 | 667 | req.response.parser.next_chunk_length = 0; |
| 655 | 668 | req.response.parser.state = .chunk_head_size; |
| 656 | 669 | }, |
| 657 | 670 | } |
| 658 | } else if (req.response.headers.content_length) |cl| { | |
| 671 | } else if (req.response.content_length) |cl| { | |
| 659 | 672 | req.response.parser.next_chunk_length = cl; |
| 660 | 673 | |
| 661 | 674 | if (cl == 0) req.response.parser.done = true; |
| ... | ... | @@ -663,7 +676,7 @@ pub const Request = struct { |
| 663 | 676 | req.response.parser.done = true; |
| 664 | 677 | } |
| 665 | 678 | |
| 666 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) { | |
| 679 | if (req.response.status.class() == .redirect and req.handle_redirects) { | |
| 667 | 680 | req.response.skip = true; |
| 668 | 681 | |
| 669 | 682 | const empty = @as([*]u8, undefined)[0..0]; |
| ... | ... | @@ -671,7 +684,7 @@ pub const Request = struct { |
| 671 | 684 | |
| 672 | 685 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 673 | 686 | |
| 674 | const location = req.response.headers.location orelse | |
| 687 | const location = req.response.headers.getFirstValue("location") orelse | |
| 675 | 688 | return error.HttpRedirectMissingLocation; |
| 676 | 689 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); |
| 677 | 690 | |
| ... | ... | @@ -683,6 +696,8 @@ pub const Request = struct { |
| 683 | 696 | req.arena = new_arena; |
| 684 | 697 | |
| 685 | 698 | const new_req = try req.client.request(resolved_url, req.headers, .{ |
| 699 | .method = req.method, | |
| 700 | .version = req.version, | |
| 686 | 701 | .max_redirects = req.redirects_left - 1, |
| 687 | 702 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ |
| 688 | 703 | .dynamic = req.response.parser.max_header_bytes, |
| ... | ... | @@ -695,7 +710,7 @@ pub const Request = struct { |
| 695 | 710 | } else { |
| 696 | 711 | req.response.skip = false; |
| 697 | 712 | if (!req.response.parser.done) { |
| 698 | if (req.response.headers.transfer_compression) |tc| switch (tc) { | |
| 713 | if (req.response.transfer_compression) |tc| switch (tc) { | |
| 699 | 714 | .compress => return error.CompressionNotSupported, |
| 700 | 715 | .deflate => req.response.compression = .{ |
| 701 | 716 | .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, |
| ... | ... | @@ -789,7 +804,7 @@ pub const Request = struct { |
| 789 | 804 | |
| 790 | 805 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 791 | 806 | pub fn finish(req: *Request) FinishError!void { |
| 792 | switch (req.headers.transfer_encoding) { | |
| 807 | switch (req.transfer_encoding) { | |
| 793 | 808 | .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| { |
| 794 | 809 | req.client.last_error = .{ .write = err }; |
| 795 | 810 | return error.WriteFailed; |
| ... | ... | @@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 908 | 923 | } |
| 909 | 924 | } |
| 910 | 925 | |
| 911 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{ | |
| 926 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || std.fmt.ParseIntError || BufferedConnection.WriteError || error{ | |
| 912 | 927 | UnsupportedUrlScheme, |
| 913 | 928 | UriMissingHost, |
| 914 | 929 | |
| 915 | 930 | CertificateBundleLoadFailure, |
| 931 | UnsupportedTransferEncoding, | |
| 916 | 932 | }; |
| 917 | 933 | |
| 918 | 934 | pub const Options = struct { |
| 935 | method: http.Method = .GET, | |
| 936 | version: http.Version = .@"HTTP/1.1", | |
| 937 | ||
| 919 | 938 | handle_redirects: bool = true, |
| 920 | 939 | max_redirects: u32 = 3, |
| 921 | 940 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| ... | ... | @@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| 946 | 965 | |
| 947 | 966 | /// Form and send a http request to a server. |
| 948 | 967 | /// This function is threadsafe. |
| 949 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { | |
| 968 | pub fn request(client: *Client, uri: Uri, headers: http.Headers, options: Options) RequestError!Request { | |
| 950 | 969 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 951 | 970 | |
| 952 | 971 | const port: u16 = uri.port orelse switch (protocol) { |
| ... | ... | @@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 973 | 992 | .client = client, |
| 974 | 993 | .connection = conn, |
| 975 | 994 | .headers = headers, |
| 995 | .method = options.method, | |
| 996 | .version = options.version, | |
| 976 | 997 | .redirects_left = options.max_redirects, |
| 977 | 998 | .handle_redirects = options.handle_redirects, |
| 978 | 999 | .response = .{ |
| 1000 | .status = undefined, | |
| 1001 | .version = undefined, | |
| 1002 | .headers = undefined, | |
| 979 | 1003 | .parser = switch (options.header_strategy) { |
| 980 | 1004 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 981 | 1005 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| ... | ... | @@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 987 | 1011 | |
| 988 | 1012 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 989 | 1013 | |
| 990 | try req.start(uri, headers); | |
| 1014 | try req.start(uri); | |
| 991 | 1015 | |
| 992 | 1016 | return req; |
| 993 | 1017 | } |
lib/std/http/Headers.zig created+386| ... | ... | @@ -0,0 +1,386 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | ||
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | ||
| 5 | const testing = std.testing; | |
| 6 | const ascii = std.ascii; | |
| 7 | const assert = std.debug.assert; | |
| 8 | ||
| 9 | pub const HeaderList = std.ArrayListUnmanaged(HeaderEntry); | |
| 10 | pub const HeaderIndexList = std.ArrayListUnmanaged(usize); | |
| 11 | pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage); | |
| 12 | ||
| 13 | pub const CaseInsensitiveStringContext = struct { | |
| 14 | pub fn hash(self: @This(), s: []const u8) u64 { | |
| 15 | _ = self; | |
| 16 | var buf: [64]u8 = undefined; | |
| 17 | var i: u8 = 0; | |
| 18 | ||
| 19 | var h = std.hash.Wyhash.init(0); | |
| 20 | while (i < s.len) : (i += 64) { | |
| 21 | const left = @min(64, s.len - i); | |
| 22 | const ret = ascii.lowerString(buf[0..], s[i..][0..left]); | |
| 23 | h.update(ret); | |
| 24 | } | |
| 25 | ||
| 26 | return h.final(); | |
| 27 | } | |
| 28 | ||
| 29 | pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { | |
| 30 | _ = self; | |
| 31 | return ascii.eqlIgnoreCase(a, b); | |
| 32 | } | |
| 33 | }; | |
| 34 | ||
| 35 | pub const HeaderEntry = struct { | |
| 36 | name: []const u8, | |
| 37 | value: []const u8, | |
| 38 | ||
| 39 | pub fn modify(entry: *HeaderEntry, allocator: Allocator, new_value: []const u8) !void { | |
| 40 | if (entry.value.len <= new_value.len) { | |
| 41 | std.mem.copy(u8, @constCast(entry.value), new_value); | |
| 42 | } else { | |
| 43 | allocator.free(entry.value); | |
| 44 | ||
| 45 | entry.value = try allocator.dupe(u8, new_value); | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | fn lessThan(ctx: void, a: HeaderEntry, b: HeaderEntry) bool { | |
| 50 | _ = ctx; | |
| 51 | if (a.name.ptr == b.name.ptr) return false; | |
| 52 | ||
| 53 | return ascii.lessThanIgnoreCase(a.name, b.name); | |
| 54 | } | |
| 55 | }; | |
| 56 | ||
| 57 | pub const Headers = struct { | |
| 58 | allocator: Allocator, | |
| 59 | list: HeaderList = .{}, | |
| 60 | index: HeaderIndex = .{}, | |
| 61 | ||
| 62 | /// When this is false, names and values will not be duplicated. | |
| 63 | /// Use with caution. | |
| 64 | owned: bool = true, | |
| 65 | ||
| 66 | pub fn init(allocator: Allocator) Headers { | |
| 67 | return .{ .allocator = allocator }; | |
| 68 | } | |
| 69 | ||
| 70 | pub fn deinit(headers: *Headers) void { | |
| 71 | var it = headers.index.iterator(); | |
| 72 | while (it.next()) |entry| { | |
| 73 | entry.value_ptr.deinit(headers.allocator); | |
| 74 | ||
| 75 | if (headers.owned) headers.allocator.free(entry.key_ptr.*); | |
| 76 | } | |
| 77 | ||
| 78 | for (headers.list.items) |entry| { | |
| 79 | if (headers.owned) headers.allocator.free(entry.value); | |
| 80 | } | |
| 81 | ||
| 82 | headers.index.deinit(headers.allocator); | |
| 83 | headers.list.deinit(headers.allocator); | |
| 84 | ||
| 85 | headers.* = undefined; | |
| 86 | } | |
| 87 | ||
| 88 | /// Appends a header to the list. Both name and value are copied. | |
| 89 | pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void { | |
| 90 | const n = headers.list.items.len; | |
| 91 | ||
| 92 | const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value; | |
| 93 | errdefer if (headers.owned) headers.allocator.free(value_duped); | |
| 94 | ||
| 95 | var entry = HeaderEntry{ .name = undefined, .value = value_duped }; | |
| 96 | ||
| 97 | if (headers.index.getEntry(name)) |kv| { | |
| 98 | entry.name = kv.key_ptr.*; | |
| 99 | try kv.value_ptr.append(headers.allocator, n); | |
| 100 | } else { | |
| 101 | const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name; | |
| 102 | errdefer if (headers.owned) headers.allocator.free(name_duped); | |
| 103 | ||
| 104 | entry.name = name_duped; | |
| 105 | ||
| 106 | var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1); | |
| 107 | errdefer new_index.deinit(headers.allocator); | |
| 108 | ||
| 109 | new_index.appendAssumeCapacity(n); | |
| 110 | try headers.index.put(headers.allocator, name_duped, new_index); | |
| 111 | } | |
| 112 | ||
| 113 | try headers.list.append(headers.allocator, entry); | |
| 114 | } | |
| 115 | ||
| 116 | pub fn contains(headers: Headers, name: []const u8) bool { | |
| 117 | return headers.index.contains(name); | |
| 118 | } | |
| 119 | ||
| 120 | pub fn delete(headers: *Headers, name: []const u8) bool { | |
| 121 | if (headers.index.fetchRemove(name)) |kv| { | |
| 122 | var index = kv.value; | |
| 123 | ||
| 124 | // iterate backwards | |
| 125 | var i = index.items.len; | |
| 126 | while (i > 0) { | |
| 127 | i -= 1; | |
| 128 | const data_index = index.items[i]; | |
| 129 | const removed = headers.list.orderedRemove(data_index); | |
| 130 | ||
| 131 | assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted | |
| 132 | if (headers.owned) headers.allocator.free(removed.value); | |
| 133 | } | |
| 134 | ||
| 135 | if (headers.owned) headers.allocator.free(kv.key); | |
| 136 | index.deinit(headers.allocator); | |
| 137 | headers.rebuildIndex(); | |
| 138 | ||
| 139 | return true; | |
| 140 | } else { | |
| 141 | return false; | |
| 142 | } | |
| 143 | } | |
| 144 | ||
| 145 | /// Returns the index of the first occurrence of a header with the given name. | |
| 146 | pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize { | |
| 147 | const index = headers.index.get(name) orelse return null; | |
| 148 | ||
| 149 | return index.items[0]; | |
| 150 | } | |
| 151 | ||
| 152 | /// Returns a list of indices containing headers with the given name. | |
| 153 | pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize { | |
| 154 | const index = headers.index.get(name) orelse return null; | |
| 155 | ||
| 156 | return index.items; | |
| 157 | } | |
| 158 | ||
| 159 | /// Returns the entry of the first occurrence of a header with the given name. | |
| 160 | pub fn getFirstEntry(headers: Headers, name: []const u8) ?HeaderEntry { | |
| 161 | const first_index = headers.firstIndexOf(name) orelse return null; | |
| 162 | ||
| 163 | return headers.list.items[first_index]; | |
| 164 | } | |
| 165 | ||
| 166 | /// Returns a slice containing each header with the given name. | |
| 167 | /// The caller owns the returned slice, but NOT the values in the slice. | |
| 168 | pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const HeaderEntry { | |
| 169 | const indices = headers.getIndices(name) orelse return null; | |
| 170 | ||
| 171 | const buf = try allocator.alloc(HeaderEntry, indices.len); | |
| 172 | for (indices, 0..) |idx, n| { | |
| 173 | buf[n] = headers.list.items[idx]; | |
| 174 | } | |
| 175 | ||
| 176 | return buf; | |
| 177 | } | |
| 178 | ||
| 179 | /// Returns the value in the entry of the first occurrence of a header with the given name. | |
| 180 | pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 { | |
| 181 | const first_index = headers.firstIndexOf(name) orelse return null; | |
| 182 | ||
| 183 | return headers.list.items[first_index].value; | |
| 184 | } | |
| 185 | ||
| 186 | /// Returns a slice containing the value of each header with the given name. | |
| 187 | /// The caller owns the returned slice, but NOT the values in the slice. | |
| 188 | pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 { | |
| 189 | const indices = headers.getIndices(name) orelse return null; | |
| 190 | ||
| 191 | const buf = try allocator.alloc([]const u8, indices.len); | |
| 192 | for (indices, 0..) |idx, n| { | |
| 193 | buf[n] = headers.list.items[idx].value; | |
| 194 | } | |
| 195 | ||
| 196 | return buf; | |
| 197 | } | |
| 198 | ||
| 199 | fn rebuildIndex(headers: *Headers) void { | |
| 200 | // clear out the indexes | |
| 201 | var it = headers.index.iterator(); | |
| 202 | while (it.next()) |entry| { | |
| 203 | entry.value_ptr.shrinkRetainingCapacity(0); | |
| 204 | } | |
| 205 | ||
| 206 | // fill up indexes again; we know capacity is fine from before | |
| 207 | for (headers.list.items, 0..) |entry, i| { | |
| 208 | headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i); | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | /// Sorts the headers in lexicographical order. | |
| 213 | pub fn sort(headers: *Headers) void { | |
| 214 | std.sort.sort(HeaderEntry, headers.list.items, {}, HeaderEntry.lessThan); | |
| 215 | headers.rebuildIndex(); | |
| 216 | } | |
| 217 | ||
| 218 | /// Writes the headers to the given stream. | |
| 219 | pub fn format( | |
| 220 | headers: Headers, | |
| 221 | comptime fmt: []const u8, | |
| 222 | options: std.fmt.FormatOptions, | |
| 223 | out_stream: anytype, | |
| 224 | ) !void { | |
| 225 | _ = fmt; | |
| 226 | _ = options; | |
| 227 | ||
| 228 | for (headers.list.items) |entry| { | |
| 229 | if (entry.value.len == 0) continue; | |
| 230 | ||
| 231 | try out_stream.writeAll(entry.name); | |
| 232 | try out_stream.writeAll(": "); | |
| 233 | try out_stream.writeAll(entry.value); | |
| 234 | try out_stream.writeAll("\r\n"); | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | /// Writes all of the headers with the given name to the given stream, separated by commas. | |
| 239 | /// | |
| 240 | /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2 | |
| 241 | pub fn formatCommaSeparated( | |
| 242 | headers: Headers, | |
| 243 | name: []const u8, | |
| 244 | out_stream: anytype, | |
| 245 | ) !void { | |
| 246 | const indices = headers.getIndices(name) orelse return; | |
| 247 | ||
| 248 | try out_stream.writeAll(name); | |
| 249 | try out_stream.writeAll(": "); | |
| 250 | ||
| 251 | for (indices, 0..) |idx, n| { | |
| 252 | if (n != 0) try out_stream.writeAll(", "); | |
| 253 | try out_stream.writeAll(headers.list.items[idx].value); | |
| 254 | } | |
| 255 | ||
| 256 | try out_stream.writeAll("\r\n"); | |
| 257 | } | |
| 258 | }; | |
| 259 | ||
| 260 | test "Headers.append" { | |
| 261 | var h = Headers{ .allocator = std.testing.allocator }; | |
| 262 | defer h.deinit(); | |
| 263 | ||
| 264 | try h.append("foo", "bar"); | |
| 265 | try h.append("hello", "world"); | |
| 266 | ||
| 267 | try testing.expect(h.contains("Foo")); | |
| 268 | try testing.expect(!h.contains("Bar")); | |
| 269 | } | |
| 270 | ||
| 271 | test "Headers.delete" { | |
| 272 | var h = Headers{ .allocator = std.testing.allocator }; | |
| 273 | defer h.deinit(); | |
| 274 | ||
| 275 | try h.append("foo", "bar"); | |
| 276 | try h.append("hello", "world"); | |
| 277 | ||
| 278 | try testing.expect(h.contains("Foo")); | |
| 279 | ||
| 280 | _ = h.delete("Foo"); | |
| 281 | ||
| 282 | try testing.expect(!h.contains("foo")); | |
| 283 | } | |
| 284 | ||
| 285 | test "Headers consistency" { | |
| 286 | var h = Headers{ .allocator = std.testing.allocator }; | |
| 287 | defer h.deinit(); | |
| 288 | ||
| 289 | try h.append("foo", "bar"); | |
| 290 | try h.append("hello", "world"); | |
| 291 | _ = h.delete("Foo"); | |
| 292 | ||
| 293 | try h.append("foo", "bar"); | |
| 294 | try h.append("bar", "world"); | |
| 295 | try h.append("foo", "baz"); | |
| 296 | try h.append("baz", "hello"); | |
| 297 | ||
| 298 | try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello")); | |
| 299 | try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo")); | |
| 300 | try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar")); | |
| 301 | try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz")); | |
| 302 | try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog")); | |
| 303 | ||
| 304 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?); | |
| 305 | try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?); | |
| 306 | try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?); | |
| 307 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?); | |
| 308 | try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog")); | |
| 309 | ||
| 310 | try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value); | |
| 311 | try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value); | |
| 312 | try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value); | |
| 313 | try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value); | |
| 314 | ||
| 315 | const hello_entries = (try h.getEntries(testing.allocator, "hello")).?; | |
| 316 | defer testing.allocator.free(hello_entries); | |
| 317 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | |
| 318 | .{ .name = "hello", .value = "world" }, | |
| 319 | }), hello_entries); | |
| 320 | ||
| 321 | const foo_entries = (try h.getEntries(testing.allocator, "foo")).?; | |
| 322 | defer testing.allocator.free(foo_entries); | |
| 323 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | |
| 324 | .{ .name = "foo", .value = "bar" }, | |
| 325 | .{ .name = "foo", .value = "baz" }, | |
| 326 | }), foo_entries); | |
| 327 | ||
| 328 | const bar_entries = (try h.getEntries(testing.allocator, "bar")).?; | |
| 329 | defer testing.allocator.free(bar_entries); | |
| 330 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | |
| 331 | .{ .name = "bar", .value = "world" }, | |
| 332 | }), bar_entries); | |
| 333 | ||
| 334 | const baz_entries = (try h.getEntries(testing.allocator, "baz")).?; | |
| 335 | defer testing.allocator.free(baz_entries); | |
| 336 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | |
| 337 | .{ .name = "baz", .value = "hello" }, | |
| 338 | }), baz_entries); | |
| 339 | ||
| 340 | const pog_entries = (try h.getEntries(testing.allocator, "pog")); | |
| 341 | try testing.expectEqual(@as(?[]const HeaderEntry, null), pog_entries); | |
| 342 | ||
| 343 | try testing.expectEqualStrings("world", h.getFirstValue("hello").?); | |
| 344 | try testing.expectEqualStrings("bar", h.getFirstValue("foo").?); | |
| 345 | try testing.expectEqualStrings("world", h.getFirstValue("bar").?); | |
| 346 | try testing.expectEqualStrings("hello", h.getFirstValue("baz").?); | |
| 347 | try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog")); | |
| 348 | ||
| 349 | const hello_values = (try h.getValues(testing.allocator, "hello")).?; | |
| 350 | defer testing.allocator.free(hello_values); | |
| 351 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values); | |
| 352 | ||
| 353 | const foo_values = (try h.getValues(testing.allocator, "foo")).?; | |
| 354 | defer testing.allocator.free(foo_values); | |
| 355 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values); | |
| 356 | ||
| 357 | const bar_values = (try h.getValues(testing.allocator, "bar")).?; | |
| 358 | defer testing.allocator.free(bar_values); | |
| 359 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values); | |
| 360 | ||
| 361 | const baz_values = (try h.getValues(testing.allocator, "baz")).?; | |
| 362 | defer testing.allocator.free(baz_values); | |
| 363 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values); | |
| 364 | ||
| 365 | const pog_values = (try h.getValues(testing.allocator, "pog")); | |
| 366 | try testing.expectEqual(@as(?[]const []const u8, null), pog_values); | |
| 367 | ||
| 368 | h.sort(); | |
| 369 | ||
| 370 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?); | |
| 371 | try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?); | |
| 372 | try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?); | |
| 373 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?); | |
| 374 | ||
| 375 | const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h}); | |
| 376 | defer testing.allocator.free(formatted_values); | |
| 377 | ||
| 378 | try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values); | |
| 379 | ||
| 380 | var buf: [128]u8 = undefined; | |
| 381 | var fbs = std.io.fixedBufferStream(&buf); | |
| 382 | const writer = fbs.writer(); | |
| 383 | ||
| 384 | try h.formatCommaSeparated("foo", writer); | |
| 385 | try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten()); | |
| 386 | } |
lib/std/http/Server.zig+149-146| ... | ... | @@ -157,134 +157,120 @@ pub const BufferedConnection = struct { |
| 157 | 157 | |
| 158 | 158 | /// A HTTP request originating from a client. |
| 159 | 159 | pub const Request = struct { |
| 160 | pub const Headers = struct { | |
| 161 | method: http.Method, | |
| 162 | target: []const u8, | |
| 163 | version: http.Version, | |
| 164 | content_length: ?u64 = null, | |
| 165 | transfer_encoding: ?http.TransferEncoding = null, | |
| 166 | transfer_compression: ?http.ContentEncoding = null, | |
| 167 | connection: http.Connection = .close, | |
| 168 | host: ?[]const u8 = null, | |
| 169 | ||
| 170 | pub const ParseError = error{ | |
| 171 | ShortHttpStatusLine, | |
| 172 | BadHttpVersion, | |
| 173 | UnknownHttpMethod, | |
| 174 | HttpHeadersInvalid, | |
| 175 | HttpHeaderContinuationsUnsupported, | |
| 176 | HttpTransferEncodingUnsupported, | |
| 177 | HttpConnectionHeaderUnsupported, | |
| 178 | InvalidCharacter, | |
| 179 | }; | |
| 180 | ||
| 181 | pub fn parse(bytes: []const u8) !Headers { | |
| 182 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 183 | ||
| 184 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 185 | if (first_line.len < 10) | |
| 186 | return error.ShortHttpStatusLine; | |
| 187 | ||
| 188 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 189 | const method_str = first_line[0..method_end]; | |
| 190 | const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; | |
| 160 | pub const ParseError = Allocator.Error || error{ | |
| 161 | ShortHttpStatusLine, | |
| 162 | BadHttpVersion, | |
| 163 | UnknownHttpMethod, | |
| 164 | HttpHeadersInvalid, | |
| 165 | HttpHeaderContinuationsUnsupported, | |
| 166 | HttpTransferEncodingUnsupported, | |
| 167 | HttpConnectionHeaderUnsupported, | |
| 168 | InvalidCharacter, | |
| 169 | }; | |
| 191 | 170 | |
| 192 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 193 | if (version_start == method_end) return error.HttpHeadersInvalid; | |
| 171 | pub fn parse(req: *Request, bytes: []const u8) !void { | |
| 172 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 194 | 173 | |
| 195 | const version_str = first_line[version_start + 1 ..]; | |
| 196 | if (version_str.len != 8) return error.HttpHeadersInvalid; | |
| 197 | const version: http.Version = switch (int64(version_str[0..8])) { | |
| 198 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 199 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 200 | else => return error.BadHttpVersion, | |
| 201 | }; | |
| 174 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 175 | if (first_line.len < 10) | |
| 176 | return error.ShortHttpStatusLine; | |
| 202 | 177 | |
| 203 | const target = first_line[method_end + 1 .. version_start]; | |
| 178 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 179 | const method_str = first_line[0..method_end]; | |
| 180 | const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; | |
| 204 | 181 | |
| 205 | var headers: Headers = .{ | |
| 206 | .method = method, | |
| 207 | .target = target, | |
| 208 | .version = version, | |
| 209 | }; | |
| 182 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 183 | if (version_start == method_end) return error.HttpHeadersInvalid; | |
| 210 | 184 | |
| 211 | while (it.next()) |line| { | |
| 212 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 213 | switch (line[0]) { | |
| 214 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 215 | else => {}, | |
| 216 | } | |
| 185 | const version_str = first_line[version_start + 1 ..]; | |
| 186 | if (version_str.len != 8) return error.HttpHeadersInvalid; | |
| 187 | const version: http.Version = switch (int64(version_str[0..8])) { | |
| 188 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 189 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 190 | else => return error.BadHttpVersion, | |
| 191 | }; | |
| 217 | 192 | |
| 218 | var line_it = mem.tokenize(u8, line, ": "); | |
| 219 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 220 | const header_value = line_it.rest(); | |
| 221 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 222 | if (headers.content_length != null) return error.HttpHeadersInvalid; | |
| 223 | headers.content_length = try std.fmt.parseInt(u64, header_value, 10); | |
| 224 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 225 | // Transfer-Encoding: second, first | |
| 226 | // Transfer-Encoding: deflate, chunked | |
| 227 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 228 | ||
| 229 | if (iter.next()) |first| { | |
| 230 | const trimmed = mem.trim(u8, first, " "); | |
| 231 | ||
| 232 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 233 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 234 | headers.transfer_encoding = te; | |
| 235 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 236 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 237 | headers.transfer_compression = ce; | |
| 238 | } else { | |
| 239 | return error.HttpTransferEncodingUnsupported; | |
| 240 | } | |
| 241 | } | |
| 193 | const target = first_line[method_end + 1 .. version_start]; | |
| 242 | 194 | |
| 243 | if (iter.next()) |second| { | |
| 244 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 195 | req.method = method; | |
| 196 | req.target = target; | |
| 197 | req.version = version; | |
| 245 | 198 | |
| 246 | const trimmed = mem.trim(u8, second, " "); | |
| 199 | while (it.next()) |line| { | |
| 200 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 201 | switch (line[0]) { | |
| 202 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 203 | else => {}, | |
| 204 | } | |
| 247 | 205 | |
| 248 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 249 | headers.transfer_compression = ce; | |
| 250 | } else { | |
| 251 | return error.HttpTransferEncodingUnsupported; | |
| 252 | } | |
| 206 | var line_it = mem.tokenize(u8, line, ": "); | |
| 207 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 208 | const header_value = line_it.rest(); | |
| 209 | ||
| 210 | try req.headers.append(header_name, header_value); | |
| 211 | ||
| 212 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 213 | if (req.content_length != null) return error.HttpHeadersInvalid; | |
| 214 | req.content_length = try std.fmt.parseInt(u64, header_value, 10); | |
| 215 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 216 | // Transfer-Encoding: second, first | |
| 217 | // Transfer-Encoding: deflate, chunked | |
| 218 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 219 | ||
| 220 | if (iter.next()) |first| { | |
| 221 | const trimmed = mem.trim(u8, first, " "); | |
| 222 | ||
| 223 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 224 | if (req.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 225 | req.transfer_encoding = te; | |
| 226 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 227 | if (req.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 228 | req.transfer_compression = ce; | |
| 229 | } else { | |
| 230 | return error.HttpTransferEncodingUnsupported; | |
| 253 | 231 | } |
| 232 | } | |
| 254 | 233 | |
| 255 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 256 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 257 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 234 | if (iter.next()) |second| { | |
| 235 | if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 258 | 236 | |
| 259 | const trimmed = mem.trim(u8, header_value, " "); | |
| 237 | const trimmed = mem.trim(u8, second, " "); | |
| 260 | 238 | |
| 261 | 239 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 262 | headers.transfer_compression = ce; | |
| 240 | req.transfer_compression = ce; | |
| 263 | 241 | } else { |
| 264 | 242 | return error.HttpTransferEncodingUnsupported; |
| 265 | 243 | } |
| 266 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 267 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 268 | headers.connection = .keep_alive; | |
| 269 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 270 | headers.connection = .close; | |
| 271 | } else { | |
| 272 | return error.HttpConnectionHeaderUnsupported; | |
| 273 | } | |
| 274 | } else if (std.ascii.eqlIgnoreCase(header_name, "host")) { | |
| 275 | headers.host = header_value; | |
| 276 | 244 | } |
| 277 | } | |
| 278 | 245 | |
| 279 | return headers; | |
| 280 | } | |
| 246 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 247 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 248 | if (req.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 281 | 249 | |
| 282 | inline fn int64(array: *const [8]u8) u64 { | |
| 283 | return @bitCast(u64, array.*); | |
| 250 | const trimmed = mem.trim(u8, header_value, " "); | |
| 251 | ||
| 252 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 253 | req.transfer_compression = ce; | |
| 254 | } else { | |
| 255 | return error.HttpTransferEncodingUnsupported; | |
| 256 | } | |
| 257 | } | |
| 284 | 258 | } |
| 285 | }; | |
| 259 | } | |
| 260 | ||
| 261 | inline fn int64(array: *const [8]u8) u64 { | |
| 262 | return @bitCast(u64, array.*); | |
| 263 | } | |
| 286 | 264 | |
| 287 | headers: Headers = undefined, | |
| 265 | method: http.Method, | |
| 266 | target: []const u8, | |
| 267 | version: http.Version, | |
| 268 | ||
| 269 | content_length: ?u64 = null, | |
| 270 | transfer_encoding: ?http.TransferEncoding = null, | |
| 271 | transfer_compression: ?http.ContentEncoding = null, | |
| 272 | ||
| 273 | headers: http.Headers = undefined, | |
| 288 | 274 | parser: proto.HeadersParser, |
| 289 | 275 | compression: Compression = .none, |
| 290 | 276 | }; |
| ... | ... | @@ -295,23 +281,17 @@ pub const Request = struct { |
| 295 | 281 | /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] |
| 296 | 282 | /// \ -> read / |
| 297 | 283 | pub const Response = struct { |
| 298 | pub const Headers = struct { | |
| 299 | version: http.Version = .@"HTTP/1.1", | |
| 300 | status: http.Status = .ok, | |
| 301 | reason: ?[]const u8 = null, | |
| 284 | version: http.Version = .@"HTTP/1.1", | |
| 285 | status: http.Status = .ok, | |
| 286 | reason: ?[]const u8 = null, | |
| 302 | 287 | |
| 303 | server: ?[]const u8 = "zig (std.http)", | |
| 304 | connection: http.Connection = .keep_alive, | |
| 305 | transfer_encoding: RequestTransfer = .none, | |
| 306 | ||
| 307 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 308 | }; | |
| 288 | transfer_encoding: ResponseTransfer = .none, | |
| 309 | 289 | |
| 310 | 290 | server: *Server, |
| 311 | 291 | address: net.Address, |
| 312 | 292 | connection: BufferedConnection, |
| 313 | 293 | |
| 314 | headers: Headers = .{}, | |
| 294 | headers: http.Headers, | |
| 315 | 295 | request: Request, |
| 316 | 296 | |
| 317 | 297 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. |
| ... | ... | @@ -346,41 +326,54 @@ pub const Response = struct { |
| 346 | 326 | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 347 | 327 | const w = buffered.writer(); |
| 348 | 328 | |
| 349 | try w.writeAll(@tagName(res.headers.version)); | |
| 329 | try w.writeAll(@tagName(res.version)); | |
| 350 | 330 | try w.writeByte(' '); |
| 351 | try w.print("{d}", .{@enumToInt(res.headers.status)}); | |
| 331 | try w.print("{d}", .{@enumToInt(res.status)}); | |
| 352 | 332 | try w.writeByte(' '); |
| 353 | if (res.headers.reason) |reason| { | |
| 333 | if (res.reason) |reason| { | |
| 354 | 334 | try w.writeAll(reason); |
| 355 | } else if (res.headers.status.phrase()) |phrase| { | |
| 335 | } else if (res.status.phrase()) |phrase| { | |
| 356 | 336 | try w.writeAll(phrase); |
| 357 | 337 | } |
| 338 | try w.writeAll("\r\n"); | |
| 358 | 339 | |
| 359 | if (res.headers.server) |server| { | |
| 360 | try w.writeAll("\r\nServer: "); | |
| 361 | try w.writeAll(server); | |
| 340 | if (!res.headers.contains("server")) { | |
| 341 | try w.writeAll("Server: zig (std.http)\r\n"); | |
| 362 | 342 | } |
| 363 | 343 | |
| 364 | if (res.headers.connection == .close) { | |
| 365 | try w.writeAll("\r\nConnection: close"); | |
| 366 | } else { | |
| 367 | try w.writeAll("\r\nConnection: keep-alive"); | |
| 344 | if (!res.headers.contains("connection")) { | |
| 345 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 368 | 346 | } |
| 369 | 347 | |
| 370 | switch (res.headers.transfer_encoding) { | |
| 371 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), | |
| 372 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), | |
| 373 | .none => {}, | |
| 374 | } | |
| 348 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); | |
| 349 | const has_content_length = res.headers.contains("content-length"); | |
| 375 | 350 | |
| 376 | for (res.headers.custom) |header| { | |
| 377 | try w.writeAll("\r\n"); | |
| 378 | try w.writeAll(header.name); | |
| 379 | try w.writeAll(": "); | |
| 380 | try w.writeAll(header.value); | |
| 351 | if (!has_transfer_encoding and !has_content_length) { | |
| 352 | switch (res.transfer_encoding) { | |
| 353 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | |
| 354 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | |
| 355 | .none => {}, | |
| 356 | } | |
| 357 | } else { | |
| 358 | if (has_content_length) { | |
| 359 | const content_length = try std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10); | |
| 360 | ||
| 361 | res.transfer_encoding = .{ .content_length = content_length }; | |
| 362 | } else if (has_transfer_encoding) { | |
| 363 | const transfer_encoding = res.headers.getFirstValue("content-length").?; | |
| 364 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | |
| 365 | res.transfer_encoding = .chunked; | |
| 366 | } else { | |
| 367 | return error.UnsupportedTransferEncoding; | |
| 368 | } | |
| 369 | } else { | |
| 370 | res.transfer_encoding = .none; | |
| 371 | } | |
| 381 | 372 | } |
| 382 | 373 | |
| 383 | try w.writeAll("\r\n\r\n"); | |
| 374 | try w.print("{}", .{res.headers}); | |
| 375 | ||
| 376 | try w.writeAll("\r\n"); | |
| 384 | 377 | |
| 385 | 378 | try buffered.flush(); |
| 386 | 379 | } |
| ... | ... | @@ -419,22 +412,28 @@ pub const Response = struct { |
| 419 | 412 | if (res.request.parser.state.isContent()) break; |
| 420 | 413 | } |
| 421 | 414 | |
| 422 | res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items); | |
| 415 | res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; | |
| 416 | try res.request.parse(res.request.parser.header_bytes.items); | |
| 423 | 417 | |
| 424 | if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) { | |
| 418 | const res_connection = res.headers.getFirstValue("connection"); | |
| 419 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 420 | ||
| 421 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 422 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 423 | if (res_keepalive and req_keepalive) { | |
| 425 | 424 | res.connection.conn.closing = false; |
| 426 | 425 | } else { |
| 427 | 426 | res.connection.conn.closing = true; |
| 428 | 427 | } |
| 429 | 428 | |
| 430 | if (res.request.headers.transfer_encoding) |te| { | |
| 429 | if (res.request.transfer_encoding) |te| { | |
| 431 | 430 | switch (te) { |
| 432 | 431 | .chunked => { |
| 433 | 432 | res.request.parser.next_chunk_length = 0; |
| 434 | 433 | res.request.parser.state = .chunk_head_size; |
| 435 | 434 | }, |
| 436 | 435 | } |
| 437 | } else if (res.request.headers.content_length) |cl| { | |
| 436 | } else if (res.request.content_length) |cl| { | |
| 438 | 437 | res.request.parser.next_chunk_length = cl; |
| 439 | 438 | |
| 440 | 439 | if (cl == 0) res.request.parser.done = true; |
| ... | ... | @@ -443,7 +442,7 @@ pub const Response = struct { |
| 443 | 442 | } |
| 444 | 443 | |
| 445 | 444 | if (!res.request.parser.done) { |
| 446 | if (res.request.headers.transfer_compression) |tc| switch (tc) { | |
| 445 | if (res.request.transfer_compression) |tc| switch (tc) { | |
| 447 | 446 | .compress => return error.CompressionNotSupported, |
| 448 | 447 | .deflate => res.request.compression = .{ |
| 449 | 448 | .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), |
| ... | ... | @@ -495,7 +494,7 @@ pub const Response = struct { |
| 495 | 494 | |
| 496 | 495 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 497 | 496 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 498 | switch (res.headers.transfer_encoding) { | |
| 497 | switch (res.transfer_encoding) { | |
| 499 | 498 | .chunked => { |
| 500 | 499 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| 501 | 500 | try res.connection.writeAll(bytes); |
| ... | ... | @@ -525,7 +524,7 @@ pub const Response = struct { |
| 525 | 524 | }; |
| 526 | 525 | |
| 527 | 526 | /// The mode of transport for responses. |
| 528 | pub const RequestTransfer = union(enum) { | |
| 527 | pub const ResponseTransfer = union(enum) { | |
| 529 | 528 | content_length: u64, |
| 530 | 529 | chunked: void, |
| 531 | 530 | none: void, |
| ... | ... | @@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { |
| 588 | 587 | .stream = in.stream, |
| 589 | 588 | .protocol = .plain, |
| 590 | 589 | } }, |
| 590 | .headers = .{ .allocator = server.allocator }, | |
| 591 | 591 | .request = .{ |
| 592 | .version = undefined, | |
| 593 | .method = undefined, | |
| 594 | .target = undefined, | |
| 592 | 595 | .parser = switch (options) { |
| 593 | 596 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 594 | 597 | .static => |buf| proto.HeadersParser.initStatic(buf), |
src/Package.zig+4-1| ... | ... | @@ -479,7 +479,10 @@ fn fetchAndUnpack( |
| 479 | 479 | }; |
| 480 | 480 | defer tmp_directory.closeAndFree(gpa); |
| 481 | 481 | |
| 482 | var req = try http_client.request(uri, .{}, .{}); | |
| 482 | var h = std.http.Headers{ .allocator = gpa }; | |
| 483 | defer h.deinit(); | |
| 484 | ||
| 485 | var req = try http_client.request(uri, h, .{ .method = .GET }); | |
| 483 | 486 | defer req.deinit(); |
| 484 | 487 | |
| 485 | 488 | try req.do(); |