Skip to content
Closed
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
84 changes: 84 additions & 0 deletions test/parallel/test-http2-client-settings-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';

const {
constants,
Http2Session,
nghttp2ErrorString
} = process.binding('http2');
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');

// tests error handling within requestOnConnect
// - NGHTTP2_ERR_NOMEM (should emit session error)
// - every other NGHTTP2 error from binding (should emit session error)

const specificTestKeys = [
'NGHTTP2_ERR_NOMEM'
];

const specificTests = [
{
ngError: constants.NGHTTP2_ERR_NOMEM,
error: {
code: 'ERR_OUTOFMEMORY',
type: Error,
message: 'Out of memory'
}
}
];

const genericTests = Object.getOwnPropertyNames(constants)
.filter((key) => (
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
))
.map((key) => ({
ngError: constants[key],
error: {
code: 'ERR_HTTP2_ERROR',
type: Error,
message: nghttp2ErrorString(constants[key])
}
}));

const tests = specificTests.concat(genericTests);

const server = http2.createServer(common.mustNotCall());
server.on('sessionError', () => {}); // not being tested

server.listen(0, common.mustCall(() => runTest(tests.shift())));

function runTest(test) {
// mock submitSettings because we only care about testing error handling
Http2Session.prototype.submitSettings = () => test.ngError;

const errorMustCall = common.expectsError(test.error);
const errorMustNotCall = common.mustNotCall(
`${test.error.code} should emit on session`
);

const url = `http://localhost:${server.address().port}`;

const client = http2.connect(url, {
settings: {
maxHeaderListSize: 1
}
});

const req = client.request();
req.resume();
req.end();

client.on('error', errorMustCall);
req.on('error', errorMustNotCall);

req.on('end', common.mustCall(() => {
client.destroy();
if (!tests.length) {
server.close();
} else {
runTest(tests.shift());
}
}));
}
229 changes: 125 additions & 104 deletions test/parallel/test-http2-session-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,110 +8,131 @@ const h2 = require('http2');

const server = h2.createServer();

server.on('stream', common.mustCall(onStream));

function assertSettings(settings) {
assert.strictEqual(typeof settings, 'object');
assert.strictEqual(typeof settings.headerTableSize, 'number');
assert.strictEqual(typeof settings.enablePush, 'boolean');
assert.strictEqual(typeof settings.initialWindowSize, 'number');
assert.strictEqual(typeof settings.maxFrameSize, 'number');
assert.strictEqual(typeof settings.maxConcurrentStreams, 'number');
assert.strictEqual(typeof settings.maxHeaderListSize, 'number');
}

function onStream(stream, headers, flags) {

const localSettings = stream.session.localSettings;
const remoteSettings = stream.session.remoteSettings;
assertSettings(localSettings);
assertSettings(remoteSettings);

// Test that stored settings are returned when called for second time
assert.strictEqual(stream.session.localSettings, localSettings);
assert.strictEqual(stream.session.remoteSettings, remoteSettings);

stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end('hello world');
}

server.listen(0);

server.on('listening', common.mustCall(() => {

const client = h2.connect(`http://localhost:${server.address().port}`, {
settings: {
enablePush: false,
initialWindowSize: 123456
}
});

client.on('localSettings', common.mustCall((settings) => {
assert(settings);
assert.strictEqual(settings.enablePush, false);
assert.strictEqual(settings.initialWindowSize, 123456);
assert.strictEqual(settings.maxFrameSize, 16384);
}, 2));
client.on('remoteSettings', common.mustCall((settings) => {
assert(settings);
}));

const headers = { ':path': '/' };

const req = client.request(headers);

req.on('connect', common.mustCall(() => {
// pendingSettingsAck will be true if a SETTINGS frame
// has been sent but we are still waiting for an acknowledgement
assert(client.pendingSettingsAck);
}));

// State will only be valid after connect event is emitted
req.on('ready', common.mustCall(() => {
assert.doesNotThrow(() => {
client.settings({
maxHeaderListSize: 1
});
server.on(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I'm following what's going on with the changes in this file. Is there a reason everything needs to be scoped within the stream handler? This looks like a lot of churn and I'm having trouble following why.

Copy link
Member Author

Choose a reason for hiding this comment

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

I originally started working on this file and formatted the code using Prettier.
I created test-http2-client-settings-errors.js after coming across the template used by other tests which checked for errors.
I've scoped method assertSettings within stream handler, as it's only used within that block.

Copy link
Contributor

@apapirovski apapirovski Oct 10, 2017

Choose a reason for hiding this comment

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

My concern is just that it's a bit easier to follow the file history (git blame) when changes aren't made just for the sake of changing code style, unless the clarity is a significant problem. That said, this is a nit so if no one else cares then this is fine. Thanks for the explanation 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree that code change shouldn't be made just for changing code style.
I added changed in test-http2-session-settings.js as the formatting was already done. I'll remove it from the commit if more people raise it in the reviews.
Btw, I created an issue at #16115 to check if we can move to Javascript formatter like Prettier.

'stream',
common.mustCall((stream) => {
const assertSettings = (settings) => {
assert.strictEqual(typeof settings, 'object');
assert.strictEqual(typeof settings.headerTableSize, 'number');
assert.strictEqual(typeof settings.enablePush, 'boolean');
assert.strictEqual(typeof settings.initialWindowSize, 'number');
assert.strictEqual(typeof settings.maxFrameSize, 'number');
assert.strictEqual(typeof settings.maxConcurrentStreams, 'number');
assert.strictEqual(typeof settings.maxHeaderListSize, 'number');
};

const localSettings = stream.session.localSettings;
const remoteSettings = stream.session.remoteSettings;
assertSettings(localSettings);
assertSettings(remoteSettings);

// Test that stored settings are returned when called for second time
assert.strictEqual(stream.session.localSettings, localSettings);
assert.strictEqual(stream.session.remoteSettings, remoteSettings);

stream.respond({
'content-type': 'text/html',
':status': 200
});

// Verify valid error ranges
[
['headerTableSize', -1],
['headerTableSize', 2 ** 32],
['initialWindowSize', -1],
['initialWindowSize', 2 ** 32],
['maxFrameSize', 16383],
['maxFrameSize', 2 ** 24],
['maxHeaderListSize', -1],
['maxHeaderListSize', 2 ** 32]
].forEach((i) => {
const settings = {};
settings[i[0]] = i[1];
assert.throws(() => client.settings(settings),
common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: RangeError
}));
});
[1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
assert.throws(() => client.settings({ enablePush: i }),
common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: TypeError
}));
stream.end('hello world');
})
);

server.listen(
0,
common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`, {
settings: {
enablePush: false,
initialWindowSize: 123456
}
});

}));

req.on('response', common.mustCall());
req.resume();
req.on('end', common.mustCall(() => {
server.close();
client.destroy();
}));
req.end();

}));
client.on(
'localSettings',
common.mustCall((settings) => {
assert(settings);
assert.strictEqual(settings.enablePush, false);
assert.strictEqual(settings.initialWindowSize, 123456);
assert.strictEqual(settings.maxFrameSize, 16384);
}, 2)
);
client.on(
'remoteSettings',
common.mustCall((settings) => {
assert(settings);
})
);

const headers = { ':path': '/' };

const req = client.request(headers);

req.on(
'connect',
common.mustCall(() => {
// pendingSettingsAck will be true if a SETTINGS frame
// has been sent but we are still waiting for an acknowledgement
assert(client.pendingSettingsAck);
})
);

// State will only be valid after connect event is emitted
req.on(
'ready',
common.mustCall(() => {
assert.doesNotThrow(() => {
client.settings({
maxHeaderListSize: 1
});
});

// Verify valid error ranges
[
['headerTableSize', -1],
['headerTableSize', 2 ** 32],
['initialWindowSize', -1],
['initialWindowSize', 2 ** 32],
['maxFrameSize', 16383],
['maxFrameSize', 2 ** 24],
['maxHeaderListSize', -1],
['maxHeaderListSize', 2 ** 32]
].forEach((i) => {
const settings = {};
settings[i[0]] = i[1];
common.expectsError(
() => client.settings(settings),
{
type: RangeError,
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
message: `Invalid value for setting "${i[0]}": ${i[1]}`
}
);
});

// error checks for enablePush
[1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
common.expectsError(
() => client.settings({ enablePush: i }),
{
type: TypeError,
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
message: `Invalid value for setting "enablePush": ${i}`
}
);
});
})
);

req.on('response', common.mustCall());
req.resume();
req.on(
'end',
common.mustCall(() => {
server.close();
client.destroy();
})
);
req.end();
})
);