compat(node:http) more compatibility improvements (#19063)

This commit is contained in:
Ciro Spaciari
2025-04-18 19:57:02 -07:00
committed by GitHub
parent 6e3519fd49
commit 218ee99155
24 changed files with 693 additions and 50 deletions

View File

@@ -0,0 +1,156 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
require('../common');
const http = require('http');
const net = require('net');
// Check that our HTTP server correctly handles HTTP/1.0 keep-alive requests.
check([{
name: 'keep-alive, no TE header',
requests: [{
expectClose: true,
data: 'POST / HTTP/1.0\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'
}, {
expectClose: true,
data: 'POST / HTTP/1.0\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'
}],
responses: [{
headers: { 'Connection': 'keep-alive' },
chunks: ['OK']
}, {
chunks: []
}]
}, {
name: 'keep-alive, with TE: chunked',
requests: [{
expectClose: false,
data: 'POST / HTTP/1.0\r\n' +
'Connection: keep-alive\r\n' +
'TE: chunked\r\n' +
'\r\n'
}, {
expectClose: true,
data: 'POST / HTTP/1.0\r\n' +
'\r\n'
}],
responses: [{
headers: { 'Connection': 'keep-alive' },
chunks: ['OK']
}, {
chunks: []
}]
}, {
name: 'keep-alive, with Transfer-Encoding: chunked',
requests: [{
expectClose: false,
data: 'POST / HTTP/1.0\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'
}, {
expectClose: true,
data: 'POST / HTTP/1.0\r\n' +
'\r\n'
}],
responses: [{
headers: { 'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked' },
chunks: ['OK']
}, {
chunks: []
}]
}, {
name: 'keep-alive, with Content-Length',
requests: [{
expectClose: false,
data: 'POST / HTTP/1.0\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'
}, {
expectClose: true,
data: 'POST / HTTP/1.0\r\n' +
'\r\n'
}],
responses: [{
headers: { 'Connection': 'keep-alive',
'Content-Length': '2' },
chunks: ['OK']
}, {
chunks: []
}]
}]);
function check(tests) {
const test = tests[0];
let server;
if (test) {
server = http.createServer(serverHandler).listen(0, '127.0.0.1', client);
}
let current = 0;
function next() {
check(tests.slice(1));
}
function serverHandler(req, res) {
if (current + 1 === test.responses.length) this.close();
const ctx = test.responses[current];
console.error('< SERVER SENDING RESPONSE', ctx);
res.writeHead(200, ctx.headers);
ctx.chunks.slice(0, -1).forEach(function(chunk) { res.write(chunk); });
res.end(ctx.chunks[ctx.chunks.length - 1]);
}
function client() {
if (current === test.requests.length) return next();
const port = server.address().port;
const conn = net.createConnection(port, '127.0.0.1', connected);
function connected() {
const ctx = test.requests[current];
console.error(' > CLIENT SENDING REQUEST', ctx);
conn.setEncoding('utf8');
conn.write(ctx.data);
function onclose() {
console.error(' > CLIENT CLOSE');
if (!ctx.expectClose) throw new Error('unexpected close');
client();
}
conn.on('close', onclose);
function ondata(s) {
console.error(' > CLIENT ONDATA %j %j', s.length, s.toString());
current++;
if (ctx.expectClose) return;
conn.removeListener('close', onclose);
conn.removeListener('data', ondata);
connected();
}
conn.on('data', ondata);
}
}
}

View File

@@ -0,0 +1,14 @@
'use strict';
require('../common');
const assert = require('assert');
const ClientRequest = require('http').ClientRequest;
{
assert.throws(() => {
new ClientRequest({ insecureHTTPParser: 'wrongValue' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /insecureHTTPParser/
}, 'http request should throw when passing invalid insecureHTTPParser');
}

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer();
server.on('request', function(req, res) {
res.writeHead(200, { 'foo': 'bar' });
res.flushHeaders();
res.flushHeaders(); // Should be idempotent.
});
server.listen(0, common.localhostIPv4, function() {
const req = http.request({
method: 'GET',
host: common.localhostIPv4,
port: this.address().port,
}, onResponse);
req.end();
function onResponse(res) {
assert.strictEqual(res.headers.foo, 'bar');
res.destroy();
server.closeAllConnections();
}
});

View File

@@ -0,0 +1,64 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
const http = require('http');
// Test that the DELETE, PATCH and PURGE verbs get passed through correctly
['DELETE', 'PATCH', 'PURGE'].forEach(function(method, index) {
const server = http.createServer(common.mustCall(function(req, res) {
assert.strictEqual(req.method, method);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('hello ');
res.write('world\n');
res.end();
}));
server.listen(0);
server.on('listening', common.mustCall(function() {
const c = net.createConnection(this.address().port);
let server_response = '';
c.setEncoding('utf8');
c.on('connect', function() {
c.write(`${method} / HTTP/1.0\r\n\r\n`);
});
c.on('data', function(chunk) {
server_response += chunk;
});
c.on('end', common.mustCall(function() {
const m = server_response.split('\r\n\r\n');
assert.strictEqual(m[1], 'hello world\n');
c.end();
}));
c.on('close', function() {
server.close();
});
}));
});

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const { strictEqual } = require('assert');
const { createServer, request } = require('http');
const server = createServer(common.mustCall((req, res) => {
strictEqual(req.method, 'QUERY');
res.end('OK');
}));
server.listen(0, common.mustCall(() => {
const req = request({ port: server.address().port, method: 'QUERY' }, common.mustCall((res) => {
strictEqual(res.statusCode, 200);
let buffer = '';
res.setEncoding('utf-8');
res.on('data', (c) => buffer += c);
res.on('end', common.mustCall(() => {
strictEqual(buffer, 'OK');
server.close();
}));
}));
req.end();
}));

View File

@@ -0,0 +1,50 @@
'use strict';
require('../common');
const assert = require('assert');
const { createServer } = require('http');
// This test validates that the HTTP server timeouts are properly validated and set.
{
const server = createServer();
assert.strictEqual(server.headersTimeout, 60000);
assert.strictEqual(server.requestTimeout, 300000);
}
{
const server = createServer({ headersTimeout: 10000, requestTimeout: 20000 });
assert.strictEqual(server.headersTimeout, 10000);
assert.strictEqual(server.requestTimeout, 20000);
}
{
const server = createServer({ headersTimeout: 10000, requestTimeout: 10000 });
assert.strictEqual(server.headersTimeout, 10000);
assert.strictEqual(server.requestTimeout, 10000);
}
{
const server = createServer({ headersTimeout: 10000 });
assert.strictEqual(server.headersTimeout, 10000);
assert.strictEqual(server.requestTimeout, 300000);
}
{
const server = createServer({ requestTimeout: 20000 });
assert.strictEqual(server.headersTimeout, 20000);
assert.strictEqual(server.requestTimeout, 20000);
}
{
const server = createServer({ requestTimeout: 100000 });
assert.strictEqual(server.headersTimeout, 60000);
assert.strictEqual(server.requestTimeout, 100000);
}
{
assert.throws(
() => createServer({ headersTimeout: 10000, requestTimeout: 1000 }),
{ code: 'ERR_OUT_OF_RANGE' }
);
}

View File

@@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { createServer } = require('https');
const fixtures = require('../common/fixtures');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
};
const server = createServer(options);
// 60000 seconds is the default
assert.strictEqual(server.headersTimeout, 60000);
const headersTimeout = common.platformTimeout(1000);
server.headersTimeout = headersTimeout;
assert.strictEqual(server.headersTimeout, headersTimeout);

View File

@@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { createServer } = require('https');
const fixtures = require('../common/fixtures');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
const server = createServer(options);
// 300 seconds is the default
assert.strictEqual(server.requestTimeout, 300000);
const requestTimeout = common.platformTimeout(1000);
server.requestTimeout = requestTimeout;
assert.strictEqual(server.requestTimeout, requestTimeout);

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
// This test assesses whether long-running writes can complete
// or timeout because the socket is not aware that the backing
// stream is still writing.
const writeSize = 3000000;
let socket;
const server = http.createServer(common.mustCall((req, res) => {
server.close();
const content = Buffer.alloc(writeSize, 0x44);
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': content.length.toString(),
'Vary': 'Accept-Encoding'
});
socket = res.socket;
const onTimeout = socket._onTimeout;
socket._onTimeout = common.mustCallAtLeast(() => onTimeout.call(socket), 1);
res.write(content);
res.end();
}));
server.on('timeout', () => {
// TODO(apapirovski): This test is faulty on certain Windows systems
// as no queue is ever created
assert(!socket._handle || socket._handle.writeQueueSize === 0,
'Should not timeout');
});
server.listen(0, common.mustCall(() => {
http.get({
path: '/',
port: server.address().port
}, (res) => {
res.once('data', () => {
socket._onTimeout();
res.on('data', () => {});
});
res.on('end', () => server.close());
});
}));