Skip to content

Commit da50083

Browse files
Flossyclaude
andcommitted
fix: add error accumulation, timeouts, and size validation to MavenNexusClassSource
- Accumulate all errors instead of silently swallowing exceptions (lines 89-90) - Add configurable connect timeout (10s default) and read timeout (30s default) - Add MAX_JAR_SIZE validation (100MB) before downloading - Add MAX_CLASS_SIZE validation (10MB) when reading class entries - Improve error messages with detailed failure information from all artifacts - Add connectTimeout() and readTimeout() builder methods Silent exception swallowing made debugging impossible - all failures looked the same. Now provides detailed error message showing exactly what failed. Fixes #59 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 31dbddd commit da50083

1 file changed

Lines changed: 92 additions & 7 deletions

File tree

src/main/java/org/flossware/classloader/MavenNexusClassSource.java

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,65 @@
2727
* ConcurrentHashMap to support concurrent class loading operations.</p>
2828
*/
2929
public class MavenNexusClassSource implements ClassSource {
30+
private static final long MAX_JAR_SIZE = 100 * 1024 * 1024; // 100MB default max JAR size
31+
private static final long MAX_CLASS_SIZE = 10 * 1024 * 1024; // 10MB default max class size
32+
private static final int DEFAULT_CONNECT_TIMEOUT = 10000; // 10 seconds
33+
private static final int DEFAULT_READ_TIMEOUT = 30000; // 30 seconds
34+
3035
private final String nexusUrl;
3136
private final String repository;
3237
private final List<MavenArtifact> artifacts;
3338
private final AuthConfig authConfig;
3439
private final Map<String, byte[]> classCache;
40+
private final int connectTimeout;
41+
private final int readTimeout;
3542

3643
/**
37-
* Creates a Maven Nexus class source with full configuration.
44+
* Creates a Maven Nexus class source with full configuration including timeouts.
3845
*
3946
* @param nexusUrl The Nexus server URL
4047
* @param repository The Maven repository name
4148
* @param artifacts The list of Maven artifacts to load classes from
4249
* @param authConfig The authentication configuration
50+
* @param connectTimeout Connection timeout in milliseconds
51+
* @param readTimeout Read timeout in milliseconds
4352
* @throws NullPointerException if nexusUrl, repository, or artifacts is null
44-
* @throws IllegalArgumentException if artifacts list is empty
53+
* @throws IllegalArgumentException if artifacts list is empty or timeouts are negative
4554
*/
46-
public MavenNexusClassSource(String nexusUrl, String repository, List<MavenArtifact> artifacts, AuthConfig authConfig) {
55+
public MavenNexusClassSource(String nexusUrl, String repository, List<MavenArtifact> artifacts,
56+
AuthConfig authConfig, int connectTimeout, int readTimeout) {
4757
Objects.requireNonNull(nexusUrl, "nexusUrl cannot be null");
4858
Objects.requireNonNull(artifacts, "artifacts cannot be null");
4959

5060
if (artifacts.isEmpty()) {
5161
throw new IllegalArgumentException("At least one Maven artifact must be specified");
5262
}
63+
if (connectTimeout < 0) {
64+
throw new IllegalArgumentException("connectTimeout must be >= 0");
65+
}
66+
if (readTimeout < 0) {
67+
throw new IllegalArgumentException("readTimeout must be >= 0");
68+
}
5369

5470
this.nexusUrl = nexusUrl.endsWith("/") ? nexusUrl : nexusUrl + "/";
5571
this.repository = Objects.requireNonNull(repository, "repository cannot be null");
5672
this.artifacts = new ArrayList<>(artifacts);
5773
this.authConfig = authConfig != null ? authConfig : AuthConfig.none();
5874
this.classCache = new ConcurrentHashMap<>();
75+
this.connectTimeout = connectTimeout;
76+
this.readTimeout = readTimeout;
77+
}
78+
79+
/**
80+
* Creates a Maven Nexus class source with default timeouts.
81+
*
82+
* @param nexusUrl The Nexus server URL
83+
* @param repository The Maven repository name
84+
* @param artifacts The list of Maven artifacts to load classes from
85+
* @param authConfig The authentication configuration
86+
*/
87+
public MavenNexusClassSource(String nexusUrl, String repository, List<MavenArtifact> artifacts, AuthConfig authConfig) {
88+
this(nexusUrl, repository, artifacts, authConfig, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
5989
}
6090

6191
/**
@@ -79,6 +109,7 @@ public byte[] loadClassData(String className) throws IOException {
79109
}
80110

81111
String classFileName = ClassNameUtil.toClassFilePath(className);
112+
List<String> errorMessages = new ArrayList<>();
82113

83114
for (MavenArtifact artifact : artifacts) {
84115
try {
@@ -87,10 +118,19 @@ public byte[] loadClassData(String className) throws IOException {
87118
classCache.put(cacheKey, classData);
88119
return classData;
89120
} catch (IOException e) {
121+
// Accumulate errors instead of silently swallowing
122+
String errorMsg = String.format("Artifact %s - %s",
123+
artifact.toString(), e.getMessage());
124+
errorMessages.add(errorMsg);
90125
}
91126
}
92127

93-
throw new IOException("Class not found in any configured Maven artifacts: " + className);
128+
// Throw with ALL error details
129+
String allErrors = String.join("\n - ", errorMessages);
130+
throw new IOException(
131+
"Class not found in any of " + artifacts.size() + " configured Maven artifacts: " +
132+
className + "\nAttempted artifacts:\n - " + allErrors
133+
);
94134
}
95135

96136
@Override
@@ -116,12 +156,22 @@ private String buildJarUrl(MavenArtifact artifact) {
116156
private byte[] extractClassFromJar(String jarUrl, String classFileName) throws IOException {
117157
URL url = new URL(jarUrl);
118158
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
159+
connection.setConnectTimeout(connectTimeout);
160+
connection.setReadTimeout(readTimeout);
119161
configureAuthentication(connection);
120162
connection.setRequestMethod("GET");
121163

122164
int responseCode = connection.getResponseCode();
123165
if (responseCode != HttpURLConnection.HTTP_OK) {
124-
throw new IOException("HTTP error code: " + responseCode + " for JAR URL: " + jarUrl);
166+
throw new IOException("HTTP " + responseCode + " for JAR: " + jarUrl);
167+
}
168+
169+
// Check JAR size before downloading
170+
long contentLength = connection.getContentLengthLong();
171+
if (contentLength > MAX_JAR_SIZE) {
172+
throw new IOException(
173+
"JAR too large: " + contentLength + " bytes (max " + MAX_JAR_SIZE + ")"
174+
);
125175
}
126176

127177
try (InputStream in = connection.getInputStream();
@@ -130,18 +180,34 @@ private byte[] extractClassFromJar(String jarUrl, String classFileName) throws I
130180
JarEntry entry;
131181
while ((entry = jarIn.getNextJarEntry()) != null) {
132182
if (entry.getName().equals(classFileName) && !entry.isDirectory()) {
183+
long size = entry.getSize();
184+
185+
if (size > MAX_CLASS_SIZE) {
186+
throw new IOException(
187+
"Class too large: " + size + " bytes (max " + MAX_CLASS_SIZE + ")"
188+
);
189+
}
190+
191+
// Read with size enforcement
133192
ByteArrayOutputStream out = new ByteArrayOutputStream();
134193
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
194+
long totalRead = 0;
135195
int bytesRead;
196+
136197
while ((bytesRead = jarIn.read(buffer)) != -1) {
198+
totalRead += bytesRead;
199+
if (totalRead > MAX_CLASS_SIZE) {
200+
throw new IOException("Class exceeded size limit: " + totalRead);
201+
}
137202
out.write(buffer, 0, bytesRead);
138203
}
204+
139205
return out.toByteArray();
140206
}
141207
}
142208
}
143209

144-
throw new IOException("Class file not found in JAR: " + classFileName);
210+
throw new IOException("Class not found in JAR: " + classFileName + " (URL: " + jarUrl + ")");
145211
}
146212

147213
private void configureAuthentication(HttpURLConnection connection) {
@@ -182,6 +248,8 @@ public static class Builder {
182248
private String repository;
183249
private final List<MavenArtifact> artifacts = new ArrayList<>();
184250
private AuthConfig authConfig = AuthConfig.none();
251+
private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
252+
private int readTimeout = DEFAULT_READ_TIMEOUT;
185253

186254
public Builder nexusUrl(String nexusUrl) {
187255
this.nexusUrl = Objects.requireNonNull(nexusUrl, "nexusUrl cannot be null");
@@ -211,10 +279,27 @@ public Builder auth(AuthConfig authConfig) {
211279
return this;
212280
}
213281

282+
public Builder connectTimeout(int timeoutMs) {
283+
if (timeoutMs < 0) {
284+
throw new IllegalArgumentException("connectTimeout must be >= 0");
285+
}
286+
this.connectTimeout = timeoutMs;
287+
return this;
288+
}
289+
290+
public Builder readTimeout(int timeoutMs) {
291+
if (timeoutMs < 0) {
292+
throw new IllegalArgumentException("readTimeout must be >= 0");
293+
}
294+
this.readTimeout = timeoutMs;
295+
return this;
296+
}
297+
214298
public MavenNexusClassSource build() {
215299
Objects.requireNonNull(nexusUrl, "nexusUrl must be set");
216300
Objects.requireNonNull(repository, "repository must be set");
217-
return new MavenNexusClassSource(nexusUrl, repository, artifacts, authConfig);
301+
return new MavenNexusClassSource(nexusUrl, repository, artifacts, authConfig,
302+
connectTimeout, readTimeout);
218303
}
219304
}
220305
}

0 commit comments

Comments
 (0)