mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
More node:http compatibility (#19173)
This commit is contained in:
@@ -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 } },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
85
src/js/node/_http_common.ts
Normal file
85
src/js/node/_http_common.ts
Normal 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,
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
82
test/js/node/http/client-timeout-error.test.ts
Normal file
82
test/js/node/http/client-timeout-error.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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));
|
||||
});
|
||||
33
test/js/node/test/parallel/test-http-common.js
Normal file
33
test/js/node/test/parallel/test-http-common.js
Normal 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);
|
||||
78
test/js/node/test/parallel/test-http-dns-error.js
Normal file
78
test/js/node/test/parallel/test-http-dns-error.js
Normal 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);
|
||||
88
test/js/node/test/parallel/test-http-invalidheaderfield2.js
Normal file
88
test/js/node/test/parallel/test-http-invalidheaderfield2.js
Normal 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`);
|
||||
});
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
63
test/js/node/test/parallel/test-http-response-splitting.js
Normal file
63
test/js/node/test/parallel/test-http-response-splitting.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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); });
|
||||
90
test/js/node/test/parallel/test-http-response-statuscode.js
Normal file
90
test/js/node/test/parallel/test-http-response-statuscode.js
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user