Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .eslintrc.js

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I confirmed this change is backwards compatible with all listed Node versions in engines block. version 2020 is the last possible option, though. To go to 2021 or newer will require dropping Node 14 in a major

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 6,
ecmaVersion: 2020,
sourceType: 'module'
},
extends: 'eslint:recommended',
Expand Down
2 changes: 2 additions & 0 deletions README.md

@apellerano-pw apellerano-pw Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's no fix for the HTTP issue even going all the way to the latest release of proxy-agent (8.0.2 at time of writing), so I just documented the flaw. If someone needs this HTTP support, it will likely require a more complex Proxy setup for httpAgent middle-manned by this lib

Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ Sets the default mime type, used when it cannot be determined from the file exte

The network proxy url used when sending requests to S3.

Note: requests to plain `http://` endpoints (for example a custom `endpoint` used with a local S3-compatible server) fail when sent through a proxy, because the AWS SDK adds an `Expect: 100-continue` header to uploads and the proxy agent cannot handle it. HTTPS endpoints, including AWS S3 itself, work through the proxy.

*Default:* `undefined`

### serverSideEncryption
Expand Down
17 changes: 10 additions & 7 deletions lib/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ module.exports = CoreObject.extend({

const proxy = this.plugin.readConfig('proxy');
if (proxy) {
var agent;
const agentOptions = function(agent) {
return { httpAgent: agent, httpsAgent: agent };
};
this._proxyAgent = this.plugin.readConfig('proxyAgent');
if (this._proxyAgent) {
agent = this._proxyAgent(proxy);
s3Options.requestHandler = agentOptions(this._proxyAgent(proxy));
} else {
const { ProxyAgent } = require('proxy-agent');
agent = new ProxyAgent(proxy);
// The SDK accepts an async provider here, which lets us load proxy-agent via import().
s3Options.requestHandler = function() {
return import('proxy-agent').then(function({ ProxyAgent }) {
return agentOptions(new ProxyAgent({ getProxyForUrl: () => proxy }));
});
};
}
s3Options.httpOptions = {
agent
};
}

const accessKeyId = this.plugin.readConfig('accessKeyId');
Expand Down
1 change: 1 addition & 0 deletions tests/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
globals: {
"describe": true,
"before": true,
"after": true,
"beforeEach": true,
"it": true
},
Expand Down
110 changes: 110 additions & 0 deletions tests/lib/s3-test.js

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These new tests reproduce the failure scenario without the bug fix

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/*eslint-env node*/
var os = require('os');
var http = require('http');
var net = require('net');
var chai = require('chai');
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
Expand Down Expand Up @@ -45,6 +47,114 @@ describe('s3', function() {
});
});

describe('with a proxy configured', function() {
// Unproxied requests to the unresolvable endpoint fail via DNS + SDK retries.
this.timeout(15000);

var proxyServer, proxyUrl, proxyPort, proxiedRequests, proxiedConnects, config;

function createSubject() {
return new S3({
plugin: {
ui: mockUi,
readConfig: function(propertyName) {
return config[propertyName];
},
log: plugin.log
}
});
}

function uploadOptions() {
return {
filePaths: ['app.js'],
cwd: process.cwd() + '/tests/fixtures/dist',
prefix: 'js-app',
bucket: 'test-bucket'
};
}

before(function(done) {
proxyServer = http.createServer(function(req, res) {
proxiedRequests.push({ method: req.method, url: req.url, host: req.headers.host });
req.resume();
req.on('end', function() {
res.writeHead(200, { 'Content-Length': 0, 'ETag': '"proxied"' });
res.end();
});
});
// HTTPS requests are tunnelled via CONNECT; record the tunnel target and refuse it.
proxyServer.on('connect', function(req, socket) {
proxiedConnects.push(req.url);
socket.end('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
});
proxyServer.listen(0, '127.0.0.1', function() {
proxyPort = proxyServer.address().port;
proxyUrl = 'http://127.0.0.1:' + proxyPort;
done();
});
});

after(function(done) {
proxyServer.close(done);
});

beforeEach(function() {
proxiedRequests = [];
proxiedConnects = [];
config = {
region: 'us-east-1',
accessKeyId: 'AKIDEXAMPLE',
secretAccessKey: 'secret',
endpoint: 'http://s3.test.invalid',
proxy: proxyUrl
};
});

it('tunnels S3 requests through the configured proxy', function() {
config.endpoint = 'https://s3.test.invalid';

return assert.isRejected(createSubject().upload(uploadOptions()))
.then(function() {
assert.isAbove(proxiedConnects.length, 0);
assert.equal(proxiedConnects[0], 'test-bucket.s3.test.invalid:443');
});
});

it('sends S3 requests through the agent returned by a custom proxyAgent', function() {
var proxyAgentArgs = [];
config.proxyAgent = function(proxy) {
proxyAgentArgs.push(proxy);
var agent = new http.Agent();
agent.createConnection = function() {
return net.connect(proxyPort, '127.0.0.1');
};
return agent;
};

return createSubject().upload(uploadOptions())
.then(function(filesUploaded) {
assert.deepEqual(proxyAgentArgs, [proxyUrl]);
assert.deepEqual(filesUploaded, ['app.js']);
assert.equal(proxiedRequests.length, 1);
assert.equal(proxiedRequests[0].method, 'PUT');
assert.equal(proxiedRequests[0].host, 'test-bucket.s3.test.invalid');
});
});

it('does not use a proxy agent when no proxy is configured', function() {
var proxyAgentCalled = false;
config.proxy = undefined;
config.proxyAgent = function() {
proxyAgentCalled = true;
};

createSubject();

assert.isFalse(proxyAgentCalled);
});
});

describe('#upload', function() {
it('resolves if all uploads succeed', function() {
var options = {
Expand Down
Loading