# How to Debug Connection Close Errors in XQUIC: A Complete Guide

> Debug XQUIC connection close errors effectively. Verify return values, inspect logs for draining or closed states, check timers, and trace transport send failures. A complete guide.

- Repository: [Alibaba/xquic](https://github.com/alibaba/xquic)
- Tags: how-to-guide
- Published: 2026-02-24

---

**To debug connection close errors in XQUIC, verify the return value of `xqc_conn_close` for errors like `-XQC_ECONN_NFOUND`, inspect connection state logs for `DRAINING` or `CLOSED` statuses, confirm the `XQC_TIMER_LINGER_CLOSE` timer fires when linger is enabled, and trace failures in `xqc_conn_immediate_close` or the transport send callback.**

When working with the **alibaba/xquic** library, understanding how to debug connection close errors in XQUIC is essential for building reliable QUIC and HTTP/3 applications. The connection teardown process involves multiple state transitions, timer management, and transport callbacks that can fail silently if not properly monitored. This guide walks through the internal mechanics of `xqc_conn_close` and provides actionable debugging strategies based on the actual source code implementation.

## Understanding the Connection Close Path in XQUIC

The XQUIC transport layer provides two primary mechanisms for terminating connections: **graceful close** with an optional linger phase, and **immediate abort**. Errors typically occur during state lookups, timer scheduling, or frame generation.

### Entry Point: `xqc_conn_close`

The primary API for closing connections resides in [`src/transport/xqc_conn.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c):

```c
xqc_int_t xqc_conn_close(xqc_engine_t *engine, const xqc_cid_t *cid)

```

*Implementation*: <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c#L3119-L3148>

**Key execution steps:**

1. **Hash table lookup** – Retrieves the `xqc_connection_t` using the provided CID. Returns `-XQC_ECONN_NFOUND` if absent.
2. **State logging** – Emits `XQC_LOG_INFO` with current state and connection flags.
3. **Duplicate close protection** – Returns `XQC_OK` immediately if state is already `DRAINING`.
4. **Linger evaluation** – If `linger_on` is enabled and the send queue contains unsent packets, schedules `XQC_TIMER_LINGER_CLOSE` and returns without calling immediate close.
5. **Immediate close fallback** – Invokes `xqc_conn_immediate_close` when linger is disabled or the queue is empty.

### Immediate Close: `xqc_conn_immediate_close`

When linger is bypassed or the timer expires, the system executes:

```c
xqc_int_t xqc_conn_immediate_close(xqc_connection_t *conn)

```

*Implementation*: <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c#L3167-L3230>

**Execution flow:**

| Step | Action |
|------|--------|
| **State validation** | Ignores if already `DRAINING` to prevent double processing. |
| **Early server abort** | If no Initial packet received, transitions directly to `CLOSED`. |
| **Resource cleanup** | Calls `xqc_conn_shutdown` to clear timers and drop the send queue. |
| **State transition** | Sets state to `CLOSING` and emits `CON_CONNECTION_STATE_UPDATED`. |
| **Frame generation** | Creates `CONNECTION_CLOSE` frame (limited to `MAX_RSP_CONN_CLOSE_CNT` attempts). |
| **Error propagation** | Returns any error from `xqc_write_conn_close_to_packet`. |

### The Linger Timer Mechanism

The **linger** phase allows queued data to transmit before terminating. The timer is scheduled in `xqc_conn_close` (lines 44-48) and handled in [`src/transport/xqc_timer.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_timer.c).

**Debugging tip:** If the connection closes immediately without lingering, verify:
- `conn->conn_settings.linger.linger_on` equals `1`
- `xqc_send_queue_out_queue_empty()` returns `false`

### Engine Queue Management

After scheduling close, the engine must process the connection:

```c
xqc_engine_remove_wakeup_queue(engine, conn);
xqc_engine_add_active_queue(engine, conn);
xqc_engine_wakeup_once(engine);

```

*Location*: Lines 58-62 in [`xqc_conn.c`](https://github.com/alibaba/xquic/blob/main/xqc_conn.c). Failures here prevent the close frame from entering the packetization pipeline.

## Common Symptoms and Root Causes

| Symptom | Typical Cause | Resolution |
|---------|--------------|------------|
| `-XQC_ECONN_NFOUND` | CID not found in engine hash table | Verify connection creation succeeded; check for double-close |
| No state change logged | Connection already in `DRAINING` or `CLOSED` | Review log timestamps for earlier close attempts |
| Linger timer never fires | `linger_on` disabled or send queue empty | Check `conn_settings.linger.linger_on` and queue depth |
| `xqc_conn_immediate_close` returns non-zero | Send-queue or timer failures | Inspect `xqc_write_conn_close_to_packet` return value |
| Peer never receives close frame | Transport callback failure | Verify `conn_send_packet` callback implementation |

## Step-by-Step Debugging Workflow

### Step 1: Verify the Return Value of `xqc_conn_close`

Always check the return code to catch lookup failures immediately:

```c
rc = xqc_conn_close(ctx.engine, &user_conn->cid);
if (rc != XQC_OK) {
    xqc_log_error("xqc_conn_close failed: %d", rc);
    if (rc == -XQC_ECONN_NFOUND) {
        xqc_log_warn("Connection CID not found - possibly already closed");
    }
}

```

*Reference*: <https://github.com/alibaba/xquic/blob/main/tests/test_client.c#L3285-L3287>

### Step 2: Inspect Connection State Logs

Search logs for the state dump performed at the start of `xqc_conn_close`:

```

|conn:0x7f...|state:CLOSING|flag:...

```

If the state is **`DRAINING`** or **`CLOSED`**, the function returns early (lines 36-38). Look for `XQC_LOG_INFO` entries to confirm the pre-close state.

### Step 3: Determine If Linger Is Active

Check the linger configuration and send queue status:

```c
if (conn->conn_settings.linger.linger_on) {
    if (!xqc_send_queue_out_queue_empty(conn->conn_send_queue)) {
        xqc_log_debug("Linger active: timer scheduled");
    } else {
        xqc_log_debug("Linger enabled but queue empty - immediate close");
    }
}

```

Confirm that `XQC_TIMER_LINGER_CLOSE` events appear in the log. Missing timer events indicate either disabled linger or an empty send queue.

### Step 4: Verify `xqc_conn_immediate_close` Success

When linger is bypassed, trace errors from the immediate close path:

```

|xqc_write_conn_close_to_packet error|ret:%d|

```

Search logs for `xqc_write_conn_close_to_packet` to identify frame generation failures. Non-zero return values here prevent the `CONNECTION_CLOSE` frame from entering the packetization buffer.

### Step 5: Examine Transport Callbacks

The actual transmission depends on the transport callback **`conn_send_packet_before_accept`** (or your registered send callback). If this callback returns an error, the close frame never reaches the network.

Verify your callback implementation passed to `xqc_engine_init`:

```c
xqc_transport_callbacks_t transport_cbs = {
    .conn_send_packet = my_send_packet_callback,
    // ... other callbacks
};

```

Check that `my_send_packet_callback` handles the packet buffer correctly and returns `XQC_OK` on success.

## Code Examples for Debugging Scenarios

### Handling Graceful Close with Error Checking

```c
xqc_int_t rc = xqc_conn_close(ctx.engine, &user_conn->cid);
if (rc != XQC_OK) {
    xqc_log_error("Graceful close failed: %d", rc);
    // Handle specific error codes
    switch (rc) {
        case -XQC_ECONN_NFOUND:
            xqc_log_warn("Connection not found in engine");
            break;
        default:
            xqc_log_error("Unexpected close error");
    }
}

```

*Reference*: <https://github.com/alibaba/xquic/blob/main/tests/test_client.c#L3285-L3290>

### Forcing Immediate Close for Error Recovery

When you need to abort immediately without lingering:

```c
xqc_connection_t *conn = xqc_engine_conns_hash_find(ctx.engine,
                                                    &user_conn->cid, 's');
if (conn) {
    xqc_int_t rc = xqc_conn_immediate_close(conn);
    if (rc != XQC_OK) {
        xqc_log_error("Immediate close failed: %d", rc);
    }
} else {
    xqc_log_warn("Cannot force close: connection not found");
}

```

### Disabling Linger for Debugging Purposes

To eliminate linger behavior during testing:

```c
xqc_conn_settings_t settings = XQC_DEFAULT_CONN_SETTINGS;
settings.linger.linger_on = 0;  // Disable linger
engine->default_conn_settings = settings;

```

*Where to set*: In `xqc_engine_init` or before connection creation (see [`demo/demo_server.c`](https://github.com/alibaba/xquic/blob/main/demo/demo_server.c)).

### Checking Send Queue Depth Before Close

```c
if (!xqc_send_queue_out_queue_empty(conn->conn_send_queue)) {
    xqc_log_debug("Send queue not empty – connection will linger");
} else {
    xqc_log_debug("Send queue empty – immediate close path");
}

```

*Reference*: <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c#L44-L47>

## Key Source Files Reference

| File | Role | Direct Link |
|------|------|-------------|
| [`src/transport/xqc_conn.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c) | Core connection close logic (`xqc_conn_close`, `xqc_conn_immediate_close`) | <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_conn.c> |
| [`src/transport/xqc_timer.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_timer.c) | Linger timer handling (`XQC_TIMER_LINGER_CLOSE`) | <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_timer.c> |
| [`src/transport/xqc_engine.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_engine.c) | Engine hash table and wake-up queues | <https://github.com/alibaba/xquic/blob/main/src/transport/xqc_engine.c> |
| [`tests/test_client.c`](https://github.com/alibaba/xquic/blob/main/tests/test_client.c) | Example of `xqc_conn_close` invocation and error handling | <https://github.com/alibaba/xquic/blob/main/tests/test_client.c#L3285-L3290> |
| [`tests/test_server.c`](https://github.com/alibaba/xquic/blob/main/tests/test_server.c) | Server-side close usage and logging patterns | <https://github.com/alibaba/xquic/blob/main/tests/test_server.c#L254-L279> |
| [`demo/demo_client.c`](https://github.com/alibaba/xquic/blob/main/demo/demo_client.c) | Real-world client demo showing graceful shutdown | <https://github.com/alibaba/xquic/blob/main/demo/demo_client.c> |
| [`demo/demo_server.c`](https://github.com/alibaba/xquic/blob/main/demo/demo_server.c) | Server demo with connection close callbacks | <https://github.com/alibaba/xquic/blob/main/demo/demo_server.c> |

## Summary

- **Check return codes immediately**: `xqc_conn_close` returns `-XQC_ECONN_NFOUND` if the CID is missing from the engine's hash table.
- **Monitor connection states**: Look for `DRAINING` or `CLOSED` states in logs to identify duplicate close attempts.
- **Verify linger behavior**: Ensure `XQC_TIMER_LINGER_CLOSE` fires when `linger_on` is enabled and the send queue contains data.
- **Trace immediate close errors**: Search logs for `xqc_write_conn_close_to_packet` failures that prevent `CONNECTION_CLOSE` frame generation.
- **Validate transport callbacks**: Confirm your `conn_send_packet` callback returns `XQC_OK` to ensure close frames reach the network.

## Frequently Asked Questions

### What does `-XQC_ECONN_NFOUND` mean when closing a connection?

This error code indicates that `xqc_conn_close` could not locate the connection ID in the engine's internal hash table. This typically occurs if you attempt to close a connection that was already destroyed, if the CID was never successfully created, or if there is a mismatch between the CID used for creation and the one used for closing. Verify that the connection handle is valid and that you are not calling close twice on the same connection.

### Why does my connection close immediately without lingering?

Immediate close bypasses the linger phase when either `conn_settings.linger.linger_on` is set to `0` (disabled), or when the send queue is already empty. Check the connection settings before creation to ensure linger is enabled, and verify that data remains in the send queue by checking `xqc_send_queue_out_queue_empty()`. If you are closing immediately after sending data, ensure you have processed the send callbacks to queue packets before initiating close.

### How can I tell if the `CONNECTION_CLOSE` frame was actually sent?

The frame generation occurs inside `xqc_conn_immediate_close` via `xqc_write_conn_close_to_packet`. To verify transmission, search your logs for the string `xqc_write_conn_close_to_packet`—a non-zero return value indicates frame generation failure. Additionally, confirm that your transport callback `conn_send_packet` (registered in `xqc_transport_callbacks_t`) successfully writes the packet to the socket and returns `XQC_OK`. Packet capture tools can also verify the `CONNECTION_CLOSE` frame appears on the wire.

### Where should I look if `xqc_conn_close` returns success but the peer never receives the close?

When the local API succeeds but the remote peer does not receive the termination signal, investigate the transport layer and network path. First, verify in [`src/transport/xqc_engine.c`](https://github.com/alibaba/xquic/blob/main/src/transport/xqc_engine.c) that `xqc_engine_wakeup_once` actually processes the connection—check that the connection is added to the active queue via `xqc_engine_add_active_queue`. Next, examine your custom `conn_send_packet` callback for silent failures or buffer overflows that might drop the close packet. Finally, check for middleboxes or firewalls that might block the final QUIC packets after the handshake completes.