December 23, 2025, 7:16pm 1
Hey everyone! I have been working on a simple TCP read/write example to understand how the new IO worked, and I have been having trouble setting up the reader to simply read till EOF / the end of stream.
Here’s the small example I have (echo.websocket.org is simply an echo server):
const std = @import("std");
const net = std.net;
const print = std.debug.print;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
pub fn main() !void {
const host = "echo.websocket.org";
const stream = try net.tcpConnectToHost(allocator, host, 443);
defer stream.close();
print("Connecting to: {s}\n", .{host});
const data = "hello!";
var buffer: [1024]u8 = undefined;
var stream_reader = stream.reader(&buffer);
var reader = stream_r...
December 23, 2025, 7:16pm 1
Hey everyone! I have been working on a simple TCP read/write example to understand how the new IO worked, and I have been having trouble setting up the reader to simply read till EOF / the end of stream.
Here’s the small example I have (echo.websocket.org is simply an echo server):
const std = @import("std");
const net = std.net;
const print = std.debug.print;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
pub fn main() !void {
const host = "echo.websocket.org";
const stream = try net.tcpConnectToHost(allocator, host, 443);
defer stream.close();
print("Connecting to: {s}\n", .{host});
const data = "hello!";
var buffer: [1024]u8 = undefined;
var stream_reader = stream.reader(&buffer);
var reader = stream_reader.interface();
var stream_writer = stream.writer(&buffer);
var writer = &stream_writer.interface;
try writer.writeAll(data);
try writer.flush();
print("sending '{s}' to {s}\n", .{ data, host });
var data_slices = [1][]u8{&buffer};
const read_len = try reader.defaultReadVec(&data_slices);
print("read '{s}' with len {d}\n", .{ data_slices[0], read_len });
}
The output of the above is:
Connecting to: echo.websocket.org
sending 'hello!' to echo.websocket.org
read 'hello!
<huge number of 0xAA>' with len 0
I have a number of questions here:
- What is the best way to read a stream to it’s end, throwing error when the buffer is full?
- As seen in the example above, the
read_len/ bytes read is 0 when the stream is shorter than the buffer. What would be the best way to get the actual size? - Somewhat tangentially related, doing
var reader = stream_reader.interface();breaks all code completion for thereaderand I need to do swap it frominterface()tointerface_stateto get it back (which fails compilation).
Thanks!
Hey @sourwheel, welcome to Ziggit!
“Best” depends on the situation. In the case you showed, you know the size of the response because it is the same size as what you sent. You can do that with Reader.take passing in the expected number of bytes. Without knowing more about the problem you are trying to solve, it’s hard give a solid answer. One nice way if you just want to allocate all the bytes is Writer.Allocating
var writer: std.Io.Writer.Allocating = .init(allocator);
defer writer.deinit();
const written = try stream_reader.interface().streamRemaining(&writer.writer);
const buf = try writer.toOwnedSlice();
Most code completion is done with ZLS. ZLS is a best effort LSP. A lot of hard work has been put into it. It works great, but you will find some edge cases where completion may not work. I would suggest not using interface_state. This is unexpected. Instead, look up the docs in the standard library if completion isn’t working for you.
I’m not sure that defaultReadVec is what you want here. Or at least not with reusing the buffer as you currently are. From the docs:
Writes to Reader.buffer or data, whichever has larger capacity.
It is probably returning 0 because it is writing to the Reader.buffer. Because you use the same memory address for both, it happens that the data is written to the place in data_slices but that is happenstance. You really don’t want to reuse the buffer here. The 0 means that no data was written to data_slices, it was all buffered internally.
sourwheel December 23, 2025, 8:06pm 3
Thank you for the welcome and the answer ^^
Sorry, I may have given the wrong impression – I use the echo server mainly to have a live endpoint to test that doesn’t need any setup. What I am interested in here is to be able to read an arbitrary length stream to its end. I am happy if this is handled by giving it a buffer and getting an error if the buffer is too small or allocating while reading.
Got it! That’s what I was doing in the meantime, just wanted to understand if there was anything obvious I might’ve missed.
Fair point!
Trying out the example you gave using std.Io.Writer.Allocating it seems like my understanding is rather lacking around how writer/reader interact with each other. Based on the docs streamRemaining should push everything to the given writer while handling EndOfStream gracefully. And, I’m assuming here, toOwnedSlice would simple give us the internal writer buffer to be used?
However trying it out this way seems to result in an empty buf array – substituting the data_slices, I get
read '' with len 0
Is there anything I’m missing here?
Full main()
pub fn main() !void {
const host = "echo.websocket.org";
const stream = try net.tcpConnectToHost(allocator, host, 443);
defer stream.close();
print("Connecting to: {s}\n", .{host});
const data = "hello!";
var buffer: [1024]u8 = undefined;
var stream_reader = stream.reader(&buffer);
var reader = stream_reader.interface();
var stream_writer = stream.writer(&buffer);
var writer = &stream_writer.interface;
try writer.writeAll(data);
try writer.flush();
print("sending '{s}' to {s}\n", .{ data, host });
var w: std.Io.Writer.Allocating = .init(allocator);
defer w.deinit();
_ = try reader.streamRemaining(&w.writer);
const buf = try w.toOwnedSlice();
print("read '{s}' with len {d}\n", .{ buf, buf.len });
}
After, exploring the code a bit, I think I understand better.
You are connecting to echo.websocket.org using port 443, which expects TLS traffic. You are connecting the tcp socket, but not sending the correct TLS handshake, and so the connection is being closed. Hence the length of 0. If you want to connect using HTTPS, I would suggest either using the std.http.Client or wrapping the tcp connection with std.crypto.tls.Client. The tls client will require more setup.
If you are trying to connect to the websocket part, then you may want to look at something like GitHub - karlseguin/websocket.zig: A websocket implementation for zig so that it will handle the websocket protocol parts for you. If you are trying to learn the websocket protocol, then you will need to write more setup code to handle that.
sourwheel December 23, 2025, 8:40pm 5

That makes a lot of sense, can’t believe I missed it. Thank you very much for all the help again.
Happy to help! Hopefully that gets you unstuck. Feel free to hop back in if you have further questions.
1 Like
squeek502 December 24, 2025, 12:58am 7
FYI defaultReadVec is only intented to be used by implementers of Reader, not users. Calling it like this is almost always a bug, as you’re effectively bypassing the Reader’s vtable.readVec function and supplanting it with the “default” implementation which may be completely inappropriate for the Reader implementation you’re using.
(it might make some sense to move those functions into the VTable struct definition to avoid confusion)
1 Like