~sircmpwn/hare-dev

hare-http: http/server: hare-ev compatible v1 PROPOSED

The first patch is a squashed commit to bring back the reviewed changed
we missed while merging the initial http server code (plus some
additional changes I did afterward).

The later patches refactorize the code to hare-ev.
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/hare-dev/patches/51137/mbox | git am -3
Learn more about email & git

[PATCH hare-http 1/5] http/server: fixes Export this patch

We changed lot of things from this initial version.

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 cmd/http/main.ha      |   9 +--
 cmd/httpd/main.ha     |  95 +++++++++++++-----------------
 net/http/do.ha        |  17 +++---
 net/http/header.ha    |  13 +++--
 net/http/request.ha   | 120 +++++++++++++++++++++++---------------
 net/http/response.ha  |  53 ++++++-----------
 net/http/server.ha    | 130 ++++++++++++++++++++++++++++++++++--------
 net/http/status.ha    |   4 +-
 net/http/transport.ha |  24 ++++++--
 9 files changed, 278 insertions(+), 187 deletions(-)

diff --git a/cmd/http/main.ha b/cmd/http/main.ha
index e266c29..81bd7c3 100644
--- a/cmd/http/main.ha
+++ b/cmd/http/main.ha
@@ -64,12 +64,5 @@ export fn main() void = {
		log::printfln("{}: {}", name, val);
	};

	const body = match (resp.body) {
	case let st: *io::stream =>
		yield st;
	case null =>
		return;
	};
	io::copy(os::stdout, body)!;
	io::close(body)!;
	io::copy(os::stdout, resp.body)!;
};
diff --git a/cmd/httpd/main.ha b/cmd/httpd/main.ha
index 819a2f5..7ecc85b 100644
--- a/cmd/httpd/main.ha
+++ b/cmd/httpd/main.ha
@@ -1,13 +1,14 @@
use getopt;
use net;
use net::ip;
use net::http;
use net::dial;
use os;
use memio;
use io;
use fmt;
use bufio;
use fmt;
use getopt;
use io;
use log;
use memio;
use net::dial;
use net::http;
use net::ip;
use net;
use os;
use strings;

const usage: [_]getopt::help = [
@@ -20,7 +21,7 @@ export fn main() void = {
	defer getopt::finish(&cmd);

	let port: u16 = 8080;
	let ip_addr: ip::addr4 = [127, 0, 0, 1];
	let ip_addr = ip::LOCAL_V4;

	for (let i = 0z; i < len(cmd.opts); i += 1) {
		const opt = cmd.opts[i];
@@ -37,56 +38,42 @@ export fn main() void = {
		};
	};

	const server = match (http::listen(ip_addr, port)) {
	case let this: *http::server =>
		yield this;
	case net::error => abort("failure while listening");
	const serv = match (http::listen(ip_addr, port)) {
	case let serv: http::server =>
		yield serv;
	case let err: net::error =>
		log::fatalf("http::listen: {}", net::strerror(err));
	};
	defer http::server_finish(server);
	defer http::server_finish(&serv);

	for (true) {
		const serv_req = match (http::serve(server)) {
		case let this: *http::server_request =>
			yield this;
		case net::error => abort("failure while serving");
		};
		defer http::serve_finish(serv_req);
		const (req, rw) = http::serve(&serv)!;
		defer http::parsed_request_finish(&req);
		defer io::close(rw.sock)!;

		let buf = memio::dynamic();
		defer io::close(&buf)!;
		handlereq(&buf, &serv_req.request);

		http::response_write(
			serv_req.socket,
			http::STATUS_OK,
			&buf,
			("Content-Type", "text/plain")
		)!;
		const (ip, port) = http::peeraddr(&rw);
		log::printfln("{}:{}: {} {}", ip::string(ip), port,
			req.method, req.target.path);
		handle(&req, &rw);
	};
};

export fn handlereq(buf: *io::stream, request: *http::request) void = {
	fmt::fprintfln(buf, "Method: {}", request.method)!;
	fmt::fprintfln(buf, "Path: {}", request.target.path)!;
	fmt::fprintfln(buf, "Fragment: {}", request.target.fragment)!;
	fmt::fprintfln(buf, "Query: {}", request.target.query)!;
	fmt::fprintfln(buf, "Headers: <<EOF")!;
	http::write_header(buf, &request.header)!;
	fmt::fprintfln(buf, "EOF")!;
fn handle(req: *http::request, rw: *http::response_writer) void = {
	const buf = memio::dynamic();
	defer io::close(&buf)!;

	match (request.body) {
	case void => void;
	case let body: io::handle =>
		fmt::fprintfln(buf, "Body: <<EOF")!;
		for (true) {
			match (bufio::read_line(body)!) {
			case let line: []u8 =>
				fmt::fprintln(buf, strings::fromutf8(line)!)!;
				break;
			case io::EOF =>
				break;
			};
		};
		fmt::fprintfln(buf, "EOF")!;
	};
	fmt::fprintfln(&buf, "Method: {}", req.method)!;
	fmt::fprintfln(&buf, "Path: {}", req.target.path)!;
	fmt::fprintfln(&buf, "Fragment: {}", req.target.fragment)!;
	fmt::fprintfln(&buf, "Query: {}", req.target.query)!;
	fmt::fprintfln(&buf, "Headers:")!;
	http::write_header(&buf, &req.header)!;
	fmt::fprintln(&buf)!;
	io::copy(&buf, req.body)!;

	io::seek(&buf, 0, io::whence::SET)!;

	http::response_add_header(rw, "Content-Type", "text/plain");
	http::response_set_body(rw, &buf);
	http::response_write(rw)!;
};
diff --git a/net/http/do.ha b/net/http/do.ha
index 9b5905b..99786eb 100644
--- a/net/http/do.ha
+++ b/net/http/do.ha
@@ -48,17 +48,18 @@ export fn do(client: *client, req: *request) (response | error) = {
	assert(trans.request_content == content_mode::AUTO);
	assert(trans.response_content == content_mode::AUTO);

	match (req.body) {
	case let body: io::handle =>
		io::copy(conn, body)?;
	case void =>
		yield;
	};
	io::copy(conn, req.body)?;

	let resp = response { ... };
	let resp = response {
		body = io::empty,
		...
	};
	const scan = bufio::newscanner(conn, 512);
	read_statusline(&resp, &scan)?;
	read_header(&resp.header, &scan)?;
	match (read_header(&resp.header, &scan)?) {
	case void => void;
	case io::EOF => return protoerr;
	};

	const response_complete =
		req.method == "HEAD" ||
diff --git a/net/http/header.ha b/net/http/header.ha
index c615484..ee9e40a 100644
--- a/net/http/header.ha
+++ b/net/http/header.ha
@@ -47,12 +47,17 @@ export fn header_get(head: *header, name: str) str = {
	return "";
};

// Frees state associated with an HTTP [[header]].
export fn header_free(head: *header) void = {
// Finish state associated with an HTTP [[header]].
export fn header_finish(head: *header) void = {
	for (let i = 0z; i < len(head); i += 1) {
		free(head[i].0);
		free(head[i].1);
	};
};

// Frees state associated with an HTTP [[header]].
export fn header_free(head: *header) void = {
	header_finish(head);
	free(*head);
};

@@ -77,13 +82,13 @@ export fn write_header(sink: io::handle, head: *header) (size | io::error) = {
	return z;
};

fn read_header(head: *header, scan: *bufio::scanner) (void | io::error | protoerr) = {
fn read_header(head: *header, scan: *bufio::scanner) (void | io::EOF | io::error | protoerr) = {
	for (true) {
		const item = match (bufio::scan_string(scan, "\r\n")) {
		case let line: const str =>
			yield line;
		case io::EOF =>
			break;
			return io::EOF;
		case let err: io::error =>
			return err;
		case utf8::invalid =>
diff --git a/net/http/request.ha b/net/http/request.ha
index cb85a8c..604c61a 100644
--- a/net/http/request.ha
+++ b/net/http/request.ha
@@ -1,13 +1,13 @@
use bufio;
use encoding::utf8;
use errors;
use fmt;
use io;
use memio;
use net::ip;
use net::uri;
use strconv;
use strings;
use bufio;
use memio;
use encoding::utf8;
use types;

// Stores state related to an HTTP request.
@@ -19,6 +19,7 @@ use types;
export type request = struct {
	// HTTP request method, e.g. GET
	method: str,

	// Request target URI.
	//
	// Note that the normal constraints for [[uri::parse]] are not upheld in
@@ -32,8 +33,9 @@ export type request = struct {
	// Transport configuration, or null to use the [[client]] default.
	transport: nullable *transport,

	// I/O reader for the request body, or void if there is no body.
	body: (io::handle | void),
	// I/O reader for the request body, or [[io::empty]] if there is no
	// body.
	body: io::handle,
};

// Frees state associated with an HTTP [[request]].
@@ -54,7 +56,7 @@ export fn new_request(
		target = alloc(uri::dup(target)),
		header = header_dup(&client.default_header),
		transport = null,
		body = void,
		body = io::empty,
	};
	switch (req.target.scheme) {
	case "http" =>
@@ -134,47 +136,79 @@ export type request_line = struct {
	version: version,
};

export fn request_parse(file: io::handle) (request | protoerr | io::error) = {
export fn request_parse(file: io::handle) (request | protoerr | errors::unsupported | io::error) = {
	const scan = bufio::newscanner(file, types::SIZE_MAX);
	defer bufio::finish(&scan);
	match (request_scan(&scan)?) {
	case io::EOF => return protoerr;
	case let req: request => return req;
	};
};

	const req_line = request_line_parse(&scan)?;
export fn request_scan(scan: *bufio::scanner) (request | io::EOF | protoerr | errors::unsupported | io::error) = {
	const req_line = match (request_line_parse(scan)?) {
	case let req_line: request_line => yield req_line;
	case io::EOF => return io::EOF;
	};
	defer request_line_finish(&req_line);

	let header: header = [];
	read_header(&header, &scan)?;
	match (read_header(&header, scan)?) {
	case void => void;
	case io::EOF =>
		header_finish(&header);
		return io::EOF;
	};

	let body = io::empty;
	const cl = header_get(&header, "Content-Length");
	const te = header_get(&header, "Transfer-Encoding");

	if (cl != "" && te == "") {
		const cl = match(strconv::stou(cl)) {
		case let l: uint => yield l;
		case (strconv::invalid | strconv::overflow) => return protoerr;
		};


		let buf: []u8 = alloc([0...], cl);

		match (io::readall(scan, buf)) {
		case let n: size => void;
		case io::EOF =>
			free(buf);
			return io::EOF;
		case let err: io::error =>
			free(buf);
			if (err is io::underread) {
				return io::EOF;
			} else {
				return err;
			};
		};

		body = alloc(memio::dynamic_from(buf));
	} else if (cl != "" || te != "") {
		return errors::unsupported;
	};

	const target = match (req_line.uri) {
	case let uri: request_server => return errors::unsupported;
	case let uri: authority => return errors::unsupported;
	case let uri: *uri::uri => yield uri;
	case let uri: *uri::uri => yield &uri::dup(uri);
	case let path: str =>
		const uri = fmt::asprintf("http:{}", path);
		defer free(uri);
		yield alloc(uri::parse(uri)!);
		const u = fmt::asprintf("http://{}", path);
		defer free(u);
		yield alloc(uri::parse(u)!);
	};

	const length: (void | size) = void;
	const head_length = header_get(&header, "Content-Length");
	if ("" != head_length) {
		match (strconv::stoz(head_length)) {
		case let s: size => length = s;
		case strconv::invalid => return protoerr;
		case strconv::overflow => return protoerr;
		};
	};

	let body: (io::handle | void) = void;
	if (length is size) {
		const limit = io::limitreader(&scan, length as size);
		let _body = alloc(memio::dynamic());
		io::copy(_body, &limit)!;
		io::seek(_body, 0, io::whence::SET)!;
		body = _body;
	const host = header_get(&header, "Host");
	if (host != "") {
		target.host = strings::dup(host);
	};

	return request {
		method = req_line.method,
		method = strings::dup(req_line.method),
		target = target,
		header = header,
		body = body,
@@ -182,21 +216,14 @@ export fn request_parse(file: io::handle) (request | protoerr | io::error) = {
	};
};

export fn parsed_request_finish(request: *request) void = {
	uri::finish(request.target);
	free(request.target);

	match (request.body) {
	case void => yield;
	case let body: io::handle =>
		io::close(body)!;
		free(body: *memio::stream);
	};

	header_free(&request.header);
export fn parsed_request_finish(req: *request) void = {
	free(req.method);
	uri::finish(req.target);
	header_finish(&req.header);
	io::close(req.body)!;
};

export fn request_line_parse(scan: *bufio::scanner) (request_line | protoerr | io::error) = {
fn request_line_parse(scan: *bufio::scanner) (request_line | io::EOF | protoerr | io::error) = {
	const line = match (bufio::scan_string(scan, "\r\n")) {
	case let line: const str =>
		yield line;
@@ -205,7 +232,7 @@ export fn request_line_parse(scan: *bufio::scanner) (request_line | protoerr | i
	case utf8::invalid =>
		return protoerr;
	case io::EOF =>
		return protoerr;
		return io::EOF;
	};

	const tok = strings::tokenize(line, " ");
@@ -269,9 +296,10 @@ export fn request_line_parse(scan: *bufio::scanner) (request_line | protoerr | i
	};
};

export fn request_line_finish(line: *request_line) void = {
fn request_line_finish(line: *request_line) void = {
	match (line.uri) {
	case let path: str => free(path);
	case let uri: *uri::uri => uri::finish(uri);
	case => yield;
	};
};
diff --git a/net/http/response.ha b/net/http/response.ha
index 84f0c81..37e15aa 100644
--- a/net/http/response.ha
+++ b/net/http/response.ha
@@ -15,49 +15,28 @@ export type response = struct {
	reason: str,
	// The HTTP headers provided by the server.
	header: header,
	// The response body, if any.
	body: nullable *io::stream,
	// The response body, if any, or [[io::empty]].
	body: io::handle,
};

// Frees state associated with an HTTP [[response]]. If the response has a
// non-null body, the user must call [[io::close]] prior to calling this
// function.
// Frees state associated with an HTTP [[response]] and closes the response
// body.
export fn response_finish(resp: *response) void = {
	// Ignore errors in case the caller closed it themselves
	io::close(resp.body): void;
	header_free(&resp.header);
	free(resp.reason);
	free(resp.body);
};

export fn response_write(
	rw: io::handle,
	status: uint,
	body: (void | io::handle),
	header: (str, str)...
// Formats an HTTP [[response]] and writes it to the given [[io::handle]].
export fn response_write_internal(
	out: io::handle,
	resp: *response,
) (void | io::error) = {
	fmt::fprintfln(rw, "HTTP/1.1 {} {}", status, status_reason(status))?;

	let header = header_dup(&header);
	defer header_free(&header);

	match (body) {
	case void => void;
	case let body: io::handle =>
		match (io::tell(body)) {
		case io::error => void;
		case let off: io::off =>
			header_add(&header, "Content-Length", strconv::i64tos(off));
			io::seek(body, 0, io::whence::SET)!;
			body = &io::limitreader(body, off: size);
		};
	};

	write_header(rw, &header)?;

	fmt::fprintln(rw)!;

	match (body) {
	case void => void;
	case let body: io::handle =>
		io::copy(rw, body)!;
	};
	fmt::fprintf(out, "HTTP/{}.{} {} {}\r\n",
		resp.version.0, resp.version.1,
		resp.status, resp.reason)?;
	write_header(out, &resp.header)?;
	fmt::fprintf(out, "\r\n")?;
	io::copy(out, resp.body)?;
};
diff --git a/net/http/server.ha b/net/http/server.ha
index 44e76ac..c709524 100644
--- a/net/http/server.ha
+++ b/net/http/server.ha
@@ -1,43 +1,125 @@
use io;
use net;
use net::ip;
use net::tcp;
use strconv;
use strings;

export type server = struct {
	socket: net::socket,
	sock: net::socket,
};

export type server_request = struct {
	socket: net::socket,
	request: request,
// Stores state for a pending HTTP [[response]] to be sent by a [[server]].
export type response_writer = struct {
	// The pending response
	resp: response,
	// The remote client socket
	sock: net::socket,
	// Where to write the response
	writeto: io::handle,
};

export fn listen(ip: ip::addr, port: u16) (*server | net::error) = {
	return alloc(server {
		socket = tcp::listen(ip, port)?,
// Creates a [[server]] which listens for HTTP traffic on the given IP address
// and port, binding the socket as appropriate.
export fn listen(ip: ip::addr, port: u16) (server | net::error) = {
	return server {
		sock = tcp::listen(ip, port, tcp::reuseaddr)?,
	};
};

// Frees resources associated with a [[server]] and closes its socket.
export fn server_finish(serv: *server) void = {
	net::close(serv.sock)!;
};

// Listens for an incoming request on a [[server]], blocking until one is
// available and returning the ([[request]], [[response_writer]]) pair. The
// caller should configure the response as necessary (by populating
// [[response_writer]].resp directly, or with convenience functions like
// [[response_set_body]]) and pass it [[response_write]] before calling
// [[serve]] again.
//
// The response is initialized with a 200 OK status, an empty header, and an
// empty response body.
export fn serve(serv: *server) ((request, response_writer) | error) = {
	// TODO: connection pooling
	const sock = net::accept(serv.sock)?;

	const req = request_parse(sock)?;
	const resp = response {
		version = (1, 1),
		status = STATUS_OK,
		reason = strings::dup(status_reason(STATUS_OK)),
		header = [],
		body = io::empty,
	};
	return (req, response_writer {
		sock = sock,
		resp = resp,
		writeto = sock, // Same socket
	});
};

export fn server_finish(server: *server) void = {
	net::close(server.socket)!;
	free(server);
// Returns the remote peer address associated with this connection.
export fn peeraddr(rw: *response_writer) (ip::addr, u16) = {
	return tcp::peeraddr(rw.sock) as (ip::addr, u16);
};

export fn accept(server: *server) (net::socket | net::error) = {
	return net::accept(server.socket)?;
// Convenience function to set the response body of a [[response_writer]].
export fn response_set_body(
	rw: *response_writer,
	body: io::handle,
) void = {
	rw.resp.body = body;
};

export fn serve(server: *server) (*server_request | net::error) = {
	const socket = accept(server)?;
	const request = request_parse(socket)!;

	return alloc(server_request {
		request = request,
		socket = socket,
	});
// Convenience function to set the response status of a [[response_writer]].
export fn response_set_status(
	rw: *response_writer,
	status: uint,
	reason: str,
) void = {
	free(rw.resp.reason);
	rw.resp.status = status;
	rw.resp.reason = strings::dup(reason);
};

export fn serve_finish(serv_req: *server_request) void = {
	parsed_request_finish(&serv_req.request);
	net::close(serv_req.socket)!;
	free(serv_req);
// Convenience function to add a header to a [[response_writer]].
export fn response_add_header(
	rw: *response_writer,
	name: str,
	val: str,
) void = {
	header_add(&rw.resp.header, name, val);
};

// Sends a completed HTTP response to the connected client and frees resources
// associated with the response. The response body is not closed; the caller
// must do so themselves if appropriate.
//
// If the response does not have a Content-Length header and the response body
// is seekable, a Content-Length header will be added automatically.
export fn response_write(rw: *response_writer) (void | error) = {
	defer response_finish(&rw.resp);

	if (header_get(&rw.resp.header, "Content-Length") == "") {
		add_content_length(rw);
	};

	response_write_internal(rw.writeto, &rw.resp)?;
};

fn add_content_length(rw: *response_writer) void = {
	const body = rw.resp.body;
	const orig = match (io::tell(body)) {
	case let off: io::off =>
		yield off;
	case io::error => return;
	};

	let length = io::seek(body, 0, io::whence::END)!;
	io::seek(body, orig, io::whence::SET)!;
	length -= orig;

	header_add(&rw.resp.header, "Content-Length", strconv::i64tos(length));
};
diff --git a/net/http/status.ha b/net/http/status.ha
index bbb418a..920b70a 100644
--- a/net/http/status.ha
+++ b/net/http/status.ha
@@ -20,9 +20,9 @@ export fn status_reason(status: uint) const str = {
	case STATUS_SWITCHING_PROTOCOLS =>
		return "Switching Protocols";
	case STATUS_OK =>
		return "Continue";
		return "OK";
	case STATUS_CREATED =>
		return "Continue";
		return "Created";
	case STATUS_ACCEPTED =>
		return "Accepted";
	case STATUS_NONAUTHORITATIVE_INFO =>
diff --git a/net/http/transport.ha b/net/http/transport.ha
index 6343ccf..6832602 100644
--- a/net/http/transport.ha
+++ b/net/http/transport.ha
@@ -53,7 +53,7 @@ fn new_reader(
	conn: io::file,
	resp: *response,
	scan: *bufio::scanner,
) (*io::stream | errors::unsupported | protoerr) = {
) (io::handle | errors::unsupported | protoerr) = {
	// TODO: Content-Encoding support
	const cl = header_get(&resp.header, "Content-Length");
	const te = header_get(&resp.header, "Transfer-Encoding");
@@ -77,11 +77,17 @@ fn new_reader(
	// And it should not close the actual connection if it's still in the
	// connection pool
	// Unless it isn't in the pool, then it should!
	// And this leaks, fix that too
	let stream: io::handle = conn;
	let buffer: []u8 = bufio::scan_buffer(scan);
	const iter = strings::tokenize(te, ",");
	for (const tok => strings::next_token(&iter)) {
		const te = strings::trim(tok);
	for (true) {
		const te = match (strings::next_token(&iter)) {
		case let tok: str =>
			yield strings::trim(tok);
		case done =>
			break;
		};

		// XXX: We could add lzw support if someone added it to
		// hare-compress
@@ -104,7 +110,7 @@ fn new_reader(
		// Empty Transfer-Encoding header
		return protoerr;
	};
	return stream as *io::stream;
	return stream;
};

type identity_reader = struct {
@@ -156,7 +162,9 @@ fn identity_close(s: *io::stream) (void | io::error) = {

	bufio::finish(rd.scan);
	free(rd.scan);
	// TODO connection pool
	io::close(rd.conn)?;
	free(rd);
};

type chunk_state = enum {
@@ -192,6 +200,7 @@ fn new_chunked_reader(

const chunked_reader_vtable = io::vtable {
	reader = &chunked_read,
	closer = &chunked_close,
	...
};

@@ -294,3 +303,10 @@ fn chunked_read(
		rd.state = chunk_state::HEADER;
	};
};

fn chunked_close(s: *io::stream) (void | io::error) = {
	let rd = s: *chunked_reader;
	// TODO connection pool
	io::close(rd.conn)?;
	free(rd);
};
-- 
2.44.0

[PATCH hare-http 2/5] Use add_content_length while doing requests Export this patch

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 net/http/do.ha     |  2 ++
 net/http/header.ha | 15 +++++++++++++++
 net/http/server.ha | 18 +-----------------
 3 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/net/http/do.ha b/net/http/do.ha
index 99786eb..a0020c6 100644
--- a/net/http/do.ha
+++ b/net/http/do.ha
@@ -32,6 +32,8 @@ export fn do(client: *client, req: *request) (response | error) = {
	uri::fmt(&file, &target)?;
	fmt::fprintf(&file, " HTTP/1.1\r\n")?;

	add_content_length(req.body, &req.header);

	write_header(&file, &req.header)?;
	fmt::fprintf(&file, "\r\n")?;
	bufio::flush(&file)?;
diff --git a/net/http/header.ha b/net/http/header.ha
index ee9e40a..88e57f1 100644
--- a/net/http/header.ha
+++ b/net/http/header.ha
@@ -2,6 +2,7 @@ use bufio;
use encoding::utf8;
use fmt;
use io;
use strconv;
use strings;

// List of HTTP headers.
@@ -108,3 +109,17 @@ fn read_header(head: *header, scan: *bufio::scanner) (void | io::EOF | io::error
		header_add(head, name, val);
	};
};

export fn add_content_length(body: io::handle, header: *header) void = {
	const orig = match (io::tell(body)) {
	case let off: io::off =>
		yield off;
	case io::error => return;
	};

	let length = io::seek(body, 0, io::whence::END)!;
	io::seek(body, orig, io::whence::SET)!;
	length -= orig;

	header_add(header, "Content-Length", strconv::i64tos(length));
};
diff --git a/net/http/server.ha b/net/http/server.ha
index c709524..b42bfb8 100644
--- a/net/http/server.ha
+++ b/net/http/server.ha
@@ -2,7 +2,6 @@ use io;
use net;
use net::ip;
use net::tcp;
use strconv;
use strings;

export type server = struct {
@@ -103,23 +102,8 @@ export fn response_write(rw: *response_writer) (void | error) = {
	defer response_finish(&rw.resp);

	if (header_get(&rw.resp.header, "Content-Length") == "") {
		add_content_length(rw);
		add_content_length(rw.resp.body, &rw.resp.header);
	};

	response_write_internal(rw.writeto, &rw.resp)?;
};

fn add_content_length(rw: *response_writer) void = {
	const body = rw.resp.body;
	const orig = match (io::tell(body)) {
	case let off: io::off =>
		yield off;
	case io::error => return;
	};

	let length = io::seek(body, 0, io::whence::END)!;
	io::seek(body, orig, io::whence::SET)!;
	length -= orig;

	header_add(&rw.resp.header, "Content-Length", strconv::i64tos(length));
};
-- 
2.44.0

[PATCH hare-http 3/5] do: move to an external request_write_internal Export this patch

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 net/http/do.ha      | 26 +-------------------------
 net/http/request.ha | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 35 insertions(+), 25 deletions(-)

diff --git a/net/http/do.ha b/net/http/do.ha
index a0020c6..33a8f71 100644
--- a/net/http/do.ha
+++ b/net/http/do.ha
@@ -25,33 +25,9 @@ export fn do(client: *client, req: *request) (response | error) = {
	let file = bufio::init(conn, [], buf);
	bufio::setflush(&file, []);

	fmt::fprintf(&file, "{} ", req.method)?;

	// TODO: Support other request-targets than origin-form
	const target = uri_origin_form(req.target);
	uri::fmt(&file, &target)?;
	fmt::fprintf(&file, " HTTP/1.1\r\n")?;

	add_content_length(req.body, &req.header);

	write_header(&file, &req.header)?;
	fmt::fprintf(&file, "\r\n")?;
	request_write_internal(&file, req, client)?;
	bufio::flush(&file)?;

	const trans = match (req.transport) {
	case let t: *transport =>
		yield t;
	case =>
		yield &client.default_transport;
	};
	// TODO: Implement None
	assert(trans.request_transport == transport_mode::AUTO);
	assert(trans.response_transport == transport_mode::AUTO);
	assert(trans.request_content == content_mode::AUTO);
	assert(trans.response_content == content_mode::AUTO);

	io::copy(conn, req.body)?;

	let resp = response {
		body = io::empty,
		...
diff --git a/net/http/request.ha b/net/http/request.ha
index 604c61a..efc07f1 100644
--- a/net/http/request.ha
+++ b/net/http/request.ha
@@ -303,3 +303,37 @@ fn request_line_finish(line: *request_line) void = {
	case => yield;
	};
};

// Formats an HTTP [[request]] and writes it to the given [[io::handle]].
export fn request_write_internal(
	out: io::handle,
	req: *request,
	cli: *client,
) (void | io::error) = {
	fmt::fprintf(out, "{} ", req.method)?;

	// TODO: Support other request-targets than origin-form
	const target = uri_origin_form(req.target);
	uri::fmt(out, &target)?;
	fmt::fprintf(out, " HTTP/1.1\r\n")?;

	add_content_length(req.body, &req.header);

	write_header(out, &req.header)?;
	fmt::fprintf(out, "\r\n")?;
	bufio::flush(out)?;

	const trans = match (req.transport) {
	case let t: *transport =>
		yield t;
	case =>
		yield &cli.default_transport;
	};
	// TODO: Implement None
	assert(trans.request_transport == transport_mode::AUTO);
	assert(trans.response_transport == transport_mode::AUTO);
	assert(trans.request_content == content_mode::AUTO);
	assert(trans.response_content == content_mode::AUTO);

	io::copy(out, req.body)?;
};
-- 
2.44.0

[PATCH hare-http 4/5] Refact do to export parse responses methods Export this patch

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 net/http/do.ha       | 102 +---------------------------
 net/http/request.ha  |   1 -
 net/http/response.ha | 156 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 158 insertions(+), 101 deletions(-)

diff --git a/net/http/do.ha b/net/http/do.ha
index 33a8f71..b43970a 100644
--- a/net/http/do.ha
+++ b/net/http/do.ha
@@ -1,15 +1,7 @@
use bufio;
use encoding::utf8;
use errors;
use fmt;
use io;
use net::dial;
use net::uri;
use net;
use os;
use strconv;
use strings;
use types;

// Performs an HTTP [[request]] with the given [[client]]. The request is
// performed synchronously; this function blocks until the server has returned
@@ -20,6 +12,7 @@ use types;
export fn do(client: *client, req: *request) (response | error) = {
	assert(req.target.scheme == "http"); // TODO: https
	const conn = dial::dial_uri("tcp", req.target)?;
	defer io::close(conn)!;

	let buf: [os::BUFSZ]u8 = [0...];
	let file = bufio::init(conn, [], buf);
@@ -28,96 +21,5 @@ export fn do(client: *client, req: *request) (response | error) = {
	request_write_internal(&file, req, client)?;
	bufio::flush(&file)?;

	let resp = response {
		body = io::empty,
		...
	};
	const scan = bufio::newscanner(conn, 512);
	read_statusline(&resp, &scan)?;
	match (read_header(&resp.header, &scan)?) {
	case void => void;
	case io::EOF => return protoerr;
	};

	const response_complete =
		req.method == "HEAD" ||
		resp.status == STATUS_NO_CONTENT ||
		resp.status == STATUS_NOT_MODIFIED ||
		(resp.status >= 100 && resp.status < 200) ||
		(req.method == "CONNECT" && resp.status >= 200 && resp.status < 300);
	if (!response_complete) {
		resp.body = new_reader(conn, &resp, &scan)?;
	} else if (req.method != "CONNECT") {
		io::close(conn)!;
	};
	return resp;
};

fn read_statusline(
	resp: *response,
	scan: *bufio::scanner,
) (void | error) = {
	const status = match (bufio::scan_string(scan, "\r\n")) {
	case let line: const str =>
		yield line;
	case let err: io::error =>
		return err;
	case utf8::invalid =>
		return protoerr;
	case io::EOF =>
		return protoerr;
	};

	const tok = strings::tokenize(status, " ");

	const version = match (strings::next_token(&tok)) {
	case let ver: str =>
		yield ver;
	case done =>
		return protoerr;
	};

	const status = match (strings::next_token(&tok)) {
	case let status: str =>
		yield status;
	case done =>
		return protoerr;
	};

	const reason = match (strings::next_token(&tok)) {
	case let reason: str =>
		yield reason;
	case done =>
		return protoerr;
	};

	const (_, version) = strings::cut(version, "/");
	const (major, minor) = strings::cut(version, ".");

	const major = match (strconv::stou(major)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};
	const minor = match (strconv::stou(minor)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};
	resp.version = (major, minor);

	if (resp.version.0 > 1) {
		return errors::unsupported;
	};

	resp.status = match (strconv::stou(status)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};

	resp.reason = strings::dup(reason);
	return response_parse(conn)?;
};
diff --git a/net/http/request.ha b/net/http/request.ha
index efc07f1..0933669 100644
--- a/net/http/request.ha
+++ b/net/http/request.ha
@@ -321,7 +321,6 @@ export fn request_write_internal(

	write_header(out, &req.header)?;
	fmt::fprintf(out, "\r\n")?;
	bufio::flush(out)?;

	const trans = match (req.transport) {
	case let t: *transport =>
diff --git a/net/http/response.ha b/net/http/response.ha
index 37e15aa..e3ece35 100644
--- a/net/http/response.ha
+++ b/net/http/response.ha
@@ -1,7 +1,13 @@
use bufio;
use io;
use os;
use fmt;
use strconv;
use errors;
use strings;
use types;
use encoding::utf8;
use memio;

export type version = (uint, uint);

@@ -40,3 +46,153 @@ export fn response_write_internal(
	fmt::fprintf(out, "\r\n")?;
	io::copy(out, resp.body)?;
};

export fn response_parse(file: io::handle) (response | protoerr | errors::unsupported | io::error) = {
	const scan = bufio::newscanner(file, types::SIZE_MAX);
	defer bufio::finish(&scan);
	match (response_scan(&scan)?) {
	case io::EOF => return protoerr;
	case let res: response => return res;
	};
};

export fn response_scan(scan: *bufio::scanner) (response | io::EOF | protoerr | errors::unsupported | io::error) = {
	const resp_line = match (response_line_parse(scan)?) {
	case let resp_line: response_line => yield resp_line;
	case io::EOF => return io::EOF;
	};
	defer response_line_finish(&resp_line);

	let header: header = [];
	match (read_header(&header, scan)?) {
	case void => void;
	case io::EOF =>
		header_finish(&header);
		return io::EOF;
	};

	let body = io::empty;
	const cl = header_get(&header, "Content-Length");
	const te = header_get(&header, "Transfer-Encoding");

	if (cl != "" && te == "") {
		const cl = match(strconv::stou(cl)) {
		case let l: uint => yield l;
		case (strconv::invalid | strconv::overflow) => return protoerr;
		};

		let buf: []u8 = alloc([0...], cl);

		match (io::readall(scan, buf)) {
		case let n: size => void;
		case io::EOF =>
			free(buf);
			return io::EOF;
		case let err: io::error =>
			free(buf);
			if (err is io::underread) {
				return io::EOF;
			} else {
				return err;
			};
		};

		body = alloc(memio::dynamic_from(buf));
	} else if (cl != "" || te != "") {
		return errors::unsupported;
	};

	return response {
		version = resp_line.version,
		status = resp_line.status,
		reason = strings::dup(resp_line.reason),
		header = header,
		body = body,
	};
};

export fn parsed_response_finish(resp: *response) void = {
	free(resp.reason);
	header_finish(&resp.header);
};

export type response_line = struct {
	version: version,
	status: uint,
	reason: str,
};

fn response_line_parse(scan: *bufio::scanner) (response_line | io::EOF | protoerr | io::error) = {
	const status = match (bufio::scan_string(scan, "\r\n")) {
	case let line: const str =>
		yield line;
	case let err: io::error =>
		return err;
	case utf8::invalid =>
		return protoerr;
	case io::EOF =>
		return io::EOF;
	};

	const tok = strings::tokenize(status, " ");

	const version = match (strings::next_token(&tok)) {
	case let ver: str =>
		yield ver;
	case done =>
		return protoerr;
	};

	const status = match (strings::next_token(&tok)) {
	case let status: str =>
		yield status;
	case done =>
		return protoerr;
	};

	const reason = match (strings::next_token(&tok)) {
	case let reason: str =>
		yield reason;
	case done =>
		return protoerr;
	};

	const (_, version) = strings::cut(version, "/");
	const (major, minor) = strings::cut(version, ".");

	const major = match (strconv::stou(major)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};
	const minor = match (strconv::stou(minor)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};

	if (major > 1) {
		return errors::unsupported;
	};

	const status = match (strconv::stou(status)) {
	case let u: uint =>
		yield u;
	case =>
		return protoerr;
	};

	const reason = strings::dup(reason);

	return response_line{
		version = (major, minor),
		status = status,
		reason = reason,
	};
};

fn response_line_finish(line: *response_line) void = {
	free(line.reason);
};
-- 
2.44.0

[PATCH hare-http 5/5] use back transport for request/response scanning Export this patch

We use an io::handle rather than an io::stream now that we check if the body is
complete while parsing the message.

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 cmd/http/main.ha      |   2 +-
 net/http/request.ha   |  50 +++-----
 net/http/response.ha  |  48 +++-----
 net/http/transport.ha | 277 ++++++++++--------------------------------
 4 files changed, 106 insertions(+), 271 deletions(-)

diff --git a/cmd/http/main.ha b/cmd/http/main.ha
index 81bd7c3..938e09f 100644
--- a/cmd/http/main.ha
+++ b/cmd/http/main.ha
@@ -53,7 +53,7 @@ export fn main() void = {
	case let resp: http::response =>
		yield resp;
	};
	defer http::response_finish(&resp);
	defer http::parsed_response_finish(&resp);

	log::printfln("HTTP/{}.{}: {} {}",
		resp.version.0, resp.version.1,
diff --git a/net/http/request.ha b/net/http/request.ha
index 0933669..e1aa4b5 100644
--- a/net/http/request.ha
+++ b/net/http/request.ha
@@ -160,36 +160,17 @@ export fn request_scan(scan: *bufio::scanner) (request | io::EOF | protoerr | er
		return io::EOF;
	};

	let body = io::empty;
	const cl = header_get(&header, "Content-Length");
	const te = header_get(&header, "Transfer-Encoding");

	if (cl != "" && te == "") {
		const cl = match(strconv::stou(cl)) {
		case let l: uint => yield l;
		case (strconv::invalid | strconv::overflow) => return protoerr;
		};


		let buf: []u8 = alloc([0...], cl);

		match (io::readall(scan, buf)) {
		case let n: size => void;
		case io::EOF =>
			free(buf);
			return io::EOF;
		case let err: io::error =>
			free(buf);
			if (err is io::underread) {
				return io::EOF;
			} else {
				return err;
			};
		};

		body = alloc(memio::dynamic_from(buf));
	} else if (cl != "" || te != "") {
		return errors::unsupported;
	let body = match (body_scan(scan, &header)) {
	case io::EOF => return io::EOF;
	case let body: io::handle => yield body;
	case let err: protoerr =>
		return err;
	case let err: errors::unsupported =>
		return err;
	case let err: utf8::invalid =>
		return protoerr;
	case let err: io::error =>
		return err;
	};

	const target = match (req_line.uri) {
@@ -220,7 +201,14 @@ export fn parsed_request_finish(req: *request) void = {
	free(req.method);
	uri::finish(req.target);
	header_finish(&req.header);
	io::close(req.body)!;
	match (req.body) {
	case let body: *io::stream =>
		if (body != io::empty) {
			io::close(body)!;
			free(body);
		};
	case => void;
	};
};

fn request_line_parse(scan: *bufio::scanner) (request_line | io::EOF | protoerr | io::error) = {
diff --git a/net/http/response.ha b/net/http/response.ha
index e3ece35..4369cf4 100644
--- a/net/http/response.ha
+++ b/net/http/response.ha
@@ -71,35 +71,17 @@ export fn response_scan(scan: *bufio::scanner) (response | io::EOF | protoerr |
		return io::EOF;
	};

	let body = io::empty;
	const cl = header_get(&header, "Content-Length");
	const te = header_get(&header, "Transfer-Encoding");

	if (cl != "" && te == "") {
		const cl = match(strconv::stou(cl)) {
		case let l: uint => yield l;
		case (strconv::invalid | strconv::overflow) => return protoerr;
		};

		let buf: []u8 = alloc([0...], cl);

		match (io::readall(scan, buf)) {
		case let n: size => void;
		case io::EOF =>
			free(buf);
			return io::EOF;
		case let err: io::error =>
			free(buf);
			if (err is io::underread) {
				return io::EOF;
			} else {
				return err;
			};
		};

		body = alloc(memio::dynamic_from(buf));
	} else if (cl != "" || te != "") {
		return errors::unsupported;
	let body = match (body_scan(scan, &header)) {
	case io::EOF => return io::EOF;
	case let body: io::handle => yield body;
	case let err: protoerr =>
		return err;
	case let err: errors::unsupported =>
		return err;
	case let err: utf8::invalid =>
		return protoerr;
	case let err: io::error =>
		return err;
	};

	return response {
@@ -114,6 +96,14 @@ export fn response_scan(scan: *bufio::scanner) (response | io::EOF | protoerr |
export fn parsed_response_finish(resp: *response) void = {
	free(resp.reason);
	header_finish(&resp.header);
	match (resp.body) {
	case let body: *io::stream =>
		if (body != io::empty) {
			io::close(body)!;
			free(body);
		};
	case => void;
	};
};

export type response_line = struct {
diff --git a/net/http/transport.ha b/net/http/transport.ha
index 6832602..2fb015c 100644
--- a/net/http/transport.ha
+++ b/net/http/transport.ha
@@ -1,7 +1,9 @@
use errors;
use bufio;
use bytes;
use encoding::utf8;
use errors;
use io;
use memio;
use os;
use strconv;
use strings;
@@ -49,26 +51,27 @@ export type transport = struct {
	response_content: content_mode,
};

fn new_reader(
	conn: io::file,
	resp: *response,
fn body_scan(
	scan: *bufio::scanner,
) (io::handle | errors::unsupported | protoerr) = {
	header: *header,
) (io::handle | io::EOF | io::error | utf8::invalid | errors::unsupported | protoerr) = {
	// TODO: Content-Encoding support
	const cl = header_get(&resp.header, "Content-Length");
	const te = header_get(&resp.header, "Transfer-Encoding");
	const cl = header_get(header, "Content-Length");
	const te = header_get(header, "Transfer-Encoding");

	if (cl == "" && te == "") {
		return io::empty;
	};

	if (cl != "" || te == "") {
		let length = types::SIZE_MAX;
		if (cl != "") {
			length = match (strconv::stoz(cl)) {
			match (strconv::stoz(cl)) {
			case let z: size =>
				yield z;
				return identity_body_scan(scan, z)?;
			case =>
				return protoerr;
			};
		};
		return new_identity_reader(conn, scan, length);
	};

	// TODO: Figure out the semantics for closing the stream
@@ -77,236 +80,90 @@ fn new_reader(
	// And it should not close the actual connection if it's still in the
	// connection pool
	// Unless it isn't in the pool, then it should!
	// And this leaks, fix that too
	let stream: io::handle = conn;
	let buffer: []u8 = bufio::scan_buffer(scan);
	const iter = strings::tokenize(te, ",");
	for (true) {
		const te = match (strings::next_token(&iter)) {
		case let tok: str =>
			yield strings::trim(tok);
		case done =>
			break;
			return errors::unsupported;
		};

		// XXX: We could add lzw support if someone added it to
		// hare-compress
		const next = switch (te) {
		switch (te) {
		case "chunked" =>
			yield new_chunked_reader(stream, buffer);
			return chunked_body_scan(scan)?;
		case "deflate" =>
			abort(); // TODO
		case "gzip" =>
			abort(); // TODO
		case =>
			return errors::unsupported;
			continue;
		};
		stream = next;

		buffer = [];
	};

	if (!(stream is *io::stream)) {
		// Empty Transfer-Encoding header
		return protoerr;
	};
	return stream;
};

type identity_reader = struct {
	vtable: io::stream,
	conn: io::file,
	scan: *bufio::scanner,
	src: io::limitstream,
};

const identity_reader_vtable = io::vtable {
	reader = &identity_read,
	closer = &identity_close,
	...
};

// Creates a new reader that reads data until the response's Content-Length is
// Creates a new body handle that contains the full body until Content-Length is
// reached; i.e. the null Transport-Encoding.
fn new_identity_reader(
	conn: io::file,
fn identity_body_scan(
	scan: *bufio::scanner,
	content_length: size,
) *io::stream = {
	const scan = alloc(*scan);
	return alloc(identity_reader {
		vtable = &identity_reader_vtable,
		conn = conn,
		scan = scan,
		src = io::limitreader(scan, content_length),
		...
	});
};
) (io::handle | io::EOF | io::error) = {
	let buf: []u8 = alloc([0...], content_length);

fn identity_read(
	s: *io::stream,
	buf: []u8,
) (size | io::EOF | io::error) = {
	let rd = s: *identity_reader;
	assert(rd.vtable == &identity_reader_vtable);
	return io::read(&rd.src, buf)?;
};

fn identity_close(s: *io::stream) (void | io::error) = {
	let rd = s: *identity_reader;
	assert(rd.vtable == &identity_reader_vtable);

	// Flush the remainder of the response in case the caller did not read
	// it out entirely
	io::copy(io::empty, &rd.src)?;

	bufio::finish(rd.scan);
	free(rd.scan);
	// TODO connection pool
	io::close(rd.conn)?;
	free(rd);
};

type chunk_state = enum {
	HEADER,
	DATA,
	FOOTER,
};

type chunked_reader = struct {
	vtable: io::stream,
	conn: io::handle,
	buffer: [os::BUFSZ]u8,
	state: chunk_state,
	// Amount of read-ahead data in buffer
	pending: size,
	// Length of current chunk
	length: size,
};

fn new_chunked_reader(
	conn: io::handle,
	buffer: []u8,
) *io::stream = {
	let rd = alloc(chunked_reader {
		vtable = &chunked_reader_vtable,
		conn = conn,
		...
	});
	rd.buffer[..len(buffer)] = buffer[..];
	rd.pending = len(buffer);
	return rd;
};

const chunked_reader_vtable = io::vtable {
	reader = &chunked_read,
	closer = &chunked_close,
	...
};

fn chunked_read(
	s: *io::stream,
	buf: []u8,
) (size | io::EOF | io::error) = {
	// XXX: I am not satisfied with this code
	let rd = s: *chunked_reader;
	assert(rd.vtable == &chunked_reader_vtable);

	for (true) switch (rd.state) {
	case chunk_state::HEADER =>
		let crlf = 0z;
		for (true) {
			const n = rd.pending;
			match (bytes::index(rd.buffer[..n], ['\r', '\n'])) {
			case let z: size =>
				crlf = z;
				break;
			case void =>
				yield;
			};
			if (rd.pending >= len(rd.buffer)) {
				// Chunk header exceeds buffer size
				return errors::overflow;
			};

			match (io::read(rd.conn, rd.buffer[rd.pending..])?) {
			case let n: size =>
				rd.pending += n;
			case io::EOF =>
				if (rd.pending > 0) {
					return errors::invalid;
				};
				return io::EOF;
			};
	match (io::readall(scan, buf)) {
	case let n: size => void;
	case io::EOF =>
		free(buf);
		return io::EOF;
	case let err: io::error =>
		free(buf);
		if (err is io::underread) {
			return io::EOF;
		} else {
			return err;
		};
	};

		// XXX: Should we do anything with chunk-ext?
		const header = rd.buffer[..crlf];
		const (ln, _) = bytes::cut(header, ';');
		const ln = match (strings::fromutf8(ln)) {
		case let s: str =>
			yield s;
		case =>
			return errors::invalid;
		};
	return alloc(memio::dynamic_from(buf));
};

		match (strconv::stoz(ln, strconv::base::HEX)) {
		case let z: size =>
			rd.length = z;
		case =>
			return errors::invalid;
		};
		if (rd.length == 0) {
fn chunked_body_scan(
	scan: *bufio::scanner,
) (io::handle | io::EOF | io::error | utf8::invalid | protoerr) = {
	let buf = alloc(memio::dynamic());

	for (true) {
		const header = match (bufio::scan_string(scan, "\r\n")?) {
		case let line: const str =>
			yield line;
		case io::EOF =>
			io::close(buf)!;
			free(buf);
			return io::EOF;
		};
		const length = match (strconv::stoz(header, strconv::base::HEX)) {
		case let z: size =>
			yield z;
		case =>
			return protoerr;
		};
		if (length == 0) {
			io::seek(buf, 0, io::whence::SET)!;
			return buf;
		};

		const n = crlf + 2;
		rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending];
		rd.pending -= n;
		rd.state = chunk_state::DATA;
	case chunk_state::DATA =>
		if (rd.pending == 0) {
			match (io::read(rd.conn, rd.buffer)?) {
			case let n: size =>
				rd.pending += n;
			case io::EOF =>
				return io::EOF;
		match (bufio::scan_string(scan, "\r\n")?) {
		case let line: const str =>
			if (len(line) != length) {
				return protoerr;
			};
			io::write(buf, strings::toutf8(line))?;
		case io::EOF =>
			io::close(buf)!;
			free(buf);
			return io::EOF;
		};
		let n = len(buf);
		if (n > rd.pending) {
			n = rd.pending;
		};
		if (n > rd.length) {
			n = rd.length;
		};
		buf[..n] = rd.buffer[..n];
		rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending];
		rd.pending -= n;
		rd.length -= n;
		rd.state = chunk_state::FOOTER;
		return n;
	case chunk_state::FOOTER =>
		for (rd.pending < 2) {
			match (io::read(rd.conn, rd.buffer[rd.pending..])?) {
			case let n: size =>
				rd.pending += n;
			case io::EOF =>
				return io::EOF;
			};
		};
		if (!bytes::equal(rd.buffer[..2], ['\r', '\n'])) {
			return errors::invalid;
		};
		rd.buffer[..rd.pending - 2] = rd.buffer[2..rd.pending];
		rd.pending -= 2;
		rd.state = chunk_state::HEADER;
	};
};

fn chunked_close(s: *io::stream) (void | io::error) = {
	let rd = s: *chunked_reader;
	// TODO connection pool
	io::close(rd.conn)?;
	free(rd);
};
-- 
2.44.0