From 669de075c57b640f58a68af6ee797862a9f8b42e Mon Sep 17 00:00:00 2001 From: Maksim Leanovich Date: Tue, 3 Nov 2026 00:27:58 +0000 Subject: [PATCH] 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 [status: ]", 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 --- index.js | 38 +++++++++++++++++++----------------- package.json | 2 +- test/missing-artifact.json | 11 +++++++++++ test/test.js | 40 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 test/missing-artifact.json diff --git a/index.js b/index.js index d6d705a..5246a71 100644 --- a/index.js +++ b/index.js @@ -443,31 +443,33 @@ module.exports = function(/*options, callback*/) { function (callback) { let repository = options.repositories[repositoryIndex]; let url = repository.url + urlPath; - let req_options = { url: url }; + // Without responseType axios decodes the body as UTF-8 text, which corrupts every jar. + let req_options = { url: url, responseType: 'arraybuffer' }; if (repository.hasOwnProperty('credentials')) { let username = repository.credentials.username; let password = repository.credentials.password; - req_options = { - url: url, - auth: { - username, - password - } + req_options.auth = { + username, + password }; } debug('downloading ' + url); - axios.get(url, req_options).catch(err => { - error = err - return callback(); - }).then(res => { - fs.promises.writeFile(destinationFile, res.data).then(() => { - foundUrl = url; - return callback() - }).catch(() => { - return callback(); - }) - }) repositoryIndex++; + axios.get(url, req_options) + .then( + (res) => fs.promises.writeFile(destinationFile, res.data).then(() => { + foundUrl = url; + }), + (err) => { + const status = err.response ? err.response.status : err.code; + error = new Error('download failed for ' + url + (reason ? ' (' + reason + ')' : '') + ' [status: ' + status + ']'); + } + ) + .catch((err) => { + error = err; + }) + // Outside the promise chain, so a throw in the next step is not swallowed as an unhandled rejection. + .then(() => process.nextTick(callback)); }, function () { if (foundUrl) { diff --git a/package.json b/package.json index 2ba3f47..af89285 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/node-java-maven", - "version": "0.1.3", + "version": "0.1.4", "description": "Utility for Node's java module to load mvn dependencies.", "main": "index.js", "bin": { diff --git a/test/missing-artifact.json b/test/missing-artifact.json new file mode 100644 index 0000000..70341b6 --- /dev/null +++ b/test/missing-artifact.json @@ -0,0 +1,11 @@ +{ + "java": { + "dependencies": [ + { + "groupId": "com.example.doesnotexist", + "artifactId": "missing-artifact", + "version": "1.0.0" + } + ] + } +} diff --git a/test/test.js b/test/test.js index 8698e4d..8dde36a 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,8 @@ +var assert = require('assert'); +var crypto = require('crypto'); var fs = require('fs-extra'); var path = require('path'); +var axios = require('axios').default; var mvn = require('../index'); var REMOVE_M2_DIR_AFTER_EACH_TEST = true; @@ -16,6 +19,43 @@ describe('maven', function() { testJson('org.springframework_spring_2.0.6.json'); testJson('com.googlecode.netlib-java.netlib-java.json'); + it('should download jars byte-for-byte as published', function(done) { + this.timeout(120 * 1000); + var opts = { + localRepository: localRepository, + packageJsonPath: path.join(__dirname, 'com.j256.ormlite_ormlite-jdbc_4.48.json') + }; + mvn(opts, function(err, mvnResults) { + if (err) { + return done(err); + } + assert.ok(mvnResults.classpath.length > 0, 'expected jars on the classpath'); + Promise.all(mvnResults.classpath.map(function(jarPath) { + var relativePath = path.relative(localRepository, jarPath).split(path.sep).join('/'); + return axios.get('https://repo1.maven.org/maven2/' + relativePath + '.sha1', { responseType: 'text' }) + .then(function(res) { + var expected = res.data.trim().split(/\s+/)[0]; + var actual = crypto.createHash('sha1').update(fs.readFileSync(jarPath)).digest('hex'); + assert.strictEqual(actual, expected, relativePath); + }); + })).then(function() { done(); }, done); + }); + }); + + it('should report a missing artifact through the callback', function(done) { + this.timeout(60 * 1000); + var opts = { + localRepository: localRepository, + packageJsonPath: path.join(__dirname, 'missing-artifact.json') + }; + mvn(opts, function(err) { + // Resolution errors come back as an array, one entry per failed dependency. + assert.ok(Array.isArray(err) && err.length === 1, 'expected one error for a missing artifact'); + assert.match(err[0].message, /download failed for .*missing-artifact.* \[status: 404\]/); + done(); + }); + }); + function testJson(jsonPath) { it('should pull the maven dependencies for ' + jsonPath, function(done) { this.timeout(120 * 1000);