tailscale/tailscale · error
androiddns: read with no complete query written
Error message
androiddns: read with no complete query written
What it means
roundTripLocked validates that the accumulated write buffer holds at least one complete DNS message: a 2-byte big-endian length prefix followed by that many payload bytes. If fewer than 2 bytes are buffered, or fewer than 2+msgLen, no complete query has been written yet, so reading an answer would be premature and errNoPendingQuery is returned.
Source
Thrown at feature/androiddns/resolver.go:52
func resolverDial(ctx context.Context, network, address string) (net.Conn, error) {
return &streamConn{}, nil
}
// streamConn is a net.Conn that speaks TCP-style framed DNS on one
// side and dnsproxyd on the other. Go's resolver writes one framed
// query and then reads one framed answer; the daemon round trip
// happens on the first Read after a complete query has been written.
type streamConn struct {
mu sync.Mutex
deadline time.Time
wbuf []byte // accumulated framed query bytes
rbuf []byte // framed answer bytes not yet read
closed bool
}
var (
errClosed = errors.New("androiddns: use of closed conn")
errNoPendingQuery = errors.New("androiddns: read with no complete query written")
)
func (c *streamConn) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return 0, errClosed
}
c.wbuf = append(c.wbuf, p...)
return len(p), nil
}
func (c *streamConn) Read(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return 0, errClosed
}View on GitHub (pinned to e2ed432399)
Solutions
- Write the complete framed DNS query (2-byte length prefix plus full message) before calling Read/Receive.
- Ensure Write was called with the entire query in one or more calls before attempting the round trip.
- Handle errNoPendingQuery as 'nothing to answer yet' and retry after writing.
- Check for short writes on the sending side that leave wbuf incomplete.
Example fix
// before answer, err := conn.Read(buf) // errNoPendingQuery // after msg := buildDNSQuery(id, name) var framed []byte framed = binary.BigEndian.AppendUint16(framed, uint16(len(msg))) framed = append(framed, msg...) conn.Write(framed) answer, err := conn.Read(buf)
Defensive patterns
Strategy: validation
Validate before calling
// ensure a complete framed query exists before reading
if len(query) < 2 || len(query) < 2+int(query[0])<<8|0+int(query[1]) {
return errors.New("incomplete DNS query frame")
} Try / catch
if err := conn.roundTrip(); errors.Is(err, errNoPendingQuery) {
// write the full framed query first, then retry
} Prevention
- Always write the 2-byte length prefix plus full message before Read.
- Check for short writes truncating the query.
- Wrap query construction and write in a single helper.
When it happens
Trigger: Calling Read/Receive (which invokes roundTripLocked) before writing a full DNS query: either nothing was written (wbuf < 2 bytes) or only a partial message was written (wbuf shorter than the declared framed length).
Common situations: Caller issues Read before any Write; caller writes a partial DNS frame (e.g. writes the length prefix, then the body in a second step, and reads in between); a short-write bug truncated the query.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- androiddns: bogus answer length %d
- androiddns: use of closed conn
- androiddns: %d byte query too large for dnsproxyd command bu
- androiddns: %w
- androiddns: reading result: %w
AI-assisted analysis of tailscale/tailscale@e2ed432399 (2026-09-14).
Data as JSON: /api/errors/631bba26371ce1b8.
Report an issue: GitHub.