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

  1. Write the complete framed DNS query (2-byte length prefix plus full message) before calling Read/Receive.
  2. Ensure Write was called with the entire query in one or more calls before attempting the round trip.
  3. Handle errNoPendingQuery as 'nothing to answer yet' and retry after writing.
  4. 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

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


AI-assisted analysis of tailscale/tailscale@e2ed432399 (2026-09-14). Data as JSON: /api/errors/631bba26371ce1b8. Report an issue: GitHub.