ginuerzh/gost · warning

accpet on closed listener

Error message

accpet on closed listener

What it means

fakeTCPListener.Accept returns this error when the listener's errChan is closed, which happens when the listener has been closed. Accept selects between receiving a connection from connChan and an error from errChan; a closed errChan (ok == false) means the listener is shut down and no further connections will arrive. It is the fakeTCP (relay-over-UDP) equivalent of the standard 'use of closed network connection' error.

Source

Thrown at ftcp.go:139

		select {
		case conn.rChan <- b[:n]:
			if Debug {
				log.Logf("[ftcp] %s >>> %s : length %d", raddr, l.Addr(), n)
			}
		default:
			log.Logf("[ftcp] %s -> %s : recv queue is full (%d)", raddr, l.Addr(), cap(conn.rChan))
		}
	}
}

func (l *fakeTCPListener) Accept() (conn net.Conn, err error) {
	var ok bool
	select {
	case conn = <-l.connChan:
	case err, ok = <-l.errChan:
		if !ok {
			err = errors.New("accpet on closed listener")
		}
	}
	return
}

func (l *fakeTCPListener) Addr() net.Addr {
	return l.ln.LocalAddr()
}

func (l *fakeTCPListener) Close() error {
	err := l.ln.Close()
	l.connMap.Range(func(k interface{}, v *udpServerConn) bool {
		v.Close()
		return true
	})

	return err
}

View on GitHub (pinned to a33fdbf4c9)

Solutions

  1. Exit the accept loop when this error is returned — after listener close it will be returned forever
  2. Coordinate shutdown: signal the accept goroutine to stop before/when calling Close()
  3. Avoid double-closing the listener (use sync.Once around Close)
  4. Log it at debug level; it is expected during orderly shutdown

Example fix

// before
c, err := ln.Accept()
if err != nil { return fmt.Errorf("accept: %w", err) }
// after
c, err := ln.Accept()
if err != nil {
  if strings.Contains(err.Error(), "closed listener") { return nil } // listener closed, stop loop
  return fmt.Errorf("accept: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track shutdown state explicitly before looping
select {
case <-ln.closedChan:
    return // do not call Accept
default:
}

Type guard

func isListenerClosedErr(err error) bool { return err != nil && strings.Contains(err.Error(), "closed listener") }

Try / catch

conn, err := ln.Accept()
if err != nil {
    if isListenerClosedErr(err) { return nil } // expected at shutdown
    return fmt.Errorf("fakeTCP accept: %w", err)
}

Prevention

When it happens

Trigger: Calling Accept() on a fakeTCPListener after Close() closed errChan, or racing with Close() during shutdown while an accept loop is still running.

Common situations: Relay service shutdown while worker goroutines still sit in Accept; double-close of the listener from both a timeout handler and an explicit Stop; accept loops that treat every Accept error as retryable and spin on the closed channel.

Related errors


AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02). Data as JSON: /api/errors/e88a9668bf2faa0f. Report an issue: GitHub.