| ... | ... | @@ -14,6 +14,7 @@ const Server = @This(); |
| 14 | 14 | /// The reader's buffer must be large enough to store the client's entire HTTP |
| 15 | 15 | /// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. |
| 16 | 16 | in: *std.io.BufferedReader, |
| 17 | /// Data from the HTTP server to the HTTP client. |
| 17 | 18 | out: *std.io.BufferedWriter, |
| 18 | 19 | /// Keeps track of whether the Server is ready to accept a new request on the |
| 19 | 20 | /// same connection, and makes invalid API usage cause assertion failures |
| ... | ... | @@ -479,12 +480,6 @@ pub const Request = struct { |
| 479 | 480 | } |
| 480 | 481 | |
| 481 | 482 | pub const RespondStreamingOptions = struct { |
| 482 | | /// An externally managed slice of memory used to batch bytes before |
| 483 | | /// sending. `respondStreaming` asserts this is large enough to store |
| 484 | | /// the full HTTP response head. |
| 485 | | /// |
| 486 | | /// Must outlive the returned Response. |
| 487 | | send_buffer: []u8, |
| 488 | 483 | /// If provided, the response will use the content-length header; |
| 489 | 484 | /// otherwise it will use transfer-encoding: chunked. |
| 490 | 485 | content_length: ?u64 = null, |
| ... | ... | @@ -492,7 +487,7 @@ pub const Request = struct { |
| 492 | 487 | respond_options: RespondOptions = .{}, |
| 493 | 488 | }; |
| 494 | 489 | |
| 495 | | /// The header is buffered but not sent until Response.flush is called. |
| 490 | /// The header is buffered but not sent until `Response.flush` is called. |
| 496 | 491 | /// |
| 497 | 492 | /// If the request contains a body and the connection is to be reused, |
| 498 | 493 | /// discards the request body, leaving the Server in the `ready` state. If |
| ... | ... | @@ -504,69 +499,63 @@ pub const Request = struct { |
| 504 | 499 | /// that flag and skipping any expensive work that would otherwise need to |
| 505 | 500 | /// be done to satisfy the request. |
| 506 | 501 | /// |
| 507 | | /// Asserts `send_buffer` is large enough to store the entire response header. |
| 508 | 502 | /// Asserts status is not `continue`. |
| 509 | | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response { |
| 503 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) std.io.Writer.Error!Response { |
| 510 | 504 | const o = options.respond_options; |
| 511 | 505 | assert(o.status != .@"continue"); |
| 512 | 506 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; |
| 513 | 507 | const server_keep_alive = !transfer_encoding_none and o.keep_alive; |
| 514 | 508 | const keep_alive = request.discardBody(server_keep_alive); |
| 515 | 509 | const phrase = o.reason orelse o.status.phrase() orelse ""; |
| 516 | | |
| 517 | | var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer); |
| 510 | const out = request.server.out; |
| 518 | 511 | |
| 519 | 512 | const elide_body = if (request.head.expect != null) eb: { |
| 520 | 513 | // reader() and hence discardBody() above sets expect to null if it |
| 521 | 514 | // is handled. So the fact that it is not null here means unhandled. |
| 522 | | h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n"); |
| 523 | | if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"); |
| 524 | | h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n"); |
| 515 | try out.writeAll("HTTP/1.1 417 Expectation Failed\r\n"); |
| 516 | if (!keep_alive) try out.writeAll("connection: close\r\n"); |
| 517 | try out.writeAll("content-length: 0\r\n\r\n"); |
| 525 | 518 | break :eb true; |
| 526 | 519 | } else eb: { |
| 527 | | h.printAssumeCapacity("{s} {d} {s}\r\n", .{ |
| 520 | try out.print("{s} {d} {s}\r\n", .{ |
| 528 | 521 | @tagName(o.version), @intFromEnum(o.status), phrase, |
| 529 | 522 | }); |
| 530 | 523 | |
| 531 | 524 | switch (o.version) { |
| 532 | | .@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"), |
| 533 | | .@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"), |
| 525 | .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), |
| 526 | .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), |
| 534 | 527 | } |
| 535 | 528 | |
| 536 | 529 | if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { |
| 537 | | .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"), |
| 530 | .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), |
| 538 | 531 | .none => {}, |
| 539 | 532 | } else if (options.content_length) |len| { |
| 540 | | h.printAssumeCapacity("content-length: {d}\r\n", .{len}); |
| 533 | try out.print("content-length: {d}\r\n", .{len}); |
| 541 | 534 | } else { |
| 542 | | h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"); |
| 535 | try out.writeAll("transfer-encoding: chunked\r\n"); |
| 543 | 536 | } |
| 544 | 537 | |
| 545 | 538 | for (o.extra_headers) |header| { |
| 546 | 539 | assert(header.name.len != 0); |
| 547 | | h.appendSliceAssumeCapacity(header.name); |
| 548 | | h.appendSliceAssumeCapacity(": "); |
| 549 | | h.appendSliceAssumeCapacity(header.value); |
| 550 | | h.appendSliceAssumeCapacity("\r\n"); |
| 540 | try out.writeAll(header.name); |
| 541 | try out.writeAll(": "); |
| 542 | try out.writeAll(header.value); |
| 543 | try out.writeAll("\r\n"); |
| 551 | 544 | } |
| 552 | 545 | |
| 553 | | h.appendSliceAssumeCapacity("\r\n"); |
| 546 | try out.writeAll("\r\n"); |
| 554 | 547 | break :eb request.head.method == .HEAD; |
| 555 | 548 | }; |
| 556 | 549 | |
| 557 | 550 | return .{ |
| 558 | | .out = request.server.out, |
| 559 | | .send_buffer = options.send_buffer, |
| 560 | | .send_buffer_start = 0, |
| 561 | | .send_buffer_end = h.items.len, |
| 551 | .server_output = request.server.out, |
| 562 | 552 | .transfer_encoding = if (o.transfer_encoding) |te| switch (te) { |
| 563 | | .chunked => .chunked, |
| 553 | .chunked => .{ .chunked = .init }, |
| 564 | 554 | .none => .none, |
| 565 | 555 | } else if (options.content_length) |len| .{ |
| 566 | 556 | .content_length = len, |
| 567 | | } else .chunked, |
| 557 | } else .{ .chunked = .init }, |
| 568 | 558 | .elide_body = elide_body, |
| 569 | | .chunk_len = 0, |
| 570 | 559 | }; |
| 571 | 560 | } |
| 572 | 561 | |
| ... | ... | @@ -836,20 +825,32 @@ pub const Request = struct { |
| 836 | 825 | }; |
| 837 | 826 | |
| 838 | 827 | pub const Response = struct { |
| 839 | | out: *std.io.BufferedWriter, |
| 840 | | send_buffer: []u8, |
| 841 | | /// Index of the first byte in `send_buffer`. |
| 842 | | /// This is 0 unless a short write happens in `write`. |
| 843 | | send_buffer_start: usize, |
| 844 | | /// Index of the last byte + 1 in `send_buffer`. |
| 845 | | send_buffer_end: usize, |
| 828 | /// HTTP protocol to the client. |
| 829 | /// |
| 830 | /// This is the underlying stream; use `buffered` to create a |
| 831 | /// `BufferedWriter` for this `Response`. |
| 832 | server_output: *std.io.BufferedWriter, |
| 846 | 833 | /// `null` means transfer-encoding: chunked. |
| 847 | 834 | /// As a debugging utility, counts down to zero as bytes are written. |
| 848 | 835 | transfer_encoding: TransferEncoding, |
| 849 | 836 | elide_body: bool, |
| 850 | | /// Indicates how much of the end of the `send_buffer` corresponds to a |
| 851 | | /// chunk. This amount of data will be wrapped by an HTTP chunk header. |
| 852 | | chunk_len: usize, |
| 837 | err: Error!void = {}, |
| 838 | |
| 839 | pub const Error = error{ |
| 840 | /// Attempted to write a file to the stream, an expensive operation |
| 841 | /// that should be avoided when `elide_body` is true. |
| 842 | UnableToElideBody, |
| 843 | }; |
| 844 | pub const WriteError = std.io.Writer.Error; |
| 845 | |
| 846 | /// How many zeroes to reserve for hex-encoded chunk length. |
| 847 | const chunk_len_digits = 8; |
| 848 | const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; |
| 849 | const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; |
| 850 | |
| 851 | comptime { |
| 852 | assert(max_chunk_len == std.math.maxInt(u32)); |
| 853 | } |
| 853 | 854 | |
| 854 | 855 | pub const TransferEncoding = union(enum) { |
| 855 | 856 | /// End of connection signals the end of the stream. |
| ... | ... | @@ -857,7 +858,19 @@ pub const Response = struct { |
| 857 | 858 | /// As a debugging utility, counts down to zero as bytes are written. |
| 858 | 859 | content_length: u64, |
| 859 | 860 | /// Each chunk is wrapped in a header and trailer. |
| 860 | | chunked, |
| 861 | chunked: Chunked, |
| 862 | |
| 863 | pub const Chunked = union(enum) { |
| 864 | /// Index of the hex-encoded chunk length in the chunk header |
| 865 | /// within the buffer of `Response.server_output`. |
| 866 | offset: usize, |
| 867 | /// We are in the middle of a chunk and this is how many bytes are |
| 868 | /// left until the next header. This includes +2 for "\r"\n", and |
| 869 | /// is zero for the beginning of the stream. |
| 870 | chunk_len: usize, |
| 871 | |
| 872 | pub const init: Chunked = .{ .chunk_len = 0 }; |
| 873 | }; |
| 861 | 874 | }; |
| 862 | 875 | |
| 863 | 876 | /// When using content-length, asserts that the amount of data sent matches |
| ... | ... | @@ -865,17 +878,17 @@ pub const Response = struct { |
| 865 | 878 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the |
| 866 | 879 | /// end-of-stream message, then flushes the stream to the system. |
| 867 | 880 | /// Respects the value of `elide_body` to omit all data after the headers. |
| 868 | | pub fn end(r: *Response) std.io.Writer.Error!void { |
| 881 | pub fn end(r: *Response) WriteError!void { |
| 869 | 882 | switch (r.transfer_encoding) { |
| 870 | 883 | .content_length => |len| { |
| 871 | 884 | assert(len == 0); // Trips when end() called before all bytes written. |
| 872 | | try flush_cl(r); |
| 885 | try flushContentLength(r); |
| 873 | 886 | }, |
| 874 | 887 | .none => { |
| 875 | | try flush_cl(r); |
| 888 | try flushContentLength(r); |
| 876 | 889 | }, |
| 877 | 890 | .chunked => { |
| 878 | | try flush_chunked(r, &.{}); |
| 891 | try flushChunked(r, &.{}); |
| 879 | 892 | }, |
| 880 | 893 | } |
| 881 | 894 | r.* = undefined; |
| ... | ... | @@ -890,9 +903,9 @@ pub const Response = struct { |
| 890 | 903 | /// flushes the stream to the system. |
| 891 | 904 | /// Respects the value of `elide_body` to omit all data after the headers. |
| 892 | 905 | /// Asserts there are at most 25 trailers. |
| 893 | | pub fn endChunked(r: *Response, options: EndChunkedOptions) std.io.Writer.Error!void { |
| 906 | pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void { |
| 894 | 907 | assert(r.transfer_encoding == .chunked); |
| 895 | | try flush_chunked(r, options.trailers); |
| 908 | try flushChunked(r, options.trailers); |
| 896 | 909 | r.* = undefined; |
| 897 | 910 | } |
| 898 | 911 | |
| ... | ... | @@ -900,163 +913,222 @@ pub const Response = struct { |
| 900 | 913 | /// would not exceed the content-length value sent in the HTTP header. |
| 901 | 914 | /// May return 0, which does not indicate end of stream. The caller decides |
| 902 | 915 | /// when the end of stream occurs by calling `end`. |
| 903 | | pub fn write(r: *Response, bytes: []const u8) std.io.Writer.Error!usize { |
| 916 | pub fn write(r: *Response, bytes: []const u8) WriteError!usize { |
| 904 | 917 | switch (r.transfer_encoding) { |
| 905 | | .content_length, .none => return cl_writeSplat(r, &.{bytes}, 1), |
| 906 | | .chunked => return chunked_writeSplat(r, &.{bytes}, 1), |
| 918 | .content_length, .none => return contentLengthWriteSplat(r, &.{bytes}, 1), |
| 919 | .chunked => return chunkedWriteSplat(r, &.{bytes}, 1), |
| 920 | } |
| 921 | } |
| 922 | |
| 923 | fn contentLengthWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { |
| 924 | const r: *Response = @alignCast(@ptrCast(context)); |
| 925 | const n = if (r.elide_body) countSplat(data, splat) else try r.server_output.writeSplat(data, splat); |
| 926 | r.transfer_encoding.content_length -= n; |
| 927 | return n; |
| 928 | } |
| 929 | |
| 930 | fn noneWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { |
| 931 | const r: *Response = @alignCast(@ptrCast(context)); |
| 932 | if (r.elide_body) return countSplat(data, splat); |
| 933 | return r.server_output.writeSplat(data, splat); |
| 934 | } |
| 935 | |
| 936 | fn countSplat(data: []const []const u8, splat: usize) usize { |
| 937 | if (data.len == 0) return 0; |
| 938 | var total: usize = 0; |
| 939 | for (data[0 .. data.len - 1]) |buf| total += buf.len; |
| 940 | total += data[data.len - 1].len * splat; |
| 941 | return total; |
| 942 | } |
| 943 | |
| 944 | fn elideWriteFile( |
| 945 | r: *Response, |
| 946 | offset: std.io.Writer.Offset, |
| 947 | limit: std.io.Writer.Limit, |
| 948 | headers_and_trailers: []const []const u8, |
| 949 | ) WriteError!usize { |
| 950 | if (offset != .none) { |
| 951 | if (countWriteFile(limit, headers_and_trailers)) |n| { |
| 952 | return n; |
| 953 | } |
| 907 | 954 | } |
| 955 | r.err = error.UnableToElideBody; |
| 956 | return error.WriteFailed; |
| 908 | 957 | } |
| 909 | 958 | |
| 910 | | fn cl_writeSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { |
| 911 | | _ = splat; |
| 912 | | return cl_write(context, data[0]); // TODO: try to send all the data |
| 959 | /// Returns `null` if size cannot be computed without making any syscalls. |
| 960 | fn countWriteFile(limit: std.io.Writer.Limit, headers_and_trailers: []const []const u8) ?usize { |
| 961 | var total: usize = limit.toInt() orelse return null; |
| 962 | for (headers_and_trailers) |buf| total += buf.len; |
| 963 | return total; |
| 913 | 964 | } |
| 914 | 965 | |
| 915 | | fn cl_writeFile( |
| 966 | fn noneWriteFile( |
| 916 | 967 | context: ?*anyopaque, |
| 917 | 968 | file: std.fs.File, |
| 918 | 969 | offset: std.io.Writer.Offset, |
| 919 | 970 | limit: std.io.Writer.Limit, |
| 920 | 971 | headers_and_trailers: []const []const u8, |
| 921 | 972 | headers_len: usize, |
| 922 | | ) std.io.Writer.Error!usize { |
| 923 | | _ = context; |
| 924 | | _ = file; |
| 925 | | _ = offset; |
| 926 | | _ = limit; |
| 927 | | _ = headers_and_trailers; |
| 928 | | _ = headers_len; |
| 929 | | @panic("TODO"); |
| 930 | | } |
| 931 | | |
| 932 | | fn cl_write(context: ?*anyopaque, bytes: []const u8) std.io.Writer.Error!usize { |
| 973 | ) std.io.Writer.FileError!usize { |
| 974 | if (limit == .nothing) return noneWriteSplat(context, headers_and_trailers, 1); |
| 933 | 975 | const r: *Response = @alignCast(@ptrCast(context)); |
| 934 | | |
| 935 | | var trash: u64 = std.math.maxInt(u64); |
| 936 | | const len = switch (r.transfer_encoding) { |
| 937 | | .content_length => |*len| len, |
| 938 | | else => &trash, |
| 939 | | }; |
| 940 | | |
| 941 | | if (r.elide_body) { |
| 942 | | len.* -= bytes.len; |
| 943 | | return bytes.len; |
| 944 | | } |
| 945 | | |
| 946 | | if (bytes.len + r.send_buffer_end > r.send_buffer.len) { |
| 947 | | const send_buffer_len = r.send_buffer_end - r.send_buffer_start; |
| 948 | | var iovecs: [2][]const u8 = .{ |
| 949 | | r.send_buffer[r.send_buffer_start..][0..send_buffer_len], |
| 950 | | bytes, |
| 951 | | }; |
| 952 | | const n = try r.out.writeVec(&iovecs); |
| 953 | | |
| 954 | | if (n >= send_buffer_len) { |
| 955 | | // It was enough to reset the buffer. |
| 956 | | r.send_buffer_start = 0; |
| 957 | | r.send_buffer_end = 0; |
| 958 | | const bytes_n = n - send_buffer_len; |
| 959 | | len.* -= bytes_n; |
| 960 | | return bytes_n; |
| 961 | | } |
| 962 | | |
| 963 | | // It didn't even make it through the existing buffer, let |
| 964 | | // alone the new bytes provided. |
| 965 | | r.send_buffer_start += n; |
| 966 | | return 0; |
| 967 | | } |
| 968 | | |
| 969 | | // All bytes can be stored in the remaining space of the buffer. |
| 970 | | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); |
| 971 | | r.send_buffer_end += bytes.len; |
| 972 | | len.* -= bytes.len; |
| 973 | | return bytes.len; |
| 976 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); |
| 977 | return r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); |
| 974 | 978 | } |
| 975 | 979 | |
| 976 | | fn chunked_writeSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { |
| 977 | | _ = splat; |
| 978 | | return chunked_write(context, data[0]); // TODO: try to send all the data |
| 980 | fn contentLengthWriteFile( |
| 981 | context: ?*anyopaque, |
| 982 | file: std.fs.File, |
| 983 | offset: std.io.Writer.Offset, |
| 984 | limit: std.io.Writer.Limit, |
| 985 | headers_and_trailers: []const []const u8, |
| 986 | headers_len: usize, |
| 987 | ) std.io.Writer.FileError!usize { |
| 988 | if (limit == .nothing) return contentLengthWriteSplat(context, headers_and_trailers, 1); |
| 989 | const r: *Response = @alignCast(@ptrCast(context)); |
| 990 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); |
| 991 | const n = try r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); |
| 992 | r.transfer_encoding.content_length -= n; |
| 993 | return n; |
| 979 | 994 | } |
| 980 | 995 | |
| 981 | | fn chunked_writeFile( |
| 996 | fn chunkedWriteFile( |
| 982 | 997 | context: ?*anyopaque, |
| 983 | 998 | file: std.fs.File, |
| 984 | 999 | offset: std.io.Writer.Offset, |
| 985 | 1000 | limit: std.io.Writer.Limit, |
| 986 | 1001 | headers_and_trailers: []const []const u8, |
| 987 | 1002 | headers_len: usize, |
| 988 | | ) std.io.Writer.Error!usize { |
| 989 | | _ = context; |
| 990 | | _ = file; |
| 991 | | _ = offset; |
| 992 | | _ = limit; |
| 993 | | _ = headers_and_trailers; |
| 994 | | _ = headers_len; |
| 995 | | @panic("TODO"); // TODO lower to a call to writeFile on the output |
| 1003 | ) std.io.Writer.FileError!usize { |
| 1004 | if (limit == .nothing) return chunkedWriteSplat(context, headers_and_trailers, 1); |
| 1005 | const r: *Response = @alignCast(@ptrCast(context)); |
| 1006 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); |
| 1007 | const data_len = countWriteFile(limit, headers_and_trailers) orelse @panic("TODO"); |
| 1008 | const bw = r.server_output; |
| 1009 | const chunked = &r.transfer_encoding.chunked; |
| 1010 | state: switch (chunked.*) { |
| 1011 | .offset => |off| { |
| 1012 | // TODO: is it better perf to read small files into the buffer? |
| 1013 | const buffered_len = bw.end - off - chunk_header_template.len; |
| 1014 | const chunk_len = data_len + buffered_len; |
| 1015 | writeHex(bw.buffer[off..][0..chunk_len_digits], chunk_len); |
| 1016 | const n = try bw.writeFile(file, offset, limit, headers_and_trailers, headers_len); |
| 1017 | chunked.* = .{ .chunk_len = data_len + 2 - n }; |
| 1018 | return n; |
| 1019 | }, |
| 1020 | .chunk_len => |chunk_len| { |
| 1021 | l: switch (chunk_len) { |
| 1022 | 0 => { |
| 1023 | const header_buf = try bw.writableArray(chunk_header_template.len); |
| 1024 | const off = bw.end; |
| 1025 | @memcpy(header_buf, chunk_header_template); |
| 1026 | chunked.* = .{ .offset = off }; |
| 1027 | continue :state .{ .offset = off }; |
| 1028 | }, |
| 1029 | 1 => { |
| 1030 | try bw.writeByte('\n'); |
| 1031 | chunked.chunk_len = 0; |
| 1032 | continue :l 0; |
| 1033 | }, |
| 1034 | 2 => { |
| 1035 | try bw.writeByte('\r'); |
| 1036 | chunked.chunk_len = 1; |
| 1037 | continue :l 1; |
| 1038 | }, |
| 1039 | else => { |
| 1040 | const new_limit = limit.min(.limited(chunk_len - 2)); |
| 1041 | const n = try bw.writeFile(file, offset, new_limit, headers_and_trailers, headers_len); |
| 1042 | chunked.chunk_len = chunk_len - n; |
| 1043 | return n; |
| 1044 | }, |
| 1045 | } |
| 1046 | }, |
| 1047 | } |
| 996 | 1048 | } |
| 997 | 1049 | |
| 998 | | fn chunked_write(context: ?*anyopaque, bytes: []const u8) std.io.Writer.Error!usize { |
| 1050 | fn chunkedWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { |
| 999 | 1051 | const r: *Response = @alignCast(@ptrCast(context)); |
| 1000 | | assert(r.transfer_encoding == .chunked); |
| 1052 | const data_len = countSplat(data, splat); |
| 1053 | if (r.elide_body) return data_len; |
| 1001 | 1054 | |
| 1002 | | if (r.elide_body) |
| 1003 | | return bytes.len; |
| 1004 | | |
| 1005 | | if (bytes.len + r.send_buffer_end > r.send_buffer.len) { |
| 1006 | | const send_buffer_len = r.send_buffer_end - r.send_buffer_start; |
| 1007 | | const chunk_len = r.chunk_len + bytes.len; |
| 1008 | | var header_buf: [18]u8 = undefined; |
| 1009 | | const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{chunk_len}) catch unreachable; |
| 1010 | | |
| 1011 | | var iovecs: [5][]const u8 = .{ |
| 1012 | | r.send_buffer[r.send_buffer_start .. send_buffer_len - r.chunk_len], |
| 1013 | | chunk_header, |
| 1014 | | r.send_buffer[r.send_buffer_end - r.chunk_len ..][0..r.chunk_len], |
| 1015 | | bytes, |
| 1016 | | "\r\n", |
| 1017 | | }; |
| 1018 | | // TODO make this writev instead of writevAll, which involves |
| 1019 | | // complicating the logic of this function. |
| 1020 | | try r.out.writeVecAll(&iovecs); |
| 1021 | | r.send_buffer_start = 0; |
| 1022 | | r.send_buffer_end = 0; |
| 1023 | | r.chunk_len = 0; |
| 1024 | | return bytes.len; |
| 1025 | | } |
| 1055 | const bw = r.server_output; |
| 1056 | const chunked = &r.transfer_encoding.chunked; |
| 1026 | 1057 | |
| 1027 | | // All bytes can be stored in the remaining space of the buffer. |
| 1028 | | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); |
| 1029 | | r.send_buffer_end += bytes.len; |
| 1030 | | r.chunk_len += bytes.len; |
| 1031 | | return bytes.len; |
| 1058 | state: switch (chunked.*) { |
| 1059 | .offset => |offset| { |
| 1060 | if (bw.unusedCapacitySlice().len >= data_len) { |
| 1061 | assert(data_len == (bw.writeSplat(data, splat) catch unreachable)); |
| 1062 | return data_len; |
| 1063 | } |
| 1064 | const buffered_len = bw.end - offset - chunk_header_template.len; |
| 1065 | const chunk_len = data_len + buffered_len; |
| 1066 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); |
| 1067 | const n = try bw.writeSplat(data, splat); |
| 1068 | chunked.* = .{ .chunk_len = data_len + 2 - n }; |
| 1069 | return n; |
| 1070 | }, |
| 1071 | .chunk_len => |chunk_len| { |
| 1072 | l: switch (chunk_len) { |
| 1073 | 0 => { |
| 1074 | const header_buf = try bw.writableArray(chunk_header_template.len); |
| 1075 | const offset = bw.end; |
| 1076 | @memcpy(header_buf, chunk_header_template); |
| 1077 | chunked.* = .{ .offset = offset }; |
| 1078 | continue :state .{ .offset = offset }; |
| 1079 | }, |
| 1080 | 1 => { |
| 1081 | try bw.writeByte('\n'); |
| 1082 | chunked.chunk_len = 0; |
| 1083 | continue :l 0; |
| 1084 | }, |
| 1085 | 2 => { |
| 1086 | try bw.writeByte('\r'); |
| 1087 | chunked.chunk_len = 1; |
| 1088 | continue :l 1; |
| 1089 | }, |
| 1090 | else => { |
| 1091 | const n = try bw.writeSplatLimit(data, splat, .limited(chunk_len - 2)); |
| 1092 | chunked.chunk_len = chunk_len - n; |
| 1093 | return n; |
| 1094 | }, |
| 1095 | } |
| 1096 | }, |
| 1097 | } |
| 1032 | 1098 | } |
| 1033 | 1099 | |
| 1034 | | /// If using content-length, asserts that writing these bytes to the client |
| 1035 | | /// would not exceed the content-length value sent in the HTTP header. |
| 1036 | | pub fn writeAll(r: *Response, bytes: []const u8) std.io.Writer.Error!void { |
| 1037 | | var index: usize = 0; |
| 1038 | | while (index < bytes.len) { |
| 1039 | | index += try write(r, bytes[index..]); |
| 1100 | /// Writes an integer as base 16 to `buf`, right-aligned, assuming the |
| 1101 | /// buffer has already been filled with zeroes. |
| 1102 | fn writeHex(buf: []u8, x: usize) void { |
| 1103 | assert(std.mem.allEqual(u8, buf, '0')); |
| 1104 | const base = 16; |
| 1105 | var index: usize = buf.len; |
| 1106 | var a = x; |
| 1107 | while (a > 0) { |
| 1108 | const digit = a % base; |
| 1109 | index -= 1; |
| 1110 | buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); |
| 1111 | a /= base; |
| 1040 | 1112 | } |
| 1041 | 1113 | } |
| 1042 | 1114 | |
| 1043 | 1115 | /// Sends all buffered data to the client. |
| 1044 | 1116 | /// This is redundant after calling `end`. |
| 1045 | 1117 | /// Respects the value of `elide_body` to omit all data after the headers. |
| 1046 | | pub fn flush(r: *Response) std.io.Writer.Error!void { |
| 1118 | pub fn flush(r: *Response) Error!void { |
| 1047 | 1119 | switch (r.transfer_encoding) { |
| 1048 | | .none, .content_length => return flush_cl(r), |
| 1049 | | .chunked => return flush_chunked(r, null), |
| 1120 | .none, .content_length => return flushContentLength(r), |
| 1121 | .chunked => return flushChunked(r, null), |
| 1050 | 1122 | } |
| 1051 | 1123 | } |
| 1052 | 1124 | |
| 1053 | | fn flush_cl(r: *Response) std.io.Writer.Error!void { |
| 1125 | fn flushContentLength(r: *Response) Error!void { |
| 1054 | 1126 | try r.out.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]); |
| 1055 | 1127 | r.send_buffer_start = 0; |
| 1056 | 1128 | r.send_buffer_end = 0; |
| 1057 | 1129 | } |
| 1058 | 1130 | |
| 1059 | | fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) std.io.Writer.Error!void { |
| 1131 | fn flushChunked(r: *Response, end_trailers: ?[]const http.Header) Error!void { |
| 1060 | 1132 | const max_trailers = 25; |
| 1061 | 1133 | if (end_trailers) |trailers| assert(trailers.len <= max_trailers); |
| 1062 | 1134 | assert(r.transfer_encoding == .chunked); |
| ... | ... | @@ -1123,17 +1195,21 @@ pub const Response = struct { |
| 1123 | 1195 | |
| 1124 | 1196 | pub fn writer(r: *Response) std.io.Writer { |
| 1125 | 1197 | return .{ |
| 1198 | .context = r, |
| 1126 | 1199 | .vtable = switch (r.transfer_encoding) { |
| 1127 | | .none, .content_length => &.{ |
| 1128 | | .writeSplat = cl_writeSplat, |
| 1129 | | .writeFile = cl_writeFile, |
| 1200 | .none => &.{ |
| 1201 | .writeSplat = noneWriteSplat, |
| 1202 | .writeFile = noneWriteFile, |
| 1203 | }, |
| 1204 | .content_length => &.{ |
| 1205 | .writeSplat = contentLengthWriteSplat, |
| 1206 | .writeFile = contentLengthWriteFile, |
| 1130 | 1207 | }, |
| 1131 | 1208 | .chunked => &.{ |
| 1132 | | .writeSplat = chunked_writeSplat, |
| 1133 | | .writeFile = chunked_writeFile, |
| 1209 | .writeSplat = chunkedWriteSplat, |
| 1210 | .writeFile = chunkedWriteFile, |
| 1134 | 1211 | }, |
| 1135 | 1212 | }, |
| 1136 | | .context = r, |
| 1137 | 1213 | }; |
| 1138 | 1214 | } |
| 1139 | 1215 | }; |