{"record":{"id":"631bba26371ce1b8","repo":"tailscale/tailscale","slug":"androiddns-read-with-no-complete-query-written","errorCode":null,"errorMessage":"androiddns: read with no complete query written","messagePattern":"androiddns: read with no complete query written","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"feature/androiddns/resolver.go","lineNumber":52,"sourceCode":"func resolverDial(ctx context.Context, network, address string) (net.Conn, error) {\n\treturn &streamConn{}, nil\n}\n\n// streamConn is a net.Conn that speaks TCP-style framed DNS on one\n// side and dnsproxyd on the other. Go's resolver writes one framed\n// query and then reads one framed answer; the daemon round trip\n// happens on the first Read after a complete query has been written.\ntype streamConn struct {\n\tmu       sync.Mutex\n\tdeadline time.Time\n\twbuf     []byte // accumulated framed query bytes\n\trbuf     []byte // framed answer bytes not yet read\n\tclosed   bool\n}\n\nvar (\n\terrClosed         = errors.New(\"androiddns: use of closed conn\")\n\terrNoPendingQuery = errors.New(\"androiddns: read with no complete query written\")\n)\n\nfunc (c *streamConn) Write(p []byte) (int, error) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.closed {\n\t\treturn 0, errClosed\n\t}\n\tc.wbuf = append(c.wbuf, p...)\n\treturn len(p), nil\n}\n\nfunc (c *streamConn) Read(p []byte) (int, error) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.closed {\n\t\treturn 0, errClosed\n\t}","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/tailscale/tailscale/blob/e2ed432399c9b0fda7aa14e9eb27784d2d893c55/feature/androiddns/resolver.go#L34-L70","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nanswer, err := conn.Read(buf) // errNoPendingQuery\n// after\nmsg := buildDNSQuery(id, name)\nvar framed []byte\nframed = binary.BigEndian.AppendUint16(framed, uint16(len(msg)))\nframed = append(framed, msg...)\nconn.Write(framed)\nanswer, err := conn.Read(buf)","handlingStrategy":"validation","validationCode":"// ensure a complete framed query exists before reading\nif len(query) < 2 || len(query) < 2+int(query[0])<<8|0+int(query[1]) {\n    return errors.New(\"incomplete DNS query frame\")\n}","typeGuard":null,"tryCatchPattern":"if err := conn.roundTrip(); errors.Is(err, errNoPendingQuery) {\n    // write the full framed query first, then retry\n}","preventionTips":["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."],"tags":["android","dns","protocol","sequencing"],"backgroundTag":"invalid-state-transition","analyzedSha":"e2ed432399c9b0fda7aa14e9eb27784d2d893c55","analyzedAt":"2026-09-14T12:49:01.353Z","contentChangedAt":"2026-09-14T12:49:01.353Z","schemaVersion":2},"datasetVersion":"2026-09-14T21:17:11.552Z"}