Skip to content

liteapi/pool: BestMasterchainInfoClient returns a client with a nil conn, panicking instead of returning ErrNoConnections #503

Description

@McSim85

Summary

ConnPool.BestMasterchainInfoClient() wraps p.bestConnection() without checking it for nil. When
the pool has no healthy connection, bestConnection() returns nil and the returned
*MasterchainInfoClient panics on first use with a nil pointer dereference, rather than surfacing
ErrNoConnections.

Present in v1.17.0 and still on main as of v1.23.0.

The code

liteapi/pool/conn_pool.go#L264-L269 and #L255-L262 (v1.17.0):

func (p *ConnPool) BestMasterchainInfoClient() *MasterchainInfoClient {
	return &MasterchainInfoClient{
		conn: p.bestConnection(),   // nil when no connection is healthy
	}
}

func (s *MasterchainInfoClient) LiteServerGetMasterchainInfo(ctx context.Context) (liteclient.LiteServerMasterchainInfoC, error) {
	info, err := s.conn.Client().LiteServerGetMasterchainInfo(ctx)   // panics: s.conn is nil
	...
}

The sibling function immediately below — #L271-L276 — already handles exactly this case, which is what makes the omission look unintentional:

func (p *ConnPool) BestMasterchainClient(ctx context.Context) (*liteclient.Client, ton.BlockIDExt, error) {
	bestConnection := p.bestConnection()
	if bestConnection == nil {
		return nil, ton.BlockIDExt{}, ErrNoConnections
	}
	...
}

LiteServerGetMasterchainInfoExt on the same type has the same problem, and ErrNoConnections already exists.

Still present on main today: BestMasterchainInfoClient#L295-L299, LiteServerGetMasterchainInfo#L283, guarded sibling at #L302.

Reproduction

Construct a client against a liteserver endpoint that is not accepting connections (for example, a
local node that has not yet bound its port), then call GetMasterchainInfo:

panic: runtime error: invalid memory address or nil pointer dereference
  github.com/tonkeeper/tongo/liteapi/pool.(*MasterchainInfoClient).LiteServerGetMasterchainInfo
      liteapi/pool/conn_pool.go:256
  github.com/tonkeeper/tongo/liteapi.(*Client).GetMasterchainInfo
      liteapi/client.go:324

Why

We hit this in production. A service using tongo started moments before its local liteserver was
accepting connections, so the pool was built empty. Because the pool is constructed once and the
nil is never re-checked, every subsequent request panicked for the lifetime of the process
the condition never cleared, even long after the liteserver became available.

Inside an HTTP handler this is particularly hard to diagnose: net/http recovers the panic per
connection, so the process stays up and its health endpoint keeps returning 200, while every real
request dies with EOF / "empty reply from server" and no HTTP status. From the outside it looks
like a network fault rather than an application bug.

An ErrNoConnections return would have been retried and recovered automatically.

Suggested fix

Mirror BestMasterchainClient — return the error rather than a client that cannot be used:

func (p *ConnPool) BestMasterchainInfoClient() (*MasterchainInfoClient, error) {
	bestConnection := p.bestConnection()
	if bestConnection == nil {
		return nil, ErrNoConnections
	}
	return &MasterchainInfoClient{conn: bestConnection}, nil
}

If changing the signature is undesirable for compatibility, a nil check inside
LiteServerGetMasterchainInfo / LiteServerGetMasterchainInfoExt returning ErrNoConnections
would fix the panic without an API break.

Happy to send a PR for either shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions