{"record":{"id":"eb6eb88b73410aea","repo":"fatedier/frp","slug":"udp-write-closed-listener","errorCode":null,"errorMessage":"udp write closed listener","messagePattern":"udp write closed listener","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/util/net/udp.go","lineNumber":221,"sourceCode":"\t\tfor {\n\t\t\tpacket, ok := <-l.writeCh\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif addr, ok := packet.RemoteAddr.(*net.UDPAddr); ok {\n\t\t\t\t_, _ = readConn.WriteToUDP(packet.Buf, addr)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn\n}\n\nfunc (l *UDPListener) writeUDPPacket(packet *UDPPacket) (err error) {\n\tdefer func() {\n\t\tif errRet := recover(); errRet != nil {\n\t\t\terr = fmt.Errorf(\"udp write closed listener\")\n\t\t}\n\t}()\n\tl.writeCh <- packet\n\treturn\n}\n\nfunc (l *UDPListener) WriteMsg(buf []byte, remoteAddr *net.UDPAddr) (err error) {\n\t// only set remote addr here\n\tpacket := &UDPPacket{\n\t\tBuf:        buf,\n\t\tRemoteAddr: remoteAddr,\n\t}\n\terr = l.writeUDPPacket(packet)\n\treturn\n}\n\nfunc (l *UDPListener) Accept() (net.Conn, error) {\n\tconn, ok := <-l.acceptCh","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/util/net/udp.go#L203-L239","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Stop all goroutines that call WriteMsg (context cancellation, WaitGroup) before calling UDPListener.Close()","Guard writes with the listener's closed state (check l.closeFlag under a lock) and drop the packet if closed","If you own the send loop, select on <-l.closeCh / ctx.Done() instead of a bare channel send","In frp itself, this usually surfaces during shutdown: upgrade/restart frps and frpc together so both ends stop cleanly"],"exampleFix":"// before\nl.writeUDPPacket(packet) // may panic->error if listener closed\n\n// after\nif l.closeFlag {\n    return fmt.Errorf(\"udp write closed listener\")\n}\nerr = l.writeUDPPacket(packet)","handlingStrategy":"validation","validationCode":"// before writing, confirm the listener is still open\nif l.closeFlag { // package-internal; if external, track Close() in your own flag\n    return\n}\nerr := conn.WriteMsg(buf, addr)","typeGuard":null,"tryCatchPattern":"if err := udpListener.WriteMsg(buf, addr); err != nil && strings.Contains(err.Error(), \"udp write closed listener\") {\n    // listener already closed: stop the writer goroutine, do not retry\n    return\n}","preventionTips":["Close the listener last: cancel writer goroutines (context/WaitGroup) before calling Close()","Never share a UDPListener across goroutines without a documented ownership rule for Close","In tests, use t.Cleanup to close listeners after, not before, dependent goroutines stop"],"tags":["udp","network","concurrency","shutdown"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}