How to Debug Connection Close Errors in XQUIC: A Complete Guide
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:
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:
- Hash table lookup – Retrieves the
xqc_connection_tusing the provided CID. Returns-XQC_ECONN_NFOUNDif absent. - State logging – Emits
XQC_LOG_INFOwith current state and connection flags. - Duplicate close protection – Returns
XQC_OKimmediately if state is alreadyDRAINING. - Linger evaluation – If
linger_onis enabled and the send queue contains unsent packets, schedulesXQC_TIMER_LINGER_CLOSEand returns without calling immediate close. - Immediate close fallback – Invokes
xqc_conn_immediate_closewhen 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:
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.
Debugging tip: If the connection closes immediately without lingering, verify:
conn->conn_settings.linger.linger_onequals1xqc_send_queue_out_queue_empty()returnsfalse
Engine Queue Management
After scheduling close, the engine must process the connection:
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. 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:
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:
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:
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
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:
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:
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).
Checking Send Queue Depth Before Close
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
Summary
- Check return codes immediately:
xqc_conn_closereturns-XQC_ECONN_NFOUNDif the CID is missing from the engine's hash table. - Monitor connection states: Look for
DRAININGorCLOSEDstates in logs to identify duplicate close attempts. - Verify linger behavior: Ensure
XQC_TIMER_LINGER_CLOSEfires whenlinger_onis enabled and the send queue contains data. - Trace immediate close errors: Search logs for
xqc_write_conn_close_to_packetfailures that preventCONNECTION_CLOSEframe generation. - Validate transport callbacks: Confirm your
conn_send_packetcallback returnsXQC_OKto 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →