fatedier/frp · error
natHoleRespMsg get error info: %s
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.
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)
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 error: %v
- wait detect message timeout
- wait detect message canceled
- open tunnel timeout
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/298fbd37ba6fdd7a.
Report an issue: GitHub.