Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ac1c120
JAVA-6035: Add backpressure flag to connection handshake (#1906)
nhachicha Mar 5, 2026
b33dca9
Add `MongoException.SYSTEM_OVERLOADED_ERROR_LABEL`/`RETRYABLE_ERROR_L…
stIncMale Mar 24, 2026
fa82369
JAVA-6055 Implement prose backpressure retryable writes tests (#1929)
stIncMale Apr 10, 2026
ffe5242
Add `maxAdaptiveRetries` API (#1944)
stIncMale Apr 20, 2026
44541fc
Add support for server selection's deprioritized servers (#1860)
vbabanin Apr 21, 2026
c380b2e
Implement prose backpressure tests (#1946)
stIncMale Apr 22, 2026
d083e1b
Add `enableOverloadRetargeting` API (#1943)
vbabanin Apr 23, 2026
bd888c9
Add handshake prose Test 9: backpressure: true in handshake documents…
nhachicha Apr 28, 2026
394f7b1
JAVA-5950 Update Transactions Convenient API with exponential backoff…
nhachicha May 1, 2026
0eb04a0
JAVA-6194 Add MongoSocksProxyException for CMAP backpressure labeling
nhachicha May 8, 2026
f2bcce5
Merge remote-tracking branch 'origin/backpressure' into nh/backpressu…
nhachicha May 16, 2026
28a074d
update submodule
nhachicha May 16, 2026
c2ca4fd
Address review nits in MongoSocksProxyException
nhachicha May 16, 2026
801127f
Close proxy socket on MongoSocksProxyException in SocksSocket.connect
nhachicha May 16, 2026
61a1c5e
Use getHostString in SocksSocket exception reporting path
nhachicha May 16, 2026
49e58f0
Fix socket leak in SOCKS5 initializer methods and DRY open()
nhachicha May 16, 2026
db26d92
Replace Thread.sleep(300) with input drain in SocksSocketTest
nhachicha May 16, 2026
fd39744
Use ephemeral closed port instead of port 1 in SocksSocketTest
nhachicha May 16, 2026
7416dd3
Use Java 8-compatible drain in SocksSocketTest mini-server
nhachicha May 18, 2026
4e3249b
Add Scala type alias for MongoSocksProxyException
nhachicha May 18, 2026
98483bb
Phase-aware MongoSocksProxyException handling in BackpressureErrorLab…
nhachicha May 18, 2026
78d6b01
Tag handshake-phase IOExceptions with the correct HandshakePhase
nhachicha May 18, 2026
dee79f0
Validate non-null HandshakePhase in MongoSocksProxyException
nhachicha May 18, 2026
b3da503
Align MongoSocksProxyException class-level Javadoc with phase-aware b…
nhachicha May 19, 2026
2c5be54
Broaden HandshakePhase enum Javadoc to cover I/O-failure path
nhachicha May 19, 2026
effa09a
Rename misleading eofDuring* tests to ioFailureDuring*
nhachicha May 19, 2026
095f524
Narrow tcpConnectFailure test to IOException, not Throwable
nhachicha May 19, 2026
582169c
Drive real EOF in ioFailureDuring* tests via half-close
nhachicha May 19, 2026
d7a9b15
Align constructor Javadoc with phase/replyCode semantics post round 2
nhachicha May 19, 2026
43e478c
Potential fix for pull request finding
nhachicha May 19, 2026
7971f9a
Widen outer catch in SocksSocket.connect to IOException
nhachicha May 19, 2026
27417eb
Drop redundant MongoSocksProxyException re-throw branches
nhachicha May 19, 2026
b2bb401
Drop redundant null-phase guard in BackpressureErrorLabeler
nhachicha May 19, 2026
2517b69
Backpressure-label SOCKS5 failures by mongod-attribution
nhachicha May 20, 2026
df8430c
Include proxy host:port in PROXY_TCP_CONNECT exception message
nhachicha May 20, 2026
0aafd71
Add SOCKS5/code context to CONNECT non-success reply message
nhachicha May 20, 2026
11a5866
Realign comments on the two outer catches in SocksSocket.connect
nhachicha May 20, 2026
4023a0b
Cleanups
nhachicha May 20, 2026
4cac195
Stop swallowing unexpected exceptions in connectWithMiniServer
nhachicha May 20, 2026
4a44d38
Document constructor parameter ordering convention
nhachicha May 20, 2026
4306d2e
Review feedback
nhachicha May 20, 2026
c5abbce
Fixing SOCKS5 failing prose tests
nhachicha May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions driver-core/src/main/com/mongodb/MongoSocksProxyException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb;

import com.mongodb.lang.Nullable;

import static com.mongodb.assertions.Assertions.notNull;

/**
* Thrown when an error occurs while establishing a connection to a SOCKS5 proxy.
*
* <p>The {@link #getHandshakePhase()} identifies which phase of the SOCKS5 handshake failed.
* {@link #getProxyReplyCode()} returns the RFC 1928 reply code sent by the proxy when a
* non-success CONNECT reply was successfully parsed; it returns {@code null} otherwise
* (including for {@link HandshakePhase#CONNECT_RELAY} failures caused by an I/O error or
* an unrecognized reply field).
*
* <p>RFC 1928 reply codes: 1=general failure, 2=connection not allowed by ruleset,
* 3=network unreachable, 4=host unreachable, 5=connection refused, 6=TTL expired,
* 7=command not supported, 8=address type not supported.
*
* <p>Constructor parameter ordering follows the parent class first (message, address,
* optional cause), then SOCKS-specific arguments (handshakePhase, optional proxyReplyCode).
*
* @since 5.8
*/
public class MongoSocksProxyException extends MongoSocketOpenException {
Comment thread
nhachicha marked this conversation as resolved.
private static final long serialVersionUID = 1L;

/**
* The phase of the SOCKS5 handshake at which the failure occurred.
*
* @since 5.8
*/
public enum HandshakePhase {
/**
* TCP connection to the proxy host itself failed before any SOCKS5 exchange.
* The proxy may be temporarily unreachable.
*/
PROXY_TCP_CONNECT,

/**
* The SOCKS5 method-selection exchange failed. Causes include: incompatible
* proxy version, no common authentication method, an unrecognized method, or
* an I/O failure (EOF, timeout, broken pipe) while sending the method-selection
* request or reading its reply.
*/
NEGOTIATION,

/**
* Username/password sub-negotiation with the proxy failed. Causes include:
* the proxy rejecting the credentials (typically wrong username/password),
* or an I/O failure (EOF, timeout, broken pipe) while sending credentials
* or reading the auth result.
*/
AUTHENTICATION,

/**
* A failure occurred while sending the CONNECT request to the proxy or
* reading/parsing its reply. Causes include: a parsed non-success RFC 1928
* reply (in which case {@link MongoSocksProxyException#getProxyReplyCode()}
* carries the code), an unrecognized reply field or address type, or an
* I/O failure (EOF, timeout, broken pipe) on the CONNECT exchange.
*/
CONNECT_RELAY
}

private final HandshakePhase handshakePhase;

@Nullable
private final Integer proxyReplyCode;

/**
* Construct an instance with no RFC 1928 reply code and no cause. Suitable for any phase
* whose failure does not carry a parsed reply code: {@link HandshakePhase#PROXY_TCP_CONNECT},
* {@link HandshakePhase#NEGOTIATION}, {@link HandshakePhase#AUTHENTICATION}, and the
* {@link HandshakePhase#CONNECT_RELAY} sub-cases driven by an I/O failure or an unrecognized
* reply field.
*
* @param message the message
* @param serverAddress the server address
* @param handshakePhase the phase at which the failure occurred
*/
public MongoSocksProxyException(final String message, final ServerAddress serverAddress, final HandshakePhase handshakePhase) {
this(message, serverAddress, notNull("handshakePhase", handshakePhase), null);
}

/**
* Construct an instance with no RFC 1928 reply code. Suitable for any phase whose failure
* does not carry a parsed reply code: {@link HandshakePhase#PROXY_TCP_CONNECT},
* {@link HandshakePhase#NEGOTIATION}, {@link HandshakePhase#AUTHENTICATION}, and the
* {@link HandshakePhase#CONNECT_RELAY} sub-cases driven by an I/O failure or an unrecognized
* reply field.
*
* @param message the message
* @param address the server address
* @param cause the cause
* @param handshakePhase the phase at which the failure occurred
*/
public MongoSocksProxyException(final String message, final ServerAddress address,
final Throwable cause, final HandshakePhase handshakePhase) {
this(message, address, cause, notNull("handshakePhase", handshakePhase), null);
}

/**
* Construct an instance with an optional RFC 1928 reply code. A non-{@code null}
* {@code proxyReplyCode} should only accompany {@link HandshakePhase#CONNECT_RELAY} and
* indicates a successfully parsed non-success reply from the proxy. Use {@code null} in
* all other cases — including {@link HandshakePhase#CONNECT_RELAY} failures caused by an
* I/O error or an unrecognized reply field.
*
* @param message the message
* @param address the server address
* @param handshakePhase the phase at which the failure occurred
* @param proxyReplyCode the RFC 1928 reply code, or {@code null}
*/
public MongoSocksProxyException(final String message, final ServerAddress address, final HandshakePhase handshakePhase,
@Nullable final Integer proxyReplyCode) {
super(message, address);
this.handshakePhase = notNull("handshakePhase", handshakePhase);
this.proxyReplyCode = proxyReplyCode;
Comment thread
nhachicha marked this conversation as resolved.
}

/**
* Construct an instance with an optional RFC 1928 reply code. A non-{@code null}
* {@code proxyReplyCode} should only accompany {@link HandshakePhase#CONNECT_RELAY} and
* indicates a successfully parsed non-success reply from the proxy. Use {@code null} in
* all other cases — including {@link HandshakePhase#CONNECT_RELAY} failures caused by an
* I/O error or an unrecognized reply field.
*
* @param message the message
* @param address the server address
* @param cause the cause
* @param handshakePhase the phase at which the failure occurred
* @param proxyReplyCode the RFC 1928 reply code, or {@code null}
*/
public MongoSocksProxyException(final String message, final ServerAddress address,
final Throwable cause, final HandshakePhase handshakePhase,
@Nullable final Integer proxyReplyCode) {
super(message, address, cause);
this.handshakePhase = notNull("handshakePhase", handshakePhase);
this.proxyReplyCode = proxyReplyCode;
Comment thread
nhachicha marked this conversation as resolved.
}
Comment thread
nhachicha marked this conversation as resolved.

/**
* Returns the phase of the SOCKS5 handshake at which the failure occurred.
*
* @return the handshake phase, never {@code null}
*/
public HandshakePhase getHandshakePhase() {
return handshakePhase;
}

/**
* Returns the RFC 1928 reply code sent by the SOCKS5 proxy when a non-success CONNECT
* reply was successfully parsed, or {@code null} otherwise.
*
* @return the RFC 1928 proxy reply code, or {@code null}
*/
@Nullable
public Integer getProxyReplyCode() {
return proxyReplyCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.mongodb.MongoException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocksProxyException;

import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
Expand Down Expand Up @@ -76,19 +77,36 @@ static void applyLabelsIfEligible(final Throwable t) {
return;
}
MongoSocketException socketException = (MongoSocketException) t;
if (isSocksFailure(socketException)) {
return;
}
if (isDnsLookupFailure(socketException)) {
return;
}
if (isTlsConfigurationError(socketException)) {
return;
}
// TODO-BACKPRESSURE Nabil - Add SOCKS5 check once JAVA-6194 is introduced
// async proxy error surfaces can be handled together — likely via a dedicated internal
// exception thrown from the proxy code path.
socketException.addLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
socketException.addLabel(MongoException.RETRYABLE_ERROR_LABEL);
}

private static boolean isSocksFailure(final MongoSocketException t) {
if (!(t instanceof MongoSocksProxyException)) {
return false;
}
MongoSocksProxyException socksException = (MongoSocksProxyException) t;
if (socksException.getHandshakePhase() != MongoSocksProxyException.HandshakePhase.CONNECT_RELAY) {
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.

I think HandshakePhase may be adding API surface mainly to support internal backpressure-labeling. For this PR, the user-visible value of exposing a handshake phase seems unclear.

The internal logic in labeler only needs to distinguish whether the failure came from a CONNECT relay response with a SOCKS reply code that should be inspected. The same gate can be achieved with getProxyReplyCode() == null (not a CONNECT relay error) vs non-null (has a reply code to inspect).

Should we simplify that and remove HandshakePhase?

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.

Taking it further, do we need getProxyReplyCode() either? The exception message already contains the ServerReply message text, for example "Network is unreachable", "Host is unreachable", or "Connection has been refused". Since BackpressureErrorLabeler is in the same package as SocksSocket.ServerReply, it could compare against ServerReply.getMessage() directly, without adding public API surface beyond the exception class itself.

 private static boolean isSocksFailure(final MongoSocketException t) {
      if (!(t instanceof MongoSocksProxyException)) {
          return false;
      }
      String message = t.getMessage();
      return !SocksSocket.ServerReply.NET_UNREACHABLE.getMessage().equals(message)
              && !SocksSocket.ServerReply.HOST_UNREACHABLE.getMessage().equals(message)
              && !SocksSocket.ServerReply.CONN_REFUSED.getMessage().equals(message);
  }

return true;
}
Integer replyCode = socksException.getProxyReplyCode();
if (replyCode == null) {
return true;
}
return replyCode != SocksSocket.ServerReply.NET_UNREACHABLE.getReplyNumber()
&& replyCode != SocksSocket.ServerReply.HOST_UNREACHABLE.getReplyNumber()
&& replyCode != SocksSocket.ServerReply.CONN_REFUSED.getReplyNumber();
}

private static boolean isDnsLookupFailure(final MongoSocketException t) {
Throwable cause = t.getCause();
while (cause != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

package com.mongodb.internal.connection;

import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketOpenException;
import com.mongodb.MongoSocketReadException;
import com.mongodb.MongoSocksProxyException;
import com.mongodb.ServerAddress;
import com.mongodb.connection.AsyncCompletionHandler;
import com.mongodb.connection.ProxySettings;
Expand All @@ -38,6 +40,7 @@
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.notNull;
Expand Down Expand Up @@ -79,10 +82,16 @@ public void open(final OperationContext operationContext) {
socket = initializeSocket(operationContext);
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
} catch (MongoSocksProxyException e) {
close();
throw e;
} catch (IOException e) {
close();
throw translateInterruptedException(e, "Interrupted while connecting")
.orElseThrow(() -> new MongoSocketOpenException("Exception opening socket", getAddress(), e));
Optional<MongoInterruptedException> interrupted = translateInterruptedException(e, "Interrupted while connecting");
if (interrupted.isPresent()) {
throw interrupted.get();
}
throw new MongoSocketOpenException("Exception opening socket", getAddress(), e);
}
}

Expand Down Expand Up @@ -119,15 +128,32 @@ private SSLSocket initializeSslSocketOverSocksProxy(final OperationContext opera
final int serverPort = address.getPort();

SocksSocket socksProxy = new SocksSocket(settings.getProxySettings());
configureSocket(socksProxy, operationContext, settings);
InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
socksProxy.connect(inetSocketAddress, operationContext.getTimeoutContext().getConnectTimeoutMs());

SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
//Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
//So it is possible to set SSL parameters before handshake is done.
configureSslSocket(sslSocket, sslSettings, inetSocketAddress);
return sslSocket;
// Track the outermost socket layer to close on failure. Initially this is socksProxy;
// once we wrap it into an SSLSocket, that becomes the outermost layer and closing it
// tears down the underlying socksProxy as well.
Socket toClose = socksProxy;
try {
configureSocket(socksProxy, operationContext, settings);
InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
try {
socksProxy.connect(inetSocketAddress, operationContext.getTimeoutContext().getConnectTimeoutMs());
} catch (IOException e) {
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.

throw wrapAsProxyTcpConnect(e);
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
toClose = sslSocket;
//Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
//So it is possible to set SSL parameters before handshake is done.
configureSslSocket(sslSocket, sslSettings, inetSocketAddress);
return sslSocket;
} catch (IOException | RuntimeException e) {
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.

try {
toClose.close();
} catch (IOException closeException) {
e.addSuppressed(closeException);
}
throw e;
}
}


Expand All @@ -139,19 +165,41 @@ private static InetSocketAddress toSocketAddress(final String serverHost, final
return InetSocketAddress.createUnresolved(serverHost, serverPort);
}

private MongoSocksProxyException wrapAsProxyTcpConnect(final IOException cause) {
ProxySettings proxySettings = settings.getProxySettings();
return new MongoSocksProxyException(
"Exception connecting to SOCKS5 proxy (" + proxySettings.getHost() + ":" + proxySettings.getPort() + ")",
getAddress(), cause,
MongoSocksProxyException.HandshakePhase.PROXY_TCP_CONNECT);
}

private Socket initializeSocketOverSocksProxy(final OperationContext operationContext) throws IOException {
Socket createdSocket = socketFactory.createSocket();
configureSocket(createdSocket, operationContext, settings);
/*
Wrap the configured socket with SocksSocket to add extra functionality.
Reason for separate steps: We can't directly extend Java 11 methods within 'SocksSocket'
to configure itself.
*/
SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings());

socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()),
operationContext.getTimeoutContext().getConnectTimeoutMs());
return socksProxy;
try {
configureSocket(createdSocket, operationContext, settings);
/*
Wrap the configured socket with SocksSocket to add extra functionality.
Reason for separate steps: We can't directly extend Java 11 methods within 'SocksSocket'
to configure itself.
*/
SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings());
try {
socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()),
operationContext.getTimeoutContext().getConnectTimeoutMs());
} catch (IOException e) {
throw wrapAsProxyTcpConnect(e);
}
Comment on lines +186 to +191
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.

Could we make SocksSocket.connect() throw only MongoSocksProxyException by contract?

    @Override
    public void connect(final SocketAddress endpoint, final int connectTimeoutMs) // no throws IOException

If it catches IOException internally and always wraps it as MongoSocksProxyException, callers would not need to branch on exception types.

Currently, SocketStream has two places that catch IOException from SocksSocket.connect() and wrap it as MongoSocksProxyException:

  • initializeSslSocketOverSocksProxy - lines 140–142
  • initializeSocketOverSocksProxy - lines 188–191

With that contract, both catch blocks could go away, along with the wrapAsProxyTcpConnect helper.

return socksProxy;
} catch (IOException | RuntimeException e) {
// SocksSocket.connect() closes itself on failure, but createdSocket may not yet
// be owned by a SocksSocket (e.g. configureSocket threw). Close defensively;
try {
createdSocket.close();
} catch (IOException closeException) {
e.addSuppressed(closeException);
}
throw e;
Comment on lines +194 to +201
Copy link
Copy Markdown
Member

@vbabanin vbabanin May 21, 2026

Choose a reason for hiding this comment

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

I think configureSocket only sets socket options; it does not open a connection. If it throws, there should not be a connected socket to close.

Could we remove this try/catch?

}
}

@Override
Expand Down
Loading