Skip to content

Commit 669de07

Browse files
bsod90claude
andcommitted
fix: download jars as binary and report failed downloads
axios decodes a response body as UTF-8 text unless responseType says otherwise, so every jar written since the request -> axios switch is corrupt (~1.8x larger, "zip END header not found" in the JVM). Request an arraybuffer instead. A failed request (e.g. 404) went through .catch() and then into .then() with an undefined response, so `res.data` threw an unhandled rejection that crashed the process before the callback ran. Report it as "download failed for <url> [status: <code>]", as the request-based version did. Tests: jars resolved for ormlite-jdbc must match Maven Central's .sha1, and a missing artifact must come back through the callback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8dda4ea commit 669de07

4 files changed

Lines changed: 72 additions & 19 deletions

File tree

‎index.js‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -443,31 +443,33 @@ module.exports = function(/*options, callback*/) {
443443
function (callback) {
444444
let repository = options.repositories[repositoryIndex];
445445
let url = repository.url + urlPath;
446-
let req_options = { url: url };
446+
// Without responseType axios decodes the body as UTF-8 text, which corrupts every jar.
447+
let req_options = { url: url, responseType: 'arraybuffer' };
447448
if (repository.hasOwnProperty('credentials')) {
448449
let username = repository.credentials.username;
449450
let password = repository.credentials.password;
450-
req_options = {
451-
url: url,
452-
auth: {
453-
username,
454-
password
455-
}
451+
req_options.auth = {
452+
username,
453+
password
456454
};
457455
}
458456
debug('downloading ' + url);
459-
axios.get(url, req_options).catch(err => {
460-
error = err
461-
return callback();
462-
}).then(res => {
463-
fs.promises.writeFile(destinationFile, res.data).then(() => {
464-
foundUrl = url;
465-
return callback()
466-
}).catch(() => {
467-
return callback();
468-
})
469-
})
470457
repositoryIndex++;
458+
axios.get(url, req_options)
459+
.then(
460+
(res) => fs.promises.writeFile(destinationFile, res.data).then(() => {
461+
foundUrl = url;
462+
}),
463+
(err) => {
464+
const status = err.response ? err.response.status : err.code;
465+
error = new Error('download failed for ' + url + (reason ? ' (' + reason + ')' : '') + ' [status: ' + status + ']');
466+
}
467+
)
468+
.catch((err) => {
469+
error = err;
470+
})
471+
// Outside the promise chain, so a throw in the next step is not swallowed as an unhandled rejection.
472+
.then(() => process.nextTick(callback));
471473
},
472474
function () {
473475
if (foundUrl) {

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cubejs-backend/node-java-maven",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Utility for Node's java module to load mvn dependencies.",
55
"main": "index.js",
66
"bin": {

‎test/missing-artifact.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"java": {
3+
"dependencies": [
4+
{
5+
"groupId": "com.example.doesnotexist",
6+
"artifactId": "missing-artifact",
7+
"version": "1.0.0"
8+
}
9+
]
10+
}
11+
}

‎test/test.js‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
var assert = require('assert');
2+
var crypto = require('crypto');
13
var fs = require('fs-extra');
24
var path = require('path');
5+
var axios = require('axios').default;
36
var mvn = require('../index');
47
var REMOVE_M2_DIR_AFTER_EACH_TEST = true;
58

@@ -16,6 +19,43 @@ describe('maven', function() {
1619
testJson('org.springframework_spring_2.0.6.json');
1720
testJson('com.googlecode.netlib-java.netlib-java.json');
1821

22+
it('should download jars byte-for-byte as published', function(done) {
23+
this.timeout(120 * 1000);
24+
var opts = {
25+
localRepository: localRepository,
26+
packageJsonPath: path.join(__dirname, 'com.j256.ormlite_ormlite-jdbc_4.48.json')
27+
};
28+
mvn(opts, function(err, mvnResults) {
29+
if (err) {
30+
return done(err);
31+
}
32+
assert.ok(mvnResults.classpath.length > 0, 'expected jars on the classpath');
33+
Promise.all(mvnResults.classpath.map(function(jarPath) {
34+
var relativePath = path.relative(localRepository, jarPath).split(path.sep).join('/');
35+
return axios.get('https://repo1.maven.org/maven2/' + relativePath + '.sha1', { responseType: 'text' })
36+
.then(function(res) {
37+
var expected = res.data.trim().split(/\s+/)[0];
38+
var actual = crypto.createHash('sha1').update(fs.readFileSync(jarPath)).digest('hex');
39+
assert.strictEqual(actual, expected, relativePath);
40+
});
41+
})).then(function() { done(); }, done);
42+
});
43+
});
44+
45+
it('should report a missing artifact through the callback', function(done) {
46+
this.timeout(60 * 1000);
47+
var opts = {
48+
localRepository: localRepository,
49+
packageJsonPath: path.join(__dirname, 'missing-artifact.json')
50+
};
51+
mvn(opts, function(err) {
52+
// Resolution errors come back as an array, one entry per failed dependency.
53+
assert.ok(Array.isArray(err) && err.length === 1, 'expected one error for a missing artifact');
54+
assert.match(err[0].message, /download failed for .*missing-artifact.* \[status: 404\]/);
55+
done();
56+
});
57+
});
58+
1959
function testJson(jsonPath) {
2060
it('should pull the maven dependencies for ' + jsonPath, function(done) {
2161
this.timeout(120 * 1000);

0 commit comments

Comments
 (0)