More node:http compatibility (#19173)

This commit is contained in:
Kai Tamkun
2025-04-23 16:44:32 -07:00
committed by GitHub
parent 9646bf1a38
commit 506afcbc7e
18 changed files with 770 additions and 49 deletions

View File

@@ -2700,6 +2700,12 @@ pub const HardcodedModule = enum {
@"node:_stream_wrap",
@"node:_stream_writable",
@"node:_tls_common",
@"node:_http_agent",
@"node:_http_client",
@"node:_http_common",
@"node:_http_incoming",
@"node:_http_outgoing",
@"node:_http_server",
/// This is gated behind '--expose-internals'
@"bun:internal-for-testing",
@@ -2778,6 +2784,12 @@ pub const HardcodedModule = enum {
.{ "node:_stream_wrap", .@"node:_stream_wrap" },
.{ "node:_stream_writable", .@"node:_stream_writable" },
.{ "node:_tls_common", .@"node:_tls_common" },
.{ "node:_http_agent", .@"node:_http_agent" },
.{ "node:_http_client", .@"node:_http_client" },
.{ "node:_http_common", .@"node:_http_common" },
.{ "node:_http_incoming", .@"node:_http_incoming" },
.{ "node:_http_outgoing", .@"node:_http_outgoing" },
.{ "node:_http_server", .@"node:_http_server" },
.{ "node-fetch", HardcodedModule.@"node-fetch" },
.{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" },
@@ -2929,18 +2941,26 @@ pub const HardcodedModule = enum {
nodeEntry("worker_threads"),
nodeEntry("zlib"),
nodeEntry("node:_http_agent"),
nodeEntry("node:_http_client"),
nodeEntry("node:_http_common"),
nodeEntry("node:_http_incoming"),
nodeEntry("node:_http_outgoing"),
nodeEntry("node:_http_server"),
nodeEntry("_http_agent"),
nodeEntry("_http_client"),
nodeEntry("_http_common"),
nodeEntry("_http_incoming"),
nodeEntry("_http_outgoing"),
nodeEntry("_http_server"),
// sys is a deprecated alias for util
.{ "sys", .{ .path = "node:util", .node_builtin = true } },
.{ "node:sys", .{ .path = "node:util", .node_builtin = true } },
// These are returned in builtinModules, but probably not many
// packages use them so we will just alias them.
.{ "node:_http_agent", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_http_client", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_http_common", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_http_incoming", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_http_outgoing", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_http_server", .{ .path = "node:http", .node_builtin = true } },
.{ "node:_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } },
.{ "node:_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } },
.{ "node:_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } },
@@ -2949,12 +2969,6 @@ pub const HardcodedModule = enum {
.{ "node:_stream_writable", .{ .path = "node:_stream_writable", .node_builtin = true } },
.{ "node:_tls_wrap", .{ .path = "node:tls", .node_builtin = true } },
.{ "node:_tls_common", .{ .path = "node:_tls_common", .node_builtin = true } },
.{ "_http_agent", .{ .path = "node:http", .node_builtin = true } },
.{ "_http_client", .{ .path = "node:http", .node_builtin = true } },
.{ "_http_common", .{ .path = "node:http", .node_builtin = true } },
.{ "_http_incoming", .{ .path = "node:http", .node_builtin = true } },
.{ "_http_outgoing", .{ .path = "node:http", .node_builtin = true } },
.{ "_http_server", .{ .path = "node:http", .node_builtin = true } },
.{ "_stream_duplex", .{ .path = "node:_stream_duplex", .node_builtin = true } },
.{ "_stream_passthrough", .{ .path = "node:_stream_passthrough", .node_builtin = true } },
.{ "_stream_readable", .{ .path = "node:_stream_readable", .node_builtin = true } },

View File

@@ -1,4 +1,3 @@
const { checkIsHttpToken } = require("internal/validators");
const { isTypedArray, isArrayBuffer } = require("node:util/types");
const {
@@ -85,8 +84,6 @@ const kRequest = Symbol("request");
const kCloseCallback = Symbol("closeCallback");
const kDeferredTimeouts = Symbol("deferredTimeouts");
const RegExpPrototypeExec = RegExp.prototype.exec;
const kEmptyObject = Object.freeze(Object.create(null));
export const enum ClientRequestEmitState {
@@ -142,31 +139,6 @@ function emitErrorNextTickIfErrorListener(self, err, cb) {
}
}
}
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
/**
* True if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*/
function checkInvalidHeaderChar(val: string) {
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
}
const validateHeaderName = (name, label?) => {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
throw $ERR_INVALID_HTTP_TOKEN(label || "Header name", name);
}
};
const validateHeaderValue = (name, value) => {
if (value === undefined) {
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
if (checkInvalidHeaderChar(value)) {
throw $ERR_INVALID_CHAR("header content", name);
}
};
// TODO: make this more robust.
function isAbortError(err) {
@@ -448,8 +420,6 @@ export {
kRequest,
kCloseCallback,
kDeferredTimeouts,
validateHeaderName,
validateHeaderValue,
isAbortError,
kEmptyObject,
getIsNextIncomingMessageHTTPS,

View File

@@ -376,7 +376,7 @@ function ClientRequest(input, options, cb) {
setIsNextIncomingMessageHTTPS(prevIsHTTPS);
res.req = this;
let timer;
response.setTimeout = (msecs, callback) => {
res.setTimeout = (msecs, callback) => {
if (timer) {
clearTimeout(timer);
}
@@ -870,7 +870,7 @@ function ClientRequest(input, options, cb) {
this.setTimeout = (msecs, callback) => {
if (this.destroyed) return this;
this.timeout = msecs = validateMsecs(msecs, "msecs");
this.timeout = msecs = validateMsecs(msecs, "timeout");
// Attempt to clear an existing timer in both cases -
// even if it will be rescheduled we don't want to leak an existing timer.

View File

@@ -0,0 +1,85 @@
const { checkIsHttpToken } = require("internal/validators");
const kIncomingMessage = Symbol("IncomingMessage");
const RegExpPrototypeExec = RegExp.prototype.exec;
let headerCharRegex;
/**
* True if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*/
function checkInvalidHeaderChar(val: string) {
if (!headerCharRegex) {
headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
}
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
}
const validateHeaderName = (name, label?) => {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
throw $ERR_INVALID_HTTP_TOKEN(label || "Header name", name);
}
};
const validateHeaderValue = (name, value) => {
if (value === undefined) {
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
if (checkInvalidHeaderChar(value)) {
throw $ERR_INVALID_CHAR("header content", name);
}
};
const methods = [
"DELETE",
"GET",
"HEAD",
"POST",
"PUT",
"CONNECT",
"OPTIONS",
"TRACE",
"COPY",
"LOCK",
"MKCOL",
"MOVE",
"PROPFIND",
"PROPPATCH",
"SEARCH",
"UNLOCK",
"BIND",
"REBIND",
"UNBIND",
"ACL",
"REPORT",
"MKACTIVITY",
"CHECKOUT",
"MERGE",
"M-SEARCH",
"NOTIFY",
"SUBSCRIBE",
"UNSUBSCRIBE",
"PATCH",
"PURGE",
"MKCALENDAR",
"LINK",
"UNLINK",
"SOURCE",
"QUERY",
];
export default {
_checkIsHttpToken: checkIsHttpToken,
_checkInvalidHeaderChar: checkInvalidHeaderChar,
chunkExpression: /(?:^|\W)chunked(?:$|\W)/i,
continueExpression: /(?:^|\W)100-continue(?:$|\W)/i,
CRLF: "\r\n",
methods,
kIncomingMessage,
validateHeaderName,
validateHeaderValue,
};

View File

@@ -6,8 +6,6 @@ const {
NodeHTTPHeaderState,
kAbortController,
fakeSocketSymbol,
validateHeaderName,
validateHeaderValue,
headersSymbol,
kBodyChunks,
kEmitState,
@@ -23,6 +21,8 @@ const {
getRawKeys,
} = require("internal/http");
const { validateHeaderName, validateHeaderValue } = require("node:_http_common");
const { FakeSocket } = require("internal/http/FakeSocket");
function OutgoingMessage(options) {
@@ -161,7 +161,7 @@ const OutgoingMessagePrototype = {
setTimeout(msecs, callback) {
if (this.destroyed) return this;
this.timeout = msecs = validateMsecs(msecs, "msecs");
this.timeout = msecs = validateMsecs(msecs, "timeout");
// Attempt to clear an existing timer in both cases -
// even if it will be rescheduled we don't want to leak an existing timer.

View File

@@ -47,11 +47,11 @@ const { format } = require("internal/util/inspect");
const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
const { kIncomingMessage } = require("node:_http_common");
const getBunServerAllClosedPromise = $newZigFunction("node_http_binding.zig", "getBunServerAllClosedPromise", 1);
const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild", 3);
const kIncomingMessage = Symbol("IncomingMessage");
const kServerResponse = Symbol("ServerResponse");
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
const GlobalPromise = globalThis.Promise;

View File

@@ -1,11 +1,12 @@
const { validateInteger } = require("internal/validators");
const { Agent, globalAgent, NODE_HTTP_WARNING } = require("node:_http_agent");
const { ClientRequest } = require("node:_http_client");
const { validateHeaderName, validateHeaderValue } = require("node:_http_common");
const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
const { Server, ServerResponse } = require("node:_http_server");
const { validateHeaderName, validateHeaderValue, METHODS, STATUS_CODES } = require("internal/http");
const { METHODS, STATUS_CODES } = require("internal/http");
const { WebSocket, CloseEvent, MessageEvent } = globalThis;

View File

@@ -0,0 +1,82 @@
import { describe, expect, it } from "bun:test";
import { createServer } from "node:http";
import { request } from "node:http";
import { once } from "node:events";
describe("node:http client timeout", () => {
it("should emit timeout event when timeout is reached", async () => {
const server = createServer((req, res) => {
// Intentionally not sending response to trigger timeout
}).listen(0);
try {
await once(server, "listening");
const port = (server.address() as any).port;
const req = request({
port,
host: "localhost",
path: "/",
timeout: 50, // Set a short timeout
});
let timeoutEventEmitted = false;
let destroyCalled = false;
req.on("timeout", () => {
timeoutEventEmitted = true;
});
req.on("close", () => {
destroyCalled = true;
});
req.end();
// Wait for events to be emitted
await new Promise(resolve => setTimeout(resolve, 100));
expect(timeoutEventEmitted).toBe(true);
expect(destroyCalled).toBe(true);
expect(req.destroyed).toBe(true);
} finally {
server.close();
}
});
it("should clear timeout when explicitly set to 0", async () => {
const server = createServer((req, res) => {
res.end("OK");
}).listen(0);
try {
await once(server, "listening");
const port = (server.address() as any).port;
const req = request({
port,
host: "localhost",
path: "/",
});
let timeoutEventEmitted = false;
req.on("timeout", () => {
timeoutEventEmitted = true;
});
// Set and then clear timeout
req.setTimeout(50);
req.setTimeout(0);
req.end();
// Wait longer than the original timeout
await new Promise(resolve => setTimeout(resolve, 100));
expect(timeoutEventEmitted).toBe(false);
expect(req.destroyed).toBe(false);
} finally {
server.close();
}
});
});

View File

@@ -0,0 +1,27 @@
'use strict';
// This tests that the error emitted on the socket does
// not get fired again when the 'error' event handler throws
// an error.
const common = require('../common');
const { addresses } = require('../common/internet');
const { errorLookupMock } = require('../common/dns');
const assert = require('assert');
const http = require('http');
const host = addresses.INVALID_HOST;
const req = http.get({
host,
lookup: common.mustCall(errorLookupMock())
});
const err = new Error('mock unexpected code error');
req.on('error', common.mustCall(() => {
throw err;
}));
process.on('uncaughtException', common.mustCall((e) => {
assert.strictEqual(e, err);
}));

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
assert.throws(() => {
http.request({ timeout: null });
}, /The "timeout" argument must be of type number/);
const options = {
method: 'GET',
port: undefined,
host: '127.0.0.1',
path: '/',
timeout: 1
};
const server = http.createServer();
server.listen(0, options.host, function() {
options.port = this.address().port;
const req = http.request(options);
req.on('error', function() {
// This space is intentionally left blank
});
req.on('close', common.mustCall(() => {
assert.strictEqual(req.destroyed, true);
server.close();
}));
let timeout_events = 0;
req.on('timeout', common.mustCall(() => timeout_events += 1));
setTimeout(function() {
req.destroy();
assert.strictEqual(timeout_events, 1);
}, common.platformTimeout(100));
setTimeout(function() {
req.end();
}, common.platformTimeout(10));
});

View File

@@ -0,0 +1,33 @@
'use strict';
require('../common');
const assert = require('assert');
const httpCommon = require('_http_common');
const checkIsHttpToken = httpCommon._checkIsHttpToken;
const checkInvalidHeaderChar = httpCommon._checkInvalidHeaderChar;
// checkIsHttpToken
assert(checkIsHttpToken('t'));
assert(checkIsHttpToken('tt'));
assert(checkIsHttpToken('ttt'));
assert(checkIsHttpToken('tttt'));
assert(checkIsHttpToken('ttttt'));
assert.strictEqual(checkIsHttpToken(''), false);
assert.strictEqual(checkIsHttpToken(' '), false);
assert.strictEqual(checkIsHttpToken('あ'), false);
assert.strictEqual(checkIsHttpToken('あa'), false);
assert.strictEqual(checkIsHttpToken('aaaaあaaaa'), false);
// checkInvalidHeaderChar
assert(checkInvalidHeaderChar('あ'));
assert(checkInvalidHeaderChar('aaaaあaaaa'));
assert.strictEqual(checkInvalidHeaderChar(''), false);
assert.strictEqual(checkInvalidHeaderChar(1), false);
assert.strictEqual(checkInvalidHeaderChar(' '), false);
assert.strictEqual(checkInvalidHeaderChar(false), false);
assert.strictEqual(checkInvalidHeaderChar('t'), false);
assert.strictEqual(checkInvalidHeaderChar('tt'), false);
assert.strictEqual(checkInvalidHeaderChar('ttt'), false);
assert.strictEqual(checkInvalidHeaderChar('tttt'), false);
assert.strictEqual(checkInvalidHeaderChar('ttttt'), false);

View File

@@ -0,0 +1,78 @@
// 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');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http = require('http');
const https = require('https');
const host = '*'.repeat(64);
const MAX_TRIES = 5;
const errCodes = ['ENOTFOUND', 'EAI_FAIL'];
function tryGet(mod, tries) {
// Bad host name should not throw an uncatchable exception.
// Ensure that there is time to attach an error listener.
const req = mod.get({ host: host, port: 42 }, common.mustNotCall());
req.on('error', common.mustCall(function(err) {
if (err.code === 'EAGAIN' && tries < MAX_TRIES) {
tryGet(mod, ++tries);
return;
}
assert(errCodes.includes(err.code), err);
}));
// http.get() called req1.end() for us
}
function tryRequest(mod, tries) {
const req = mod.request({
method: 'GET',
host: host,
port: 42
}, common.mustNotCall());
req.on('error', common.mustCall(function(err) {
if (err.code === 'EAGAIN' && tries < MAX_TRIES) {
tryRequest(mod, ++tries);
return;
}
assert(errCodes.includes(err.code), err);
}));
req.end();
}
function test(mod) {
tryGet(mod, 0);
tryRequest(mod, 0);
}
if (common.hasCrypto) {
test(https);
} else {
common.printSkipMessage('missing crypto');
}
test(http);

View File

@@ -0,0 +1,88 @@
'use strict';
require('../common');
const assert = require('assert');
const inspect = require('util').inspect;
const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common');
// Good header field names
[
'TCN',
'ETag',
'date',
'alt-svc',
'Content-Type',
'0',
'Set-Cookie2',
'Set_Cookie',
'foo`bar^',
'foo|bar',
'~foobar',
'FooBar!',
'#Foo',
'$et-Cookie',
'%%Test%%',
'Test&123',
'It\'s_fun',
'2*3',
'4+2',
'3.14159265359',
].forEach(function(str) {
assert.strictEqual(
_checkIsHttpToken(str), true,
`_checkIsHttpToken(${inspect(str)}) unexpectedly failed`);
});
// Bad header field names
[
':',
'@@',
'中文呢', // unicode
'((((())))',
':alternate-protocol',
'alternate-protocol:',
'foo\nbar',
'foo\rbar',
'foo\r\nbar',
'foo\x00bar',
'\x7FMe!',
'{Start',
'(Start',
'[Start',
'End}',
'End)',
'End]',
'"Quote"',
'This,That',
].forEach(function(str) {
assert.strictEqual(
_checkIsHttpToken(str), false,
`_checkIsHttpToken(${inspect(str)}) unexpectedly succeeded`);
});
// Good header field values
[
'foo bar',
'foo\tbar',
'0123456789ABCdef',
'!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`',
].forEach(function(str) {
assert.strictEqual(
_checkInvalidHeaderChar(str), false,
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed`);
});
// Bad header field values
[
'foo\rbar',
'foo\nbar',
'foo\r\nbar',
'中文呢', // unicode
'\x7FMe!',
'Testing 123\x00',
'foo\vbar',
'Ding!\x07',
].forEach(function(str) {
assert.strictEqual(
_checkInvalidHeaderChar(str), true,
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded`);
});

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
// Verify that after calling end() on an `OutgoingMessage` (or a type that
// inherits from `OutgoingMessage`), its `writable` property is not set to false
const server = http.createServer(common.mustCall(function(req, res) {
assert.strictEqual(res.writable, true);
assert.strictEqual(res.finished, false);
assert.strictEqual(res.writableEnded, false);
res.end();
// res.writable is set to false after it has finished sending
// Ref: https://github.com/nodejs/node/issues/15029
assert.strictEqual(res.writable, true);
assert.strictEqual(res.finished, true);
assert.strictEqual(res.writableEnded, true);
server.close();
}));
server.listen(0);
server.on('listening', common.mustCall(function() {
const clientRequest = http.request({
port: server.address().port,
method: 'GET',
path: '/'
});
assert.strictEqual(clientRequest.writable, true);
clientRequest.end();
// Writable is still true when close
// THIS IS LEGACY, we cannot change it
// unless we break error detection
assert.strictEqual(clientRequest.writable, true);
}));

View File

@@ -0,0 +1,24 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('header1', 1);
res.write('abc');
assert.throws(
() => res.setHeader('header2', 2),
{
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
message: 'Cannot set headers after they are sent to the client'
}
);
res.end();
});
server.listen(0, () => {
http.get({ port: server.address().port }, () => {
server.close();
});
});

View File

@@ -0,0 +1,63 @@
'use strict';
require('../common');
const http = require('http');
const net = require('net');
const url = require('url');
const assert = require('assert');
const Countdown = require('../common/countdown');
// Response splitting example, credit: Amit Klein, Safebreach
const str = '/welcome?lang=bar%c4%8d%c4%8aContent-Length:%200%c4%8d%c4%8a%c' +
'4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContent-Length:%202' +
'0%c4%8d%c4%8aLast-Modified:%20Mon,%2027%20Oct%202003%2014:50:18' +
'%20GMT%c4%8d%c4%8aContent-Type:%20text/html%c4%8d%c4%8a%c4%8' +
'd%c4%8a%3chtml%3eGotcha!%3c/html%3e';
// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR)
const x = 'fooഊSet-Cookie: foo=barഊഊ<script>alert("Hi!")</script>';
const y = 'foo⠊Set-Cookie: foo=bar';
let count = 0;
const countdown = new Countdown(3, () => server.close());
function test(res, code, key, value) {
const header = { [key]: value };
assert.throws(
() => res.writeHead(code, header),
{
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: `Invalid character in header content ["${key}"]`
}
);
}
const server = http.createServer((req, res) => {
switch (count++) {
case 0: {
const loc = url.parse(req.url, true).query.lang;
test(res, 302, 'Location', `/foo?lang=${loc}`);
break;
}
case 1:
test(res, 200, 'foo', x);
break;
case 2:
test(res, 200, 'foo', y);
break;
default:
assert.fail('should not get to here.');
}
countdown.dec();
res.end('ok');
});
server.listen(0, () => {
const end = 'HTTP/1.1\r\nHost: example.com\r\n\r\n';
const client = net.connect({ port: server.address().port }, () => {
client.write(`GET ${str} ${end}`);
client.write(`GET / ${end}`);
client.write(`GET / ${end}`);
client.end();
});
});

View File

@@ -0,0 +1,86 @@
// 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 http = require('http');
const net = require('net');
const Countdown = require('../common/countdown');
const testCases = [
{ path: '/200', statusMessage: 'OK',
response: 'HTTP/1.1 200 OK\r\n\r\n' },
{ path: '/500', statusMessage: 'Internal Server Error',
response: 'HTTP/1.1 500 Internal Server Error\r\n\r\n' },
{ path: '/302', statusMessage: 'Moved Temporarily',
response: 'HTTP/1.1 302 Moved Temporarily\r\n\r\n' },
{ path: '/missing', statusMessage: '',
response: 'HTTP/1.1 200 \r\n\r\n' },
{ path: '/missing-no-space', statusMessage: '',
response: 'HTTP/1.1 200\r\n\r\n' },
];
testCases.findByPath = function(path) {
const matching = this.filter(function(testCase) {
return testCase.path === path;
});
if (matching.length === 0) {
assert.fail(`failed to find test case with path ${path}`);
}
return matching[0];
};
const server = net.createServer(function(connection) {
connection.on('data', function(data) {
const path = data.toString().match(/GET (.*) HTTP\/1\.1/)[1];
const testCase = testCases.findByPath(path);
connection.write(testCase.response);
connection.end();
});
});
const countdown = new Countdown(testCases.length, () => server.close());
function runTest(testCaseIndex) {
const testCase = testCases[testCaseIndex];
http.get({
port: server.address().port,
path: testCase.path
}, function(response) {
console.log(`client: expected status message: ${testCase.statusMessage}`);
console.log(`client: actual status message: ${response.statusMessage}`);
assert.strictEqual(testCase.statusMessage, response.statusMessage);
response.on('aborted', common.mustNotCall());
response.on('end', function() {
countdown.dec();
if (testCaseIndex + 1 < testCases.length) {
runTest(testCaseIndex + 1);
}
});
response.resume();
});
}
server.listen(0, function() { runTest(0); });

View File

@@ -0,0 +1,90 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');
const MAX_REQUESTS = 13;
let reqNum = 0;
function test(res, header, code) {
assert.throws(() => {
res.writeHead(header);
}, {
code: 'ERR_HTTP_INVALID_STATUS_CODE',
name: 'RangeError',
message: `Invalid status code: ${code}`
});
}
const server = http.Server(common.mustCall(function(req, res) {
switch (reqNum) {
case 0:
test(res, -1, '-1');
break;
case 1:
test(res, Infinity, 'Infinity');
break;
case 2:
test(res, NaN, 'NaN');
break;
case 3:
test(res, {}, '{}');
break;
case 4:
test(res, 99, '99');
break;
case 5:
test(res, 1000, '1000');
break;
case 6:
test(res, '1000', '1000');
break;
case 7:
test(res, null, 'null');
break;
case 8:
test(res, true, 'true');
break;
case 9:
test(res, [], '[]');
break;
case 10:
test(res, 'this is not valid', 'this is not valid');
break;
case 11:
test(res, '404 this is not valid either', '404 this is not valid either');
break;
case 12:
assert.throws(() => { res.writeHead(); },
{
code: 'ERR_HTTP_INVALID_STATUS_CODE',
name: 'RangeError',
message: 'Invalid status code: undefined'
});
this.close();
break;
default:
throw new Error('Unexpected request');
}
res.statusCode = 200;
res.end();
}, MAX_REQUESTS));
server.listen();
const countdown = new Countdown(MAX_REQUESTS, () => server.close());
server.on('listening', function makeRequest() {
http.get({
port: this.address().port
}, (res) => {
assert.strictEqual(res.statusCode, 200);
res.on('end', () => {
countdown.dec();
reqNum = MAX_REQUESTS - countdown.remaining;
if (countdown.remaining > 0)
makeRequest.call(this);
});
res.resume();
});
});