fatedier/frp · error

udp write closed listener

Error message

udp write closed listener

What it means

Thrown by UDPListener.WriteMsg (via writeUDPPacket) in frp's UDP listener wrapper. Sending a packet is implemented by pushing to an unbuffered workflow channel l.writeCh; if the listener has already been Closed (which closes writeCh), the channel send panics, and the deferred recover converts that panic into this error. It therefore means: you wrote to a UDP listener after it was shut down.

Source

Thrown at pkg/util/net/udp.go:221

		for {
			packet, ok := <-l.writeCh
			if !ok {
				return
			}

			if addr, ok := packet.RemoteAddr.(*net.UDPAddr); ok {
				_, _ = readConn.WriteToUDP(packet.Buf, addr)
			}
		}
	}()

	return
}

func (l *UDPListener) writeUDPPacket(packet *UDPPacket) (err error) {
	defer func() {
		if errRet := recover(); errRet != nil {
			err = fmt.Errorf("udp write closed listener")
		}
	}()
	l.writeCh <- packet
	return
}

func (l *UDPListener) WriteMsg(buf []byte, remoteAddr *net.UDPAddr) (err error) {
	// only set remote addr here
	packet := &UDPPacket{
		Buf:        buf,
		RemoteAddr: remoteAddr,
	}
	err = l.writeUDPPacket(packet)
	return
}

func (l *UDPListener) Accept() (net.Conn, error) {
	conn, ok := <-l.acceptCh

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Stop all goroutines that call WriteMsg (context cancellation, WaitGroup) before calling UDPListener.Close()
  2. Guard writes with the listener's closed state (check l.closeFlag under a lock) and drop the packet if closed
  3. If you own the send loop, select on <-l.closeCh / ctx.Done() instead of a bare channel send
  4. In frp itself, this usually surfaces during shutdown: upgrade/restart frps and frpc together so both ends stop cleanly

Example fix

// before
l.writeUDPPacket(packet) // may panic->error if listener closed

// after
if l.closeFlag {
    return fmt.Errorf("udp write closed listener")
}
err = l.writeUDPPacket(packet)
Defensive patterns

Strategy: validation

Validate before calling

// before writing, confirm the listener is still open
if l.closeFlag { // package-internal; if external, track Close() in your own flag
    return
}
err := conn.WriteMsg(buf, addr)

Try / catch

if err := udpListener.WriteMsg(buf, addr); err != nil && strings.Contains(err.Error(), "udp write closed listener") {
    // listener already closed: stop the writer goroutine, do not retry
    return
}

Prevention

When it happens

Trigger: Calling UDPListener.WriteMsg (or WriteToUDP-style send) after UDPListener.Close() has run, or concurrently with Close(). The goroutine that drains writeCh has exited because readConn was closed, so the send on the closed channel panics and is recovered here.

Common situations: Client disconnect and server shutdown racing in frps's UDP vhost/proxy handling; a metrics or keepalive goroutine still writing stats/Echo packets after the listener teardown began; test code forgetting to stop a writer goroutine before closing the listener.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/eb6eb88b73410aea. Report an issue: GitHub.