OpenNHP/opennhp · error

duplicate in-flight counter

Error message

duplicate in-flight counter

What it means

handleRelay responds with HTTP 409 'duplicate in-flight counter' when the same client (identified by realAddrKey) already has a waiter registered for the same inner packet counter. Since responses are matched to requests by (instance, counter, client), a concurrent duplicate would be ambiguous, so the relay rejects it fast while holding pendingMu only briefly.

Solutions

  1. Regenerate a fresh counter (NextCounterIndex) for each request before sending
  2. Disable automatic transport-level retries, or make retries rebuild the packet with a new counter
  3. Serialize requests per client, or wait for the in-flight response before reusing a counter
  4. Verify the client increments the counter at bytes [16:24] on every send

Example fix

// before: retry reuses the same packet/counter
for try := 0; try < 3; try++ { send(packet) }
// after
for try := 0; try < 3; try++ {
    packet = packetWithNewCounter() // fresh counter at [16:24]
    send(packet)
}
Defensive patterns

Strategy: validation

Validate before calling

// always mint a fresh counter before each send
binary.BigEndian.PutUint64(header[16:24], nextCounter())

Try / catch

if resp.StatusCode == http.StatusConflict {
    b, _ := io.ReadAll(resp.Body)
    if strings.Contains(string(b), "duplicate in-flight counter") {
        return errors.New("counter reuse detected; rebuild packet with a new counter instead of retrying the same bytes")
    }
}

Prevention

When it happens

Trigger: POSTing two requests concurrently with identical inner packet counters from the same client IP — e.g. a client retrying with the same serialized packet before the first response arrives, or a buggy client that never increments its packet counter.

Common situations: HTTP client timeout/retry at the transport layer resending the exact same body; client code reusing a cached/stale packet with a fixed counter; parallel workers sharing one packet buffer.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/74c2b245f5cc0e2a. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/relay/relay.go:1046

	log.Info("[Relay] forwarding %d-byte inner packet (counter=%d, server=%s) from client %s to %s (sticky=%v)",
		n, innerCounter, cr.id, realAddr, inst.addr, cr.sticky)

	// Register a pending request under (counter, realAddr) on the instance.
	// The connection routine dispatches the server's ACK/COK to this channel
	// only if this handler is the sole waiter on this counter — see the
	// ambiguity check in connectionRoutine above.
	responseCh := make(chan []byte, 1)
	inst.pendingMu.Lock()
	waiters, ok := inst.pendingRequests[innerCounter]
	if !ok {
		waiters = make(map[string]chan []byte)
		inst.pendingRequests[innerCounter] = waiters
	}
	if _, dup := waiters[realAddrKey]; dup {
		// Same client reusing the same counter concurrently — reject fast.
		inst.pendingMu.Unlock()
		http.Error(w, "duplicate in-flight counter", http.StatusConflict)
		return
	}
	waiters[realAddrKey] = responseCh
	inst.pendingMu.Unlock()

	// Ensure cleanup on timeout / early return.
	defer func() {
		inst.pendingMu.Lock()
		if waiters, ok := inst.pendingRequests[innerCounter]; ok {
			delete(waiters, realAddrKey)
			if len(waiters) == 0 {
				delete(inst.pendingRequests, innerCounter)
			}
		}
		inst.pendingMu.Unlock()
	}()

	// Construct RelayForwardMsg (standard JSON body).

View on GitHub (pinned to 6e04ca5ff0)