Skip to content

Commit ca44050

Browse files
committed
Make BC TLS detection testable; refactor to FQCN walk
1 parent f50b3a6 commit ca44050

2 files changed

Lines changed: 87 additions & 27 deletions

File tree

driver-core/src/main/com/mongodb/internal/connection/BackpressureErrorLabeler.java

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import com.mongodb.MongoException;
2020
import com.mongodb.MongoSocketException;
21-
import com.mongodb.lang.Nullable;
2221

2322
import javax.net.ssl.SSLHandshakeException;
2423
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -30,12 +29,8 @@
3029
import java.util.Arrays;
3130
import java.util.Collections;
3231
import java.util.HashSet;
33-
import java.util.List;
3432
import java.util.Locale;
35-
import java.util.Objects;
3633
import java.util.Set;
37-
import java.util.stream.Collectors;
38-
import java.util.stream.Stream;
3934

4035
/**
4136
* Attaches {@link MongoException#SYSTEM_OVERLOADED_ERROR_LABEL} and
@@ -48,16 +43,13 @@
4843
final class BackpressureErrorLabeler {
4944

5045
/**
51-
* BouncyCastle TLS fatal-alert exception types resolved at class-load time. If BC isn't on the
52-
* classpath the list is empty and {@link #isBouncyCastleTlsError(Throwable)} short-circuits to false.
46+
* BouncyCastle TLS fatal-alert exception type names.
5347
*/
54-
private static final List<Class<?>> BOUNCY_CASTLE_TLS_FATAL_TYPES = Stream.of(
48+
private static final Set<String> BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES = Collections.unmodifiableSet(
49+
new HashSet<>(Arrays.asList(
5550
"org.bouncycastle.tls.TlsFatalAlert",
5651
"org.bouncycastle.tls.TlsFatalAlertReceived",
57-
"org.bouncycastle.tls.crypto.TlsCryptoException")
58-
.map(BackpressureErrorLabeler::loadClassOrNull)
59-
.filter(Objects::nonNull)
60-
.collect(Collectors.toList());
52+
"org.bouncycastle.tls.crypto.TlsCryptoException")));
6153

6254
/**
6355
* RFC 5246 / RFC 8446 alert descriptions that surface in BouncyCastle TLS exception messages.
@@ -90,7 +82,7 @@ static void applyLabelsIfEligible(final Throwable t) {
9082
if (isTlsConfigurationError(socketException)) {
9183
return;
9284
}
93-
// TODO-BACKPRESSURE Nabil - SOCKS5 Revisit alongside JAVA-5205 (SOCKS5 in async) so both sync and
85+
// TODO-BACKPRESSURE Nabil - Add SOCKS5 check once JAVA-6194 is introduced
9486
// async proxy error surfaces can be handled together — likely via a dedicated internal
9587
// exception thrown from the proxy code path.
9688
socketException.addLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
@@ -140,14 +132,7 @@ private static boolean isTlsConfigurationError(final MongoSocketException t) {
140132
}
141133

142134
private static boolean isBouncyCastleTlsError(final Throwable cause) {
143-
boolean isBcType = false;
144-
for (Class<?> bcType : BOUNCY_CASTLE_TLS_FATAL_TYPES) {
145-
if (bcType.isInstance(cause)) {
146-
isBcType = true;
147-
break;
148-
}
149-
}
150-
if (!isBcType) {
135+
if (!isBouncyCastleTlsFatalType(cause.getClass())) {
151136
return false;
152137
}
153138
String message = cause.getMessage();
@@ -163,12 +148,18 @@ private static boolean isBouncyCastleTlsError(final Throwable cause) {
163148
return false;
164149
}
165150

166-
@Nullable
167-
private static Class<?> loadClassOrNull(final String fqn) {
168-
try {
169-
return Class.forName(fqn);
170-
} catch (ClassNotFoundException e) {
171-
return null;
151+
/**
152+
* Walks the class hierarchy comparing fully qualified names so a subclass of a known BC type
153+
* still matches without requiring the BC classes to be loadable at static-init time.
154+
*/
155+
private static boolean isBouncyCastleTlsFatalType(final Class<?> exceptionClass) {
156+
Class<?> cls = exceptionClass;
157+
while (cls != null) {
158+
if (BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES.contains(cls.getName())) {
159+
return true;
160+
}
161+
cls = cls.getSuperclass();
172162
}
163+
return false;
173164
}
174165
}

driver-core/src/test/unit/com/mongodb/internal/connection/BackpressureErrorLabelerTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import com.mongodb.MongoSocketOpenException;
2424
import com.mongodb.MongoSocketReadTimeoutException;
2525
import com.mongodb.ServerAddress;
26+
import net.bytebuddy.ByteBuddy;
2627
import org.junit.jupiter.api.Named;
28+
import org.junit.jupiter.api.Test;
2729
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.Arguments;
2831
import org.junit.jupiter.params.provider.MethodSource;
2932
import org.junit.jupiter.params.provider.ValueSource;
3033

@@ -166,6 +169,72 @@ void nonSocketErrorShouldNotBeLabeled(final Throwable e) {
166169
}
167170
}
168171

172+
/**
173+
* BouncyCastle isn't on the test classpath, so we use ByteBuddy to synthesise classes with the
174+
* exact FQCNs the labeler matches against. This exercises the FQCN-walk in
175+
* {@code isBouncyCastleTlsFatalType} without taking a compile- or runtime dependency on BC.
176+
*/
177+
@ParameterizedTest(name = "{0} with alert {1}")
178+
@MethodSource
179+
void bouncyCastleTlsFatalAlertShouldNotBeLabeled(final String bcFqcn, final String alertMessage) throws Exception {
180+
Throwable bcCause = newBouncyCastleStub(bcFqcn, alertMessage);
181+
MongoSocketException e = new MongoSocketException("tls", ADDRESS, bcCause);
182+
BackpressureErrorLabeler.applyLabelsIfEligible(e);
183+
assertLacksBackpressureLabels(e);
184+
}
185+
186+
static Stream<Arguments> bouncyCastleTlsFatalAlertShouldNotBeLabeled() {
187+
return Stream.of(
188+
Arguments.of("org.bouncycastle.tls.TlsFatalAlert", "handshake_failure(40)"),
189+
Arguments.of("org.bouncycastle.tls.TlsFatalAlertReceived", "unknown_ca(48)"),
190+
Arguments.of("org.bouncycastle.tls.crypto.TlsCryptoException", "bad_certificate(42)"));
191+
}
192+
193+
/**
194+
* Subclasses of known BC types must still match — the labeler walks the superclass chain.
195+
*/
196+
@Test
197+
void bouncyCastleSubclassWithAlertShouldNotBeLabeled() throws Exception {
198+
Class<?> bcParent = new ByteBuddy()
199+
.subclass(Exception.class)
200+
.name("org.bouncycastle.tls.TlsFatalAlert")
201+
.make()
202+
.load(BackpressureErrorLabelerTest.class.getClassLoader())
203+
.getLoaded();
204+
Class<?> bcSubclass = new ByteBuddy()
205+
.subclass(bcParent)
206+
.name("com.example.CustomBcSubclass")
207+
.make()
208+
.load(bcParent.getClassLoader())
209+
.getLoaded();
210+
Throwable cause = (Throwable) bcSubclass.getConstructor(String.class).newInstance("handshake_failure(40)");
211+
MongoSocketException e = new MongoSocketException("tls", ADDRESS, cause);
212+
BackpressureErrorLabeler.applyLabelsIfEligible(e);
213+
assertLacksBackpressureLabels(e);
214+
}
215+
216+
/**
217+
* BC type but the message has no recognised alert description — the alert-keyword filter
218+
* rejects it, so the labeler falls through and applies backpressure labels.
219+
*/
220+
@Test
221+
void bouncyCastleTypeWithoutAlertKeywordShouldBeLabeled() throws Exception {
222+
Throwable bcCause = newBouncyCastleStub("org.bouncycastle.tls.TlsFatalAlert", "something unrelated");
223+
MongoSocketException e = new MongoSocketException("tls", ADDRESS, bcCause);
224+
BackpressureErrorLabeler.applyLabelsIfEligible(e);
225+
assertHasBackpressureLabels(e);
226+
}
227+
228+
private static Throwable newBouncyCastleStub(final String fqcn, final String message) throws Exception {
229+
Class<?> cls = new ByteBuddy()
230+
.subclass(Exception.class)
231+
.name(fqcn)
232+
.make()
233+
.load(BackpressureErrorLabelerTest.class.getClassLoader())
234+
.getLoaded();
235+
return (Throwable) cls.getConstructor(String.class).newInstance(message);
236+
}
237+
169238
private static <T extends Throwable> Named<T> named(final T e) {
170239
return Named.of(e.getClass().getSimpleName(), e);
171240
}

0 commit comments

Comments
 (0)