fatedier/frp · error
natHoleRespMsg get error info
Error message
natHoleRespMsg get error info: %s
What it means
The NatHoleResp message arrived intact but carries a non-empty Error field: the frps server explicitly rejected the hole-punch transaction. The message string is the server's own error text (e.g. transaction not found, no counterpart, analysis failed), so it is the authoritative reason for the refusal.
Solutions
- Read the embedded server error text — it names the exact server-side refusal reason
- Verify the sk is running and the target proxy is registered before the visitor connects
- Retry the visit: transient analysis failures (race between client and visitor prepare messages) often succeed on a second attempt
- If both endpoints are behind symmetric NAT, configure a fallback (STUN server config or direct relay via server)
Defensive patterns
Strategy: try-catch
Try / catch
resp, err := nathole.ExchangeInfo(ctx, tp, laneKey, m, timeout)
if err != nil && strings.Contains(err.Error(), "natHoleRespMsg get error info") {
// server-side refusal; err text after ': ' is the server's reason
return fmt.Errorf("punch refused by server: %w", err)
} Prevention
- Ensure the target client is online and registered before the visitor dials
- Retry once after a short delay to absorb client/visitor prepare races
- Keep a relay fallback for server-refused transactions
When it happens
Trigger: ExchangeInfo receives NatHoleResp with Error != "" — frps could not match client and visitor, the transaction ID expired, or the server-side analysis concluded hole punching is impossible.
Common situations: Visitor trying to reach an offline or not-yet-registered client; both peers behind symmetric NATs so the server analysis gives up; transaction timeout because client and visitor did not send their NatHolePrepare within the server's window.
Related errors
- natHoleRespMsg get empty candidate addresses
- wait detect message canceled
- wait detect message error
- wait detect message timeout
- classify client nat feature error
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/298fbd37ba6fdd7a.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/nathole/nathole.go:183
ctx context.Context, transporter transport.MessageTransporter,
laneKey string, m msg.Message, timeout time.Duration,
) (*msg.NatHoleResp, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var natHoleRespMsg *msg.NatHoleResp
m, err := transporter.Do(timeoutCtx, m, laneKey, msg.TypeNameNatHoleResp)
if err != nil {
return nil, fmt.Errorf("get natHoleRespMsg error: %v", err)
}
mm, ok := m.(*msg.NatHoleResp)
if !ok {
return nil, fmt.Errorf("get natHoleRespMsg error: invalid message type")
}
natHoleRespMsg = mm
if natHoleRespMsg.Error != "" {
return nil, fmt.Errorf("natHoleRespMsg get error info: %s", natHoleRespMsg.Error)
}
if len(natHoleRespMsg.CandidateAddrs) == 0 {
return nil, fmt.Errorf("natHoleRespMsg get empty candidate addresses")
}
return natHoleRespMsg, nil
}
// MakeHole is used to make a NAT hole between client and visitor.
func MakeHole(ctx context.Context, listenConn *net.UDPConn, m *msg.NatHoleResp, key []byte) (*net.UDPConn, *net.UDPAddr, error) {
xl := xlog.FromContextSafe(ctx)
transactionID := NewTransactionID()
sendToRangePortsFunc := func(conn *net.UDPConn, addr string) error {
return sendSidMessage(ctx, conn, m.Sid, transactionID, addr, key, m.DetectBehavior.TTL)
}
listenConns := []*net.UDPConn{listenConn}
var detectAddrs []string
if m.DetectBehavior.Role == DetectRoleSender {View on GitHub (pinned to 6c8a8d0a97)