I'm too dumb for Zig's new IO interface You might have heard that Zig 0.15 introduces a new IO interface, with the focus for this release being the new std.Io.Reader and std.Io.Writer types. The old "interfaces" had problems. Like this performance issue that I opened. And it relied on a mix of types, which always confused me, and a lot of anytype - which is generally great, but a poor foundation to build an interface on. I've been slowly upgrading my libraries, and I ran into changes to the tls.Client client used by my smtp library. For the life of me, I just don't understand how it works. Zig has never been known for its documentation, but if we look at the documentation for tls.Client.init , we'll find: pub fn init ( input : * std . Io . Reader , output : * std . Io . Writer , options : Options ) InitError ! Client Initiates a TLS handshake and establishes a TLSv1 . 2 or TLSv1 . 3 session . So it takes one of these new Readers and a new Writer, along with some options (sneak peak, which aren't all optional). It doesn't look like you can just give it a net.Stream , but net.Stream does expose a reader() and writer() method, so that's probably a good place to start: const stream = try std . net . tcpConnectToHost ( allocator , "www.openmymind.net" , 443 ) ; defer stream . close ( ) ; var writer = stream . writer ( & . { } ) ; var reader = stream . reader ( & . { } ) ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { } , ) ; Note that stream.writer() returns a Stream.Writer and stream.reader() returns a Stream.Reader - those aren't the types our tls.Client expects. To convert the Stream.Reader to an *std.Io.Reader , we need to call its interface() method. To get a *std.io.Writer from an Stream.Writer , we need the address of its &interface field. This doesn't seem particularly consistent. Don't forget that the writer and reader need a stable address. Because I'm trying to get the simplest example working, this isn't an issue - everything will live on the stack of main . In a real word example, I think it means that I'll always have to wrap the tls.Client into my own heap-allocated type; giving the writer and reader have a cozy stable home. Speaking of allocations, you might have noticed that stream.writer and stream.reader take a parameter. It's the buffer they should use. Buffering is a first class citizen of the new Io interface - who needs composition? The documentation does tell me these need to be at least std.crypto.tls.max_ciphertext_record_len large, so we need to fix things a bit: var write_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var writer = stream . writer ( & write_buf ) ; var read_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var reader = stream . reader ( & read_buf ) ; Here's where the code stands: const std = @import ( "std" ) ; pub fn main ( ) ! void { var gpa : std . heap . DebugAllocator ( . { } ) = . init ; const allocator = gpa . allocator ( ) ; const stream = try std . net . tcpConnectToHost ( allocator , "www.openmymind.net" , 443 ) ; defer stream . close ( ) ; var write_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var writer = stream . writer ( & write_buf ) ; var read_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var reader = stream . reader ( & read_buf ) ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { } , ) ; defer tls_client . end ( ) catch { } ; } But if you try to run it, you'll get a compilation error. Turns out we have to provide 4 options: the ca_bundle, a host, a write_buffer and a read_buffer . Normally I'd expect the options parameter to be for optional parameters, I don't understand why some parameters (input and output) are passed one way while writer_buffer and read_buffer are passed another. Let's give it what it wants AND send some data: var bundle = std . crypto . Certificate . Bundle { } ; try bundle . rescan ( allocator ) ; defer bundle . deinit ( allocator ) ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { . ca = . { . bundle = bundle } , . host = . { . explicit = "www.openmymind.net" } , . read_buffer = & . { } , . write_buffer = & . { } , } , ) ; defer tls_client . end ( ) catch { } ; try tls_client . writer . writeAll ( "GET / HTTP/1.1\r \r " ) ; Now, if I try to run it, the program just hangs. I don't know what write_buffer is, but I know Zig now loves buffers, so let's try to give it something: var write_buf2 : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { . ca = . { . bundle = bundle } , . host = . { . explicit = "www.openmymind.net" } , . read_buffer = & . { } , . write_buffer = & write_buf2 , } , ) ; defer tls_client . end ( ) catch { } ; try tls_client . writer . writeAll ( "GET / HTTP/1.1\r \r " ) ; Great, now the code doesn't hang, all we need to do is read the response. tls.Client exposes a reader: *std.Io.Reader field which is "Decrypted stream from the server to the client." That sounds like what we want, but believe it or not std.Io.Reader doesn't have a read method. It has a peak a takeByteSigned , a readSliceShort (which seems close, but it blocks until the provided buffer is full), a peekArray and a lot more, but nothing like the read I'd expect. The closest I can find, which I think does what I want, is to stream it to a writer: var buf : [ 1024 ] u8 = undefined ; var w : std . Io . Writer = . fixed ( & buf ) ; const n = try tls_client . reader . stream ( & w , . limited ( buf . len ) ) ; std . debug . print ( "read: {d} - {s} " , . { n , buf [ 0 .. n ] } ) ; If we try to run the code now, it crashes. We've apparently failed an assertion regarding the length of a buffer. So it seems like we also have to provide a read_buffer . Here's my current version (it doesn't work, but it doesn't crash!): const std = @import ( "std" ) ; pub fn main ( ) ! void { var gpa : std . heap . DebugAllocator ( . { } ) = . init ; const allocator = gpa . allocator ( ) ; const stream = try std . net . tcpConnectToHost ( allocator , "www.openmymind.net" , 443 ) ; defer stream . close ( ) ; var write_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var writer = stream . writer ( & write_buf ) ; var read_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var reader = stream . reader ( & read_buf ) ; var bundle = std . crypto . Certificate . Bundle { } ; try bundle . rescan ( allocator ) ; defer bundle . deinit ( allocator ) ; var write_buf2 : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var read_buf2 : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { . ca = . { . bundle = bundle } , . host = . { . explicit = "www.openmymind.net" } , . read_buffer = & read_buf2 , . write_buffer = & write_buf2 , } , ) ; defer tls_client . end ( ) catch { } ; try tls_client . writer . writeAll ( "GET / HTTP/1.1\r \r " ) ; var buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var w : std . Io . Writer = . fixed ( & buf ) ; const n = try tls_client . reader . stream ( & w , . limited ( buf . len ) ) ; std . debug . print ( "read: {d} - {s} " , . { n , buf [ 0 .. n ] } ) ; } When I looked through Zig's source code, there's only one place using tls.Client . It helped to get me where where I am. I couldn't find any tests.