diff --git a/doq.go b/doq.go index 9d0bdd9..9729607 100644 --- a/doq.go +++ b/doq.go @@ -91,8 +91,9 @@ func newDOQConnPool(uc *UpstreamConfig, addrs []string) *doqConnPool { // Resolve performs a DNS query using a pooled QUIC connection. func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { - // Retry logic for transient errors: io.EOF (connection reset) and - // IdleTimeoutError (stale pooled connection timed out). + // Retry logic for transient errors: io.EOF (connection reset), + // IdleTimeoutError (stale pooled connection timed out), and + // StreamLimitReachedError (stream credit exhausted before server MAX_STREAMS arrived). for range 5 { answer, err := p.doResolve(ctx, msg) if err == io.EOF { @@ -102,6 +103,10 @@ func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro if errors.As(err, &idleErr) { continue } + var streamLimitErr quic.StreamLimitReachedError + if errors.As(err, &streamLimitErr) { + continue + } if err != nil { return nil, wrapCertificateVerificationError(err) } @@ -126,18 +131,25 @@ func (p *doqConnPool) doResolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, er return nil, err } - // Open a new stream for this query - stream, err := conn.OpenStream() + // Ensure the context has a deadline before calling OpenStreamSync, which + // blocks until the server sends a MAX_STREAMS update. Without a deadline the + // call could block indefinitely when the server never sends the update. + deadline, ok := ctx.Deadline() + if !ok { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 5*time.Second) + defer cancel() + deadline, _ = ctx.Deadline() + } + + // OpenStreamSync blocks until the server's MAX_STREAMS credit arrives, + // avoiding the StreamLimitReachedError race that OpenStream (non-blocking) + // triggers when the credit replenishment frame is still in flight. + stream, err := conn.OpenStreamSync(ctx) if err != nil { p.putConn(conn, false) return nil, err } - - // Set deadline - deadline, ok := ctx.Deadline() - if !ok { - deadline = time.Now().Add(5 * time.Second) - } _ = stream.SetDeadline(deadline) // Write message length (2 bytes) followed by message