std.http.Server: rework the API entirely

Mainly, this removes the poorly named `wait`, `send`, `finish`
functions, which all operated on the same "Response" object, which was
actually being used as the request.

Now, it looks like this:
1. std.net.Server.accept() gives you a std.net.Server.Connection
2. std.http.Server.init() with the connection
3. Server.receiveHead() gives you a Request
4. Request.reader() gives you a body reader
5. Request.respond() is a one-shot, or Request.respondStreaming() creates
   a Response
6. Response.writer() gives you a body writer
7. Response.end() finishes the response; Response.endChunked() allows
   passing response trailers.

In other words, the type system now guides the API user down the correct
path.

receiveHead allows extra bytes to be read into the read buffer, and then
will reuse those bytes for the body or the next request upon connection
reuse.

respond(), the one-shot function, will send the entire response in one
syscall.

Streaming response bodies no longer wastefully wraps every call to write
with a chunk header and trailer; instead it only sends the HTTP chunk
wrapper when flushing. This means the user can still control when it
happens but it also does not add unnecessary chunks.

Empirically, in my example project that uses this API, the usage code is
significantly less noisy, it has less error handling while handling
errors more correctly, it's more obvious what is happening, and it is
syscall-optimal.

Additionally:
* Uncouple std.http.HeadParser from protocol.zig
* Delete std.Server.Connection; use std.net.Server.Connection instead.
  - The API user supplies the read buffer when initializing the
    http.Server, and it is used for the HTTP head as well as a buffer
    for reading the body into.
* Replace and document the State enum. No longer is there both "start"
  and "first".
This commit is contained in:
Andrew Kelley 2024-02-20 03:30:51 -07:00
parent 9129fb28dc
commit 6395ba852a
6 changed files with 1150 additions and 1025 deletions

View File

@ -3,6 +3,7 @@ const std = @import("std.zig");
pub const Client = @import("http/Client.zig");
pub const Server = @import("http/Server.zig");
pub const protocol = @import("http/protocol.zig");
pub const HeadParser = @import("http/HeadParser.zig");
pub const Version = enum {
@"HTTP/1.0",
@ -311,5 +312,6 @@ test {
_ = Method;
_ = Server;
_ = Status;
_ = HeadParser;
_ = @import("http/test.zig");
}

370
lib/std/http/HeadParser.zig Normal file
View File

@ -0,0 +1,370 @@
state: State = .start,
pub const State = enum {
start,
seen_n,
seen_r,
seen_rn,
seen_rnr,
finished,
};
/// Returns the number of bytes consumed by headers. This is always less
/// than or equal to `bytes.len`.
///
/// If the amount returned is less than `bytes.len`, the parser is in a
/// content state and the first byte of content is located at
/// `bytes[result]`.
pub fn feed(p: *HeadParser, bytes: []const u8) usize {
const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
const len: u32 = @intCast(bytes.len);
var index: u32 = 0;
while (true) {
switch (p.state) {
.finished => return index,
.start => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
return index + 1;
},
2 => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
return index + 2;
},
3 => {
const b24 = int24(bytes[index..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => p.state = .seen_rnr,
else => {},
}
return index + 3;
},
4...vector_len - 1 => {
const b32 = int32(bytes[index..][0..4]);
const b24 = intShift(u24, b32);
const b16 = intShift(u16, b32);
const b8 = intShift(u8, b32);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => p.state = .seen_rnr,
else => {},
}
switch (b32) {
int32("\r\n\r\n") => p.state = .finished,
else => {},
}
index += 4;
continue;
},
else => {
const chunk = bytes[index..][0..vector_len];
const matches = if (use_vectors) matches: {
const Vector = @Vector(vector_len, u8);
// const BoolVector = @Vector(vector_len, bool);
const BitVector = @Vector(vector_len, u1);
const SizeVector = @Vector(vector_len, u8);
const v: Vector = chunk.*;
const matches_r: BitVector = @bitCast(v == @as(Vector, @splat('\r')));
const matches_n: BitVector = @bitCast(v == @as(Vector, @splat('\n')));
const matches_or: SizeVector = matches_r | matches_n;
break :matches @reduce(.Add, matches_or);
} else matches: {
var matches: u8 = 0;
for (chunk) |byte| switch (byte) {
'\r', '\n' => matches += 1,
else => {},
};
break :matches matches;
};
switch (matches) {
0 => {},
1 => switch (chunk[vector_len - 1]) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
},
2 => {
const b16 = int16(chunk[vector_len - 2 ..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
},
3 => {
const b24 = int24(chunk[vector_len - 3 ..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => p.state = .seen_rnr,
else => {},
}
},
4...vector_len => {
inline for (0..vector_len - 3) |i_usize| {
const i = @as(u32, @truncate(i_usize));
const b32 = int32(chunk[i..][0..4]);
const b16 = intShift(u16, b32);
if (b32 == int32("\r\n\r\n")) {
p.state = .finished;
return index + i + 4;
} else if (b16 == int16("\n\n")) {
p.state = .finished;
return index + i + 2;
}
}
const b24 = int24(chunk[vector_len - 3 ..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => p.state = .seen_rnr,
else => {},
}
},
else => unreachable,
}
index += vector_len;
continue;
},
},
.seen_n => switch (len - index) {
0 => return index,
else => {
switch (bytes[index]) {
'\n' => p.state = .finished,
else => p.state = .start,
}
index += 1;
continue;
},
},
.seen_r => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\n' => p.state = .seen_rn,
'\r' => p.state = .seen_r,
else => p.state = .start,
}
return index + 1;
},
2 => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_rn,
else => p.state = .start,
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\r") => p.state = .seen_rnr,
int16("\n\n") => p.state = .finished,
else => {},
}
return index + 2;
},
else => {
const b24 = int24(bytes[index..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => p.state = .seen_r,
'\n' => p.state = .seen_n,
else => p.state = .start,
}
switch (b16) {
int16("\r\n") => p.state = .seen_rn,
int16("\n\n") => p.state = .finished,
else => {},
}
switch (b24) {
int24("\n\r\n") => p.state = .finished,
else => {},
}
index += 3;
continue;
},
},
.seen_rn => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\r' => p.state = .seen_rnr,
'\n' => p.state = .seen_n,
else => p.state = .start,
}
return index + 1;
},
else => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => p.state = .seen_rnr,
'\n' => p.state = .seen_n,
else => p.state = .start,
}
switch (b16) {
int16("\r\n") => p.state = .finished,
int16("\n\n") => p.state = .finished,
else => {},
}
index += 2;
continue;
},
},
.seen_rnr => switch (len - index) {
0 => return index,
else => {
switch (bytes[index]) {
'\n' => p.state = .finished,
else => p.state = .start,
}
index += 1;
continue;
},
},
}
return index;
}
}
inline fn int16(array: *const [2]u8) u16 {
return @bitCast(array.*);
}
inline fn int24(array: *const [3]u8) u24 {
return @bitCast(array.*);
}
inline fn int32(array: *const [4]u8) u32 {
return @bitCast(array.*);
}
inline fn intShift(comptime T: type, x: anytype) T {
switch (@import("builtin").cpu.arch.endian()) {
.little => return @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T))),
.big => return @truncate(x),
}
}
const HeadParser = @This();
const std = @import("std");
const use_vectors = builtin.zig_backend != .stage2_x86_64;
const builtin = @import("builtin");
test feed {
const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
for (0..36) |i| {
var p: HeadParser = .{};
try std.testing.expectEqual(i, p.feed(data[0..i]));
try std.testing.expectEqual(35 - i, p.feed(data[i..]));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,119 +0,0 @@
stream: std.net.Stream,
read_buf: [buffer_size]u8,
read_start: u16,
read_end: u16,
pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
pub fn rawReadAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
return conn.stream.readAtLeast(buffer, len) catch |err| {
switch (err) {
error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
else => return error.UnexpectedReadFailure,
}
};
}
pub fn fill(conn: *Connection) ReadError!void {
if (conn.read_end != conn.read_start) return;
const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);
if (nread == 0) return error.EndOfStream;
conn.read_start = 0;
conn.read_end = @intCast(nread);
}
pub fn peek(conn: *Connection) []const u8 {
return conn.read_buf[conn.read_start..conn.read_end];
}
pub fn drop(conn: *Connection, num: u16) void {
conn.read_start += num;
}
pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var out_index: u16 = 0;
while (out_index < len) {
const available_read = conn.read_end - conn.read_start;
const available_buffer = buffer.len - out_index;
if (available_read > available_buffer) { // partially read buffered data
@memcpy(buffer[out_index..], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
out_index += @as(u16, @intCast(available_buffer));
conn.read_start += @as(u16, @intCast(available_buffer));
break;
} else if (available_read > 0) { // fully read buffered data
@memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]);
out_index += available_read;
conn.read_start += available_read;
if (out_index >= len) break;
}
const leftover_buffer = available_buffer - available_read;
const leftover_len = len - out_index;
if (leftover_buffer > conn.read_buf.len) {
// skip the buffer if the output is large enough
return conn.rawReadAtLeast(buffer[out_index..], leftover_len);
}
try conn.fill();
}
return out_index;
}
pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
return conn.readAtLeast(buffer, 1);
}
pub const ReadError = error{
ConnectionTimedOut,
ConnectionResetByPeer,
UnexpectedReadFailure,
EndOfStream,
};
pub const Reader = std.io.Reader(*Connection, ReadError, read);
pub fn reader(conn: *Connection) Reader {
return .{ .context = conn };
}
pub fn writeAll(conn: *Connection, buffer: []const u8) WriteError!void {
return conn.stream.writeAll(buffer) catch |err| switch (err) {
error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
else => return error.UnexpectedWriteFailure,
};
}
pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
return conn.stream.write(buffer) catch |err| switch (err) {
error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
else => return error.UnexpectedWriteFailure,
};
}
pub const WriteError = error{
ConnectionResetByPeer,
UnexpectedWriteFailure,
};
pub const Writer = std.io.Writer(*Connection, WriteError, write);
pub fn writer(conn: *Connection) Writer {
return .{ .context = conn };
}
pub fn close(conn: *Connection) void {
conn.stream.close();
}
const Connection = @This();
const std = @import("../../std.zig");
const assert = std.debug.assert;

View File

@ -73,339 +73,28 @@ pub const HeadersParser = struct {
return hp.header_bytes_buffer[0..hp.header_bytes_len];
}
/// Returns the number of bytes consumed by headers. This is always less
/// than or equal to `bytes.len`.
/// You should check `r.state.isContent()` after this to check if the
/// headers are done.
///
/// If the amount returned is less than `bytes.len`, you may assume that
/// the parser is in a content state and the
/// first byte of content is located at `bytes[result]`.
pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
const len: u32 = @intCast(bytes.len);
var index: u32 = 0;
while (true) {
switch (r.state) {
.invalid => unreachable,
.finished => return index,
.start => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
return index + 1;
},
2 => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
return index + 2;
},
3 => {
const b24 = int24(bytes[index..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => r.state = .seen_rnr,
else => {},
}
return index + 3;
},
4...vector_len - 1 => {
const b32 = int32(bytes[index..][0..4]);
const b24 = intShift(u24, b32);
const b16 = intShift(u16, b32);
const b8 = intShift(u8, b32);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => r.state = .seen_rnr,
else => {},
}
switch (b32) {
int32("\r\n\r\n") => r.state = .finished,
else => {},
}
index += 4;
continue;
},
else => {
const chunk = bytes[index..][0..vector_len];
const matches = if (use_vectors) matches: {
const Vector = @Vector(vector_len, u8);
// const BoolVector = @Vector(vector_len, bool);
const BitVector = @Vector(vector_len, u1);
const SizeVector = @Vector(vector_len, u8);
const v: Vector = chunk.*;
const matches_r: BitVector = @bitCast(v == @as(Vector, @splat('\r')));
const matches_n: BitVector = @bitCast(v == @as(Vector, @splat('\n')));
const matches_or: SizeVector = matches_r | matches_n;
break :matches @reduce(.Add, matches_or);
} else matches: {
var matches: u8 = 0;
for (chunk) |byte| switch (byte) {
'\r', '\n' => matches += 1,
else => {},
};
break :matches matches;
};
switch (matches) {
0 => {},
1 => switch (chunk[vector_len - 1]) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
},
2 => {
const b16 = int16(chunk[vector_len - 2 ..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
},
3 => {
const b24 = int24(chunk[vector_len - 3 ..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => r.state = .seen_rnr,
else => {},
}
},
4...vector_len => {
inline for (0..vector_len - 3) |i_usize| {
const i = @as(u32, @truncate(i_usize));
const b32 = int32(chunk[i..][0..4]);
const b16 = intShift(u16, b32);
if (b32 == int32("\r\n\r\n")) {
r.state = .finished;
return index + i + 4;
} else if (b16 == int16("\n\n")) {
r.state = .finished;
return index + i + 2;
}
}
const b24 = int24(chunk[vector_len - 3 ..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => {},
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
switch (b24) {
int24("\r\n\r") => r.state = .seen_rnr,
else => {},
}
},
else => unreachable,
}
index += vector_len;
continue;
},
},
.seen_n => switch (len - index) {
0 => return index,
else => {
switch (bytes[index]) {
'\n' => r.state = .finished,
else => r.state = .start,
}
index += 1;
continue;
},
},
.seen_r => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\n' => r.state = .seen_rn,
'\r' => r.state = .seen_r,
else => r.state = .start,
}
return index + 1;
},
2 => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_rn,
else => r.state = .start,
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\r") => r.state = .seen_rnr,
int16("\n\n") => r.state = .finished,
else => {},
}
return index + 2;
},
else => {
const b24 = int24(bytes[index..][0..3]);
const b16 = intShift(u16, b24);
const b8 = intShift(u8, b24);
switch (b8) {
'\r' => r.state = .seen_r,
'\n' => r.state = .seen_n,
else => r.state = .start,
}
switch (b16) {
int16("\r\n") => r.state = .seen_rn,
int16("\n\n") => r.state = .finished,
else => {},
}
switch (b24) {
int24("\n\r\n") => r.state = .finished,
else => {},
}
index += 3;
continue;
},
},
.seen_rn => switch (len - index) {
0 => return index,
1 => {
switch (bytes[index]) {
'\r' => r.state = .seen_rnr,
'\n' => r.state = .seen_n,
else => r.state = .start,
}
return index + 1;
},
else => {
const b16 = int16(bytes[index..][0..2]);
const b8 = intShift(u8, b16);
switch (b8) {
'\r' => r.state = .seen_rnr,
'\n' => r.state = .seen_n,
else => r.state = .start,
}
switch (b16) {
int16("\r\n") => r.state = .finished,
int16("\n\n") => r.state = .finished,
else => {},
}
index += 2;
continue;
},
},
.seen_rnr => switch (len - index) {
0 => return index,
else => {
switch (bytes[index]) {
'\n' => r.state = .finished,
else => r.state = .start,
}
index += 1;
continue;
},
},
.chunk_head_size => unreachable,
.chunk_head_ext => unreachable,
.chunk_head_r => unreachable,
.chunk_data => unreachable,
.chunk_data_suffix => unreachable,
.chunk_data_suffix_r => unreachable,
}
return index;
}
var hp: std.http.HeadParser = .{
.state = switch (r.state) {
.start => .start,
.seen_n => .seen_n,
.seen_r => .seen_r,
.seen_rn => .seen_rn,
.seen_rnr => .seen_rnr,
.finished => .finished,
else => unreachable,
},
};
const result = hp.feed(bytes);
r.state = switch (hp.state) {
.start => .start,
.seen_n => .seen_n,
.seen_r => .seen_r,
.seen_rn => .seen_rn,
.seen_rnr => .seen_rnr,
.finished => .finished,
};
return @intCast(result);
}
/// Returns the number of bytes consumed by the chunk size. This is always
@ -775,17 +464,6 @@ const MockBufferedConnection = struct {
}
};
test "HeadersParser.findHeadersEnd" {
var r: HeadersParser = undefined;
const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
for (0..36) |i| {
r = HeadersParser.init(&.{});
try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));
try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));
}
}
test "HeadersParser.findChunkedLen" {
var r: HeadersParser = undefined;
const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";

View File

@ -69,31 +69,35 @@ test "trailers" {
fn serverThread(http_server: *std.net.Server) anyerror!void {
var header_buffer: [1024]u8 = undefined;
var remaining: usize = 1;
accept: while (remaining != 0) : (remaining -= 1) {
while (remaining != 0) : (remaining -= 1) {
const conn = try http_server.accept();
defer conn.stream.close();
var res = std.http.Server.init(conn, .{ .client_header_buffer = &header_buffer });
var server = std.http.Server.init(conn, &header_buffer);
res.wait() catch |err| switch (err) {
error.HttpHeadersInvalid => continue :accept,
error.EndOfStream => continue,
else => return err,
};
try serve(&res);
try testing.expectEqual(.reset, res.reset());
try testing.expectEqual(.ready, server.state);
var request = try server.receiveHead();
try serve(&request);
try testing.expectEqual(.ready, server.state);
}
}
fn serve(res: *std.http.Server) !void {
try testing.expectEqualStrings(res.request.target, "/trailer");
res.transfer_encoding = .chunked;
fn serve(request: *std.http.Server.Request) !void {
try testing.expectEqualStrings(request.head.target, "/trailer");
try res.send();
try res.writeAll("Hello, ");
try res.writeAll("World!\n");
try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");
var send_buffer: [1024]u8 = undefined;
var response = request.respondStreaming(.{
.send_buffer = &send_buffer,
});
try response.writeAll("Hello, ");
try response.flush();
try response.writeAll("World!\n");
try response.flush();
try response.endChunked(.{
.trailers = &.{
.{ .name = "X-Checksum", .value = "aaaa" },
},
});
}
test "HTTP server handles a chunked transfer coding request" {
@ -116,34 +120,33 @@ test "HTTP server handles a chunked transfer coding request" {
const max_header_size = 8192;
const address = try std.net.Address.parseIp("127.0.0.1", 0);
var server = try address.listen(.{ .reuse_address = true });
defer server.deinit();
const server_port = server.listen_address.in.getPort();
var socket_server = try address.listen(.{ .reuse_address = true });
defer socket_server.deinit();
const server_port = socket_server.listen_address.in.getPort();
const server_thread = try std.Thread.spawn(.{}, (struct {
fn apply(s: *std.net.Server) !void {
fn apply(net_server: *std.net.Server) !void {
var header_buffer: [max_header_size]u8 = undefined;
const conn = try s.accept();
const conn = try net_server.accept();
defer conn.stream.close();
var res = std.http.Server.init(conn, .{ .client_header_buffer = &header_buffer });
try res.wait();
try expect(res.request.transfer_encoding == .chunked);
const server_body: []const u8 = "message from server!\n";
res.transfer_encoding = .{ .content_length = server_body.len };
res.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
};
res.keep_alive = false;
try res.send();
var server = std.http.Server.init(conn, &header_buffer);
var request = try server.receiveHead();
try expect(request.head.transfer_encoding == .chunked);
var buf: [128]u8 = undefined;
const n = try res.readAll(&buf);
const n = try request.reader().readAll(&buf);
try expect(std.mem.eql(u8, buf[0..n], "ABCD"));
_ = try res.writer().writeAll(server_body);
try res.finish();
try request.respond("message from server!\n", .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
.keep_alive = false,
});
}
}).apply, .{&server});
}).apply, .{&socket_server});
const request_bytes =
"POST / HTTP/1.1\r\n" ++