Skip to content

Strict Client

go-while edited this page Oct 23, 2026 · 2 revisions

Strict Client

Package client (doc.go) is a test client for NNTP servers. In strict mode it reports wire-level mistakes that lenient newsreaders hide.

Connecting

Function Effect
Dial(t, addr) connect, read the greeting, close when the test ends; fail the test on error
DialWith(t, addr, deadline) the same with its own deadline
TryDial(addr, deadline) the same without testing; close it yourself
New(t, conn) wrap an open net.Conn (TLS, net.Pipe, proc stdio); no greeting is read
c.WithTB(t) attach a test to a TryDial connection
c.For(t) a view of c that fails t instead, for subtests sharing one connection

All of them turn strict mode on. c.Greeting holds the greeting line. A deadline of 0 is the hang guard (NNTP_TEST_DEADLINE, default 60s).

Commands and replies

Method Effect
Cmd(format, args...) send one line and CRLF, read the status: (code, line); fails the test on error
TryCmd(...) the same, returning the error
ReadDotLines() / TryReadDotLines() a multi-line reply: un-stuffed, line ends stripped
RawUntilDot() / TryRawUntilDot() the exact bytes up to and including the terminator
Send(raw) raw bytes, nothing added: for pipelining and POST payloads
ReadLine(), ReadStatus(), ReadGreeting() one line, one status line, the greeting
ExpectNoPending(d) nothing may arrive within d
Compress() / TryCompress() send COMPRESS DEFLATE; on 206 compress both directions (see below)
StartCompress(), Compressed() compress from now on without sending a command; whether compression is on
Close() safe to call twice, and on nil

Cmd does not check the code; compare it yourself, or use client.ExpectCode(code, line, want...). ExpectLines(got, want), ParseCode and Unstuff are text helpers.

The methods without Try fail the test. On a TryDial connection without WithTB they panic, so use the Try forms there.

What strict mode reports

A *client.StrictError with one of these kinds:

Kind When
BareLF a line ends in LF without CR
BareCR a CR inside a line
NoCRLF the connection ends (EOF or deadline) after data without a line end
BadDotStuffing a line of a multi-line reply starts with . but is neither .. nor the terminator
NoTerminator a multi-line reply ends without .\r\n
Pending ExpectNoPending saw bytes (they stay unread; Bytes holds them)

StrictError has Kind, Line (the raw line), Bytes and Err. Err is the read error and Unwrap returns it, so errors.Is(err, io.EOF) works:

lines, err := c.TryReadDotLines()
var serr *client.StrictError
if errors.As(err, &serr) && serr.Kind == client.NoTerminator {
	// the reply was cut: lines holds what arrived
}

Call ExpectNoPending after a reply to catch a stray byte or a second status line, for example a NUL after 400 too many users. It never consumes data and returns nil on timeout or EOF.

With c.Strict = false a bare LF counts as a line end, like a lenient newsreader.

COMPRESS DEFLATE

c.Compress() sends COMPRESS DEFLATE (RFC 8054) and, when the server answers 206, compresses the connection in both directions from the end of the 206 line on. TryCompress() returns (code, line, err): any other reply (403, 500, 501, 502, 503) is an error and the connection stays plain. After a test sent the command itself and read the 206, StartCompress() does the switch alone.

c := client.Dial(t, addr)
c.Compress()             // 206, or the test fails
c.Cmd("CAPABILITIES")    // sent compressed; the reply is inflated
caps := c.ReadDotLines() // must not list COMPRESS DEFLATE any more
if code, line := c.Cmd("COMPRESS DEFLATE"); code != 502 {
	t.Errorf("second COMPRESS: %q, want 502", line)
}

What stays the same and what to know:

  • Every Send and Cmd is deflated (raw DEFLATE, no zlib header) and sync-flushed, so the server can decode each command at once.
  • Strict mode, ReadDotLines, RawUntilDot and ExpectNoPending work on the inflated stream. An empty flush block inflates to nothing, so ExpectNoPending still waits its time and returns nil.
  • The inflater passes on each byte as soon as it can be decoded, whatever flush the server uses. INN's nnrpd ends each response with zlib's Z_PARTIAL_FLUSH; Go's compress/flate reader holds such a response back until more data comes, so a Go client built on it hangs on INN. The kit has its own inflater for that reason.
  • The hang guard and other read deadlines apply to the inflated stream: a timeout loses nothing, and the rest can still be read.
  • A server that closes the connection reads as io.EOF; corrupt compressed data is an error wrapping flate.CorruptInputError.
  • To send bytes past the compressor (corrupt input for a server test), keep c.Conn from before Compress.
  • The client doesn't stop a test from sending a second COMPRESS, STARTTLS or AUTHINFO: RFC 8054 wants 502 for all of them.

Clone this wiki locally