Skip to content

Fix reconnection hang on HTTP proxy connect failures and stalled handshakes#1275

Open
rishabhagl wants to merge 3 commits into
quickfix-j:masterfrom
rishabhagl:fix/http-proxy-reconnect-hang
Open

Fix reconnection hang on HTTP proxy connect failures and stalled handshakes#1275
rishabhagl wants to merge 3 commits into
quickfix-j:masterfrom
rishabhagl:fix/http-proxy-reconnect-hang

Conversation

@rishabhagl

Copy link
Copy Markdown

Summary

Fixes #1274 — reconnection permanently stalls when connecting through an HTTP proxy if the proxy connect fails with an exception, or if the CONNECT handshake stalls without resolving the ConnectFuture at all.

Problem

IoSessionInitiator.pollConnectFuture() only checked future.getSession() != null. If the future instead completed exceptionally (e.g. proxy returns 407, or the CONNECT is rejected), the code fell through into the "still pending" branch, which just logged a message and never cleared connectFuture or called handleConnectException(). Since shouldReconnect() requires connectFuture == null, the initiator got stuck and never retried.

Separately, a stalled proxy handshake (no session, no exception — MINA's proxy filter wedged mid-negotiation) had no timeout at all and could hang indefinitely.

Fix

  • pollConnectFuture() now explicitly checks future.getException() and routes it through handleConnectException(), clearing connectFuture immediately.
  • Added a connectTimeoutMillis-based watchdog: if a connect attempt has been pending longer than this, cancelAndResetPendingConnectAttempt() is invoked to:
    • force-close any half-open IoSession and cancel the future,
    • if ioConnector instanceof ProxyConnector, cancel the proxy connector's internal connect future and recreate the connector via setupIoConnector(),
    • reset lastConnectTime/lastReconnectAttemptTime to the current time so the next scheduled reconnect doesn't fire immediately (previously pendingMillis would already exceed ReconnectInterval, causing a premature/rapid retry loop) or never (if left unset).

Testing

Added IoSessionInitiatorConnectTaskTest, which targets the private IoSessionInitiator.ConnectTask polling state machine via reflection (the fix lives inside this private class, so tests construct ConnectTask directly and invoke pollConnectFuture(), cancelAndResetPendingConnectAttempt(), and handleConnectException() reflectively).

Coverage includes:

  • Pending connect exceeds connectTimeoutMillis — future is cancelled, timeout events are logged, onConnectException fires, connectFuture clears, failure count increments once, and both timers reset so shouldReconnect() correctly waits out the full ReconnectInterval.
  • Pending connect within timeout — future is left untouched, no timeout logs/callbacks fire, failure count stays at zero.
  • Future completes with a real exception (not a timeout) — routes through the existing exception path, not the new watchdog: no explicit cancel, correct error logged, future cleared, failure count incremented.
  • Successful connect — session stored, future cleared, failure count and address index reset, lastConnectTime updated.
  • Proxy connect timeout (via a TestProxyConnector stub) — confirms the pending future is cancelled, cancelConnectFuture() is called, and the connector instance is recreated (not reused).
  • cancelAndResetPendingConnectAttempt — closes a half-open session before cancelling the future and separately confirms proxy-connector cancellation/recreation in isolation.
  • handleConnectException — unwraps nested causes, increments failure count, clears the future, and notifies the state listener.

All existing tests in quickfixj-core pass locally with this change.

Related

…dshakes

- Handle exceptional ConnectFuture completion in pollConnectFuture() instead of treating it as still-pending, which previously left connectFuture non-null forever and blocked re-connection.
- Add a connectTimeoutMillis watchdog to cancel and reset pending connects that never resolve (no session, no exception) — common with stalled HTTP CONNECT proxy handshakes.
- Recreate the ProxyConnector via setupIoConnector() after cancelling a stuck pending attempt, and reset lastConnectTime/lastReconnectAttemptTime to avoid an immediate re-fire on the next reconnect tick.
- Add unit tests covering the changes.
Comment thread quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java Outdated
Comment thread quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java Outdated
Comment thread quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java Outdated
Rishabh Agarwal and others added 2 commits July 5, 2026 22:40
`pollConnect()` is only called from methods which originate from `run()` which is called by a single thread from the executor, so `synchronized` could be removed.
}

try {
setupIoConnector();

@chrjohn chrjohn Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this only done on a ProxyConnector? I assume because it has some internal state that needs to be handled differently? Maybe add some explanation?
Edit: I'm still thinking about if this could conflict with the dispose-logic.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this exception when reused IoConnector for proxy.
java.lang.IllegalStateException: handler cannot be set while the service is active.
at org.apache.mina.core.service.AbstractIoService.setHandler(...)
at org.apache.mina.proxy.ProxyConnector.connect0(...)

This exception does not occure when reusing the IoConnector in non-proxy connection timeout, where the server does not reply TCP SYN.
by explanation, you mean adding code comment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reconnection hangs indefinitely when ConnectFuture fails or stalls through an HTTP proxy (IoSessionInitiator)

2 participants