Compare commits

...

62 Commits

Author SHA1 Message Date
Meghan Denny
ec67c571c1 last one 2025-10-31 09:49:17 +01:00
Jarred Sumner
f7c1f61092 asan 2025-10-31 08:50:33 +01:00
Meghan Denny
82bd167124 windows 2025-10-30 22:45:33 -07:00
Meghan Denny
9f0f413514 address ci 2025-10-30 22:14:17 -07:00
Meghan Denny
24b53d4761 oopies 2025-10-30 20:28:48 -07:00
Meghan Denny
9e35139d48 Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-10-30 19:05:15 -07:00
Meghan Denny
16ebb9ff4b address review 2025-10-30 19:05:01 -07:00
Meghan Denny
37eb2e109a Merge branch 'main' into nektro-patch-44307 2025-10-28 14:13:04 -08:00
Meghan Denny
efa796686c Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-10-23 16:30:33 -07:00
Meghan Denny
fd9661444e oops 2025-10-09 20:45:12 -07:00
Meghan Denny
91fd7b8539 address merge 2025-10-09 20:19:18 -07:00
Meghan Denny
f82c3d2647 Merge branch 'main' into nektro-patch-44307 2025-10-09 18:15:07 -08:00
Meghan Denny
8ba4ecc645 address node-http.test.ts 2025-10-09 17:16:47 -07:00
Meghan Denny
e87f28b8ff fix these too 2025-10-08 17:30:20 -07:00
Meghan Denny
80444cbe5f fix from merge 2025-10-08 16:58:10 -07:00
Meghan Denny
b5005bacce remove dupe of test-https-get-can-use-Agent.ts 2025-10-08 16:14:20 -07:00
Meghan Denny
19b35561fd Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-10-08 16:01:43 -07:00
Meghan Denny
7ccbe81f55 more fix 2025-10-08 15:58:31 -07:00
Meghan Denny
851b4c8946 fix these 2025-10-01 16:52:38 -07:00
Meghan Denny
a45ec96a46 Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-09-30 17:28:51 -07:00
Meghan Denny
0003e28e85 Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-09-29 22:54:35 -07:00
Meghan Denny
5174f56dde Merge branch 'main' into nektro-patch-44307 2025-09-22 10:22:31 -08:00
Meghan Denny
e3ea1b50a4 Merge branch 'main' into nektro-patch-44307 2025-09-12 16:57:17 -08:00
Meghan Denny
25f06a32b1 new and only in release
investigate in followup
2025-09-10 23:59:23 -07:00
Meghan Denny
6fb772a295 windows 2025-09-10 23:58:35 -07:00
Meghan Denny
ef74316ca9 undo this change 2025-09-10 23:56:34 -07:00
Meghan Denny
9f78fd7dd6 runner: newFiles calc was slightly wrong 2025-09-10 23:55:45 -07:00
Meghan Denny
90f45180fb fix bun-types.test.ts 2025-09-10 00:19:08 -07:00
Meghan Denny
94dd5d5bdb fix client-timeout-error.test.ts 2025-09-10 00:17:45 -07:00
Meghan Denny
4898e4ef0c Merge branch 'main' into nektro-patch-44307 2025-09-09 20:08:52 -08:00
Meghan Denny
4f9ad23fa5 fix test-async-local-storage-http-multiclients.js 2025-09-09 20:32:00 -07:00
Meghan Denny
b0fc59a836 bun-types: fill out HTTPParser 2025-09-09 20:29:31 -07:00
Meghan Denny
16b260e3f7 stripe.test.ts needs this too 2025-09-09 20:29:01 -07:00
Meghan Denny
81489d39cb windows 2025-09-09 20:27:55 -07:00
Meghan Denny
3ec8e702c6 22234 fixed this 2025-09-09 20:27:07 -07:00
Meghan Denny
ef89084b44 fix stripe.test.ts 2025-09-08 20:10:37 -07:00
Meghan Denny
78fef11e64 fix test-http-outgoing-finish and test-http-keep-alive-large-write 2025-09-04 18:42:00 -07:00
Meghan Denny
c573f05a47 fix test-http-head-request 2025-09-04 18:36:54 -07:00
Meghan Denny
dc8fd146ed add test-http-client-keep-alive-release-before-finish.js back 2025-09-04 18:02:55 -07:00
Meghan Denny
160d2e2b2b bump 2025-09-04 17:55:04 -07:00
Meghan Denny
e8bc2623ec Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-09-04 15:48:38 -07:00
Meghan Denny
e5a0f0386c fix verdaccio and others 2025-08-25 18:21:33 -07:00
Meghan Denny
096a711247 fix more node test timeouts 2025-08-25 17:18:16 -07:00
Meghan Denny
3d4dc08905 missing tidy 2025-08-25 16:28:13 -07:00
Meghan Denny
85c2652fa3 tidy and fix some timeouts 2025-08-25 15:42:05 -07:00
Meghan Denny
5c8cfa44f3 Merge remote-tracking branch 'origin/main' into nektro-patch-44307 2025-08-25 11:24:49 -07:00
Meghan Denny
3ff6af3236 integrate OutgoingMessage old with new 2025-08-23 01:14:16 -07:00
Meghan Denny
c23d8a9d61 integrate IncomingMessage old with new 2025-08-23 01:11:49 -07:00
Meghan Denny
5caf27484d back out 2025-08-22 18:58:26 -07:00
Meghan Denny
8750884853 rework server too 2025-08-22 18:21:13 -07:00
Meghan Denny
c1453a664d more fix 2025-08-22 17:31:09 -07:00
autofix-ci[bot]
6c8a9d3da4 [autofix.ci] apply automated fixes 2025-08-19 21:53:53 +00:00
Meghan Denny
3907641cfd temp 2025-08-19 00:25:24 -07:00
Meghan Denny
5ab767e993 some more 2025-08-18 21:59:10 -07:00
Meghan Denny
87878d4c0a Merge branch 'main' into nektro-patch-44307 2025-08-15 09:54:46 -08:00
Meghan Denny
95064ceb7b clean prototype of Server and ServerResponse 2025-08-15 00:31:12 -07:00
Meghan Denny
e4a4b9179e temp 2025-08-14 20:09:59 -07:00
Meghan Denny
2c06cfd119 temp 2025-08-14 20:04:12 -07:00
Meghan Denny
849db19655 a bit more 2025-08-14 19:06:41 -07:00
Meghan Denny
0447c4743b some cleanup 2025-08-12 22:25:54 -07:00
Meghan Denny
c5deba01d4 fixups and notes 2025-08-12 19:40:51 -07:00
Meghan Denny
655cc58d90 node:http client rework 2025-08-12 18:46:36 -07:00
159 changed files with 8867 additions and 2015 deletions

View File

@@ -615,7 +615,7 @@ function getTestBunStep(platform, options, testOptions = {}) {
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
parallelism: unifiedTests ? undefined : os === "darwin" ? 2 : 10,
timeout_in_minutes: profile === "asan" || os === "windows" ? 45 : 30,
timeout_in_minutes: 120,
env: {
ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0",
},

View File

@@ -60,6 +60,7 @@
"lint:fix": "oxlint --config oxlint.json --fix",
"test": "node scripts/runner.node.mjs --exec-path ./build/debug/bun-debug",
"test:release": "node scripts/runner.node.mjs --exec-path ./build/release/bun",
"testleak": "BUN_DESTRUCT_VM_ON_EXIT=1 ASAN_OPTIONS=detect_leaks=1 LSAN_OPTIONS=malloc_context_size=100:print_suppressions=1:suppressions=$npm_config_local_prefix/test/leaksan.supp ./build/debug/bun-debug",
"banned": "bun test test/internal/ban-words.test.ts",
"glob-sources": "bun scripts/glob-sources.mjs",
"zig": "vendor/zig/zig.exe",

View File

@@ -1,5 +1,7 @@
export {};
type TODO = any;
declare module "stream/web" {
interface ReadableStream {
/**
@@ -261,7 +263,7 @@ declare global {
"FLUSH",
"QUERY",
];
HTTPParser: unknown;
HTTPParser: HTTPParserConstructor;
ConnectionsList: unknown;
};
binding(m: string): object;
@@ -270,6 +272,34 @@ declare global {
interface ProcessVersions extends Dict<string> {
bun: string;
}
interface HTTPParserConstructor {
new (): TODO;
REQUEST: 1;
RESPONSE: 2;
kOnMessageBegin: 0;
kOnHeaders: 1;
kOnHeadersComplete: 2;
kOnBody: 3;
kOnMessageComplete: 4;
kOnExecute: 5;
kOnTimeout: 6;
kLenientNone: 0;
kLenientHeaders: 1;
kLenientChunkedLength: 2;
kLenientKeepAlive: 4;
kLenientTransferEncoding: 8;
kLenientVersion: 16;
kLenientDataAfterClose: 32;
kLenientOptionalLFAfterCR: 64;
kLenientOptionalCRLFAfterChunk: 128;
kLenientOptionalCRBeforeLF: 256;
kLenientSpacesAfterChunkSize: 512;
kLenientAll: 1023;
}
}
}

View File

@@ -303,6 +303,8 @@ public:
auto context = (struct us_socket_context_t *)this->httpContext;
struct us_socket_t *s = context->head_sockets;
while (s) {
HttpResponseData<SSL> *httpResponseData = HttpResponse<SSL>::getHttpResponseDataS(s);
httpResponseData->shouldCloseOnceIdle = true;
// no matter the type of socket will always contain the AsyncSocketData
auto *data = ((AsyncSocket<SSL> *) s)->getAsyncSocketData();
struct us_socket_t *next = s->next;

View File

@@ -195,7 +195,7 @@ private:
/* Call filter */
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
if(httpResponseData && httpResponseData->isConnectRequest) {
if (httpResponseData->socketData && httpContextData->onSocketData) {
httpContextData->onSocketData(httpResponseData->socketData, SSL, s, "", 0, true);
@@ -203,7 +203,7 @@ private:
if(httpResponseData->inStream) {
httpResponseData->inStream(reinterpret_cast<HttpResponse<SSL> *>(s), "", 0, true, httpResponseData->userData);
httpResponseData->inStream = nullptr;
}
}
}
@@ -253,7 +253,7 @@ private:
/* Mark that we are inside the parser now */
httpContextData->flags.isParsingHttp = true;
httpResponseData->isIdle = false;
// clients need to know the cursor after http parse, not servers!
// how far did we read then? we need to know to continue with websocket parsing data? or?
@@ -266,7 +266,7 @@ private:
auto result = httpResponseData->consumePostPadded(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
/* For every request we reset the timeout and hang until user makes action */
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
us_socket_timeout(SSL, (us_socket_t *) s, 0);
@@ -391,6 +391,7 @@ private:
/* Mark that we are no longer parsing Http */
httpContextData->flags.isParsingHttp = false;
/* If we got fullptr that means the parser wants us to close the socket from error (same as calling the errorHandler) */
if (httpErrorStatusCode) {
if(httpContextData->onClientError) {
@@ -425,9 +426,20 @@ private:
/* We need to force close after sending FIN since we want to hinder
* clients from keeping to send their huge data */
((AsyncSocket<SSL> *) s)->close();
return (us_socket_t *) returnedData;
}
}
}
/* Check if we should gracefully close the socket after parsing HTTP */
if (httpResponseData->shouldCloseOnceIdle && !(httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING)) {
/* Gracefully close the socket by shutting down and then closing */
if (!us_socket_is_closed(SSL, s) && !us_socket_is_shut_down(SSL, s)) {
us_socket_shutdown(SSL, s);
us_socket_shutdown_read(SSL, s);
}
}
return (us_socket_t *) returnedData;
}
@@ -466,7 +478,7 @@ private:
us_socket_context_on_writable(SSL, getSocketContext(), [](us_socket_t *s) {
auto *asyncSocket = reinterpret_cast<AsyncSocket<SSL> *>(s);
auto *httpResponseData = reinterpret_cast<HttpResponseData<SSL> *>(asyncSocket->getAsyncSocketData());
/* Attempt to drain the socket buffer before triggering onWritable callback */
size_t bufferedAmount = asyncSocket->getBufferedAmount();
if (bufferedAmount > 0) {
@@ -537,7 +549,7 @@ private:
us_socket_context_on_end(SSL, getSocketContext(), [](us_socket_t *s) {
auto *asyncSocket = reinterpret_cast<AsyncSocket<SSL> *>(s);
asyncSocket->uncorkWithoutSending();
/* We do not care for half closed sockets */
return asyncSocket->close();
});

View File

@@ -38,6 +38,11 @@
#include "ProxyParser.h"
#include "QueryParser.h"
#include "HttpErrors.h"
#if defined(_WIN32)
#define strncasecmp _strnicmp
#endif
extern "C" size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;
extern "C" int16_t Bun__HTTPMethod__from(const char *str, size_t len);
@@ -232,11 +237,11 @@ namespace uWS
TransferEncoding getTransferEncoding()
{
TransferEncoding te;
if (!bf.mightHave("transfer-encoding")) {
return te;
}
for (Header *h = headers; (++h)->key.length();) {
if (h->key.length() == 17 && !strncmp(h->key.data(), "transfer-encoding", 17)) {
// Parse comma-separated values, ensuring "chunked" is last if present
@@ -244,33 +249,33 @@ namespace uWS
size_t pos = 0;
size_t lastTokenStart = 0;
size_t lastTokenLen = 0;
while (pos < value.length()) {
// Skip leading whitespace
while (pos < value.length() && (value[pos] == ' ' || value[pos] == '\t')) {
pos++;
}
// Remember start of this token
size_t tokenStart = pos;
// Find end of token (until comma or end)
while (pos < value.length() && value[pos] != ',') {
pos++;
}
// Trim trailing whitespace from token
size_t tokenEnd = pos;
while (tokenEnd > tokenStart && (value[tokenEnd - 1] == ' ' || value[tokenEnd - 1] == '\t')) {
tokenEnd--;
}
size_t tokenLen = tokenEnd - tokenStart;
if (tokenLen > 0) {
lastTokenStart = tokenStart;
lastTokenLen = tokenLen;
}
// Move past comma if present
if (pos < value.length() && value[pos] == ',') {
pos++;
@@ -283,12 +288,12 @@ namespace uWS
}
te.has = lastTokenLen > 0;
// Check if the last token is "chunked"
if (lastTokenLen == 7 && !strncmp(value.data() + lastTokenStart, "chunked", 7)) [[likely]] {
if (lastTokenLen == 7 && strncasecmp(value.data() + lastTokenStart, "chunked", 7) == 0) [[likely]] {
te.chunked = true;
}
}
}
@@ -852,7 +857,7 @@ namespace uWS
* ought to be handled as an error. */
const std::string_view contentLengthString = req->getHeader("content-length");
const auto contentLengthStringLen = contentLengthString.length();
/* Check Transfer-Encoding header validity and conflicts */
HttpRequest::TransferEncoding transferEncoding = req->getTransferEncoding();
@@ -962,7 +967,7 @@ public:
data = (char *) dataToConsume.data();
length = (unsigned int) dataToConsume.length();
} else {
// this is exactly the same as below!
// todo: refactor this
if (remainingStreamingBytes >= length) {

View File

@@ -125,6 +125,10 @@ public:
}
}
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER) {
allowContentLength = false;
}
/* if write was called and there was previously no Content-Length header set */
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_WRITE_CALLED && !(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_CONTENT_LENGTH_HEADER) && !httpResponseData->fromAncientRequest) {

View File

@@ -87,6 +87,7 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
HTTP_CONNECTION_CLOSE = 16, // used
HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, // used
HTTP_WROTE_DATE_HEADER = 64, // used
HTTP_WROTE_TRANSFER_ENCODING_HEADER = 128, // used
};
/* Shared context pointer */
@@ -109,6 +110,7 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
uint8_t idleTimeout = 10; // default HTTP_TIMEOUT 10 seconds
bool fromAncientRequest = false;
bool isConnectRequest = false;
bool shouldCloseOnceIdle = false;
#ifdef UWS_WITH_PROXY
ProxyParser proxyParser;

View File

@@ -81,6 +81,7 @@ function getNodeParallelTestTimeout(testPath) {
return 90_000;
}
if (!isCI) return 60_000; // everything slower in debug mode
if (options["step"]?.includes("-asan-")) return 60_000;
return 20_000;
}
@@ -202,12 +203,12 @@ if (isBuildkite) {
const doc = await res.json();
console.log(`-> page ${i}, found ${doc.length} items`);
if (doc.length === 0) break;
if (doc.length < per_page) break;
for (const { filename, status } of doc) {
prFileCount += 1;
if (status !== "added") continue;
newFiles.push(filename);
}
if (doc.length < per_page) break;
}
console.log(`- PR ${process.env.BUILDKITE_PULL_REQUEST}, ${prFileCount} files, ${newFiles.length} new files`);
} catch (e) {
@@ -1579,8 +1580,7 @@ function isJavaScriptTest(path) {
* @returns {boolean}
*/
function isNodeTest(path) {
// Do not run node tests on macOS x64 in CI
// TODO: Unclear why we decided to do this?
// Do not run node tests on macOS x64 in CI, those machines are slow.
if (isCI && isMacOS && isX64) {
return false;
}

View File

@@ -7,6 +7,11 @@ const char* __asan_default_options(void)
// which breaks some JSC classes that have to be on the stack:
// ASSERTION FAILED: Thread::currentSingleton().stack().contains(this)
// cache/webkit-eda8b0fb4fb1aa23/include/JavaScriptCore/JSGlobalObjectInlines.h(63) : JSC::JSGlobalObject::GlobalPropertyInfo::GlobalPropertyInfo(const Identifier &, JSValue, unsigned int)
return "detect_stack_use_after_return=0";
// > https://clang.llvm.org/docs/AddressSanitizer.html#memory-leak-detection
// > The leak detection is turned on by default on Linux, and can be enabled using ASAN_OPTIONS=detect_leaks=1 on macOS.
// we want it to always be opt-in
return "detect_stack_use_after_return=0:detect_leaks=0";
}
#endif

View File

@@ -687,7 +687,7 @@ pub fn NewSocket(comptime ssl: bool) type {
}
pub fn getListener(this: *This, _: *jsc.JSGlobalObject) JSValue {
const handlers = this.getHandlers();
const handlers = this.handlers orelse return .js_undefined;
if (!handlers.is_server or this.socket.isDetached()) {
return .js_undefined;

View File

@@ -566,12 +566,19 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
inspector_server_id: jsc.Debugger.DebuggerId = .init(0),
pub const doStop = host_fn.wrapInstanceMethod(ThisServer, "stopFromJS", false);
pub const dispose = host_fn.wrapInstanceMethod(ThisServer, "disposeFromJS", false);
pub const doUpgrade = host_fn.wrapInstanceMethod(ThisServer, "onUpgrade", false);
pub const doPublish = host_fn.wrapInstanceMethod(ThisServer, "publish", false);
pub const doReload = onReload;
pub const doFetch = onFetch;
pub const doRequestIP = host_fn.wrapInstanceMethod(ThisServer, "requestIP", false);
pub const doTimeout = timeout;
pub const UserRoute = struct {

View File

@@ -2375,6 +2375,26 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_ZSTD_INVALID_PARAM, message));
}
case Bun::ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto arg1 = callFrame->argument(2);
auto str1 = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto message = makeString("Response body's content-length of "_s, str0, " byte(s) does not match the content-length of "_s, str1, " byte(s) set in header"_s);
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH, message));
}
case ErrorCode::ERR_SSL_NO_CIPHER_MATCH: {
auto arg0 = callFrame->argument(1);
auto err = createError(globalObject, ErrorCode::ERR_SSL_NO_CIPHER_MATCH, "No cipher match"_s);
err->putDirect(vm, Identifier::fromString(vm, "reason"_s), JSC::jsString(vm, WTF::String("no cipher match"_s)));
err->putDirect(vm, Identifier::fromString(vm, "library"_s), JSC::jsString(vm, WTF::String("SSL routines"_s)));
err->putDirect(vm, Identifier::fromString(vm, "cipher"_s), arg0);
return JSC::JSValue::encode(err);
}
case ErrorCode::ERR_IPC_DISCONNECTED:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s));
case ErrorCode::ERR_SERVER_NOT_RUNNING:
@@ -2435,17 +2455,6 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_CERT_ALTNAME_FORMAT, "Invalid subject alternative name string"_s));
case ErrorCode::ERR_TLS_SNI_FROM_SERVER:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_SNI_FROM_SERVER, "Cannot issue SNI from a TLS server-side socket"_s));
case ErrorCode::ERR_SSL_NO_CIPHER_MATCH: {
auto err = createError(globalObject, ErrorCode::ERR_SSL_NO_CIPHER_MATCH, "No cipher match"_s);
auto reason = JSC::jsString(vm, WTF::String("no cipher match"_s));
err->putDirect(vm, Identifier::fromString(vm, "reason"_s), reason);
auto library = JSC::jsString(vm, WTF::String("SSL routines"_s));
err->putDirect(vm, Identifier::fromString(vm, "library"_s), library);
return JSC::JSValue::encode(err);
}
case ErrorCode::ERR_INVALID_URI:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_URI, "URI malformed"_s));
case ErrorCode::ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED:
@@ -2508,6 +2517,12 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT, "Linked modules must use the same context"_s));
case ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s));
case ErrorCode::ERR_HTTP_TRAILER_INVALID:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_TRAILER_INVALID, "Trailers are invalid with this transfer encoding"_s));
case ErrorCode::ERR_HTTP_SOCKET_ENCODING:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_SOCKET_ENCODING, "Changing the socket encoding is not allowed per RFC7230 Section 3."_s));
case ErrorCode::ERR_HTTP_REQUEST_TIMEOUT:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_REQUEST_TIMEOUT, "Request timeout"_s));
case ErrorCode::ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, "The ALPNCallback and ALPNProtocols TLS options are mutually exclusive"_s));
case ErrorCode::ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS:

View File

@@ -87,7 +87,9 @@ const errors: ErrorCodeMapping = [
["ERR_HTTP_INVALID_HEADER_VALUE", TypeError],
["ERR_HTTP_INVALID_STATUS_CODE", RangeError],
["ERR_HTTP_TRAILER_INVALID", Error],
["ERR_HTTP_REQUEST_TIMEOUT", Error],
["ERR_HTTP_SOCKET_ASSIGNED", Error],
["ERR_HTTP_SOCKET_ENCODING", Error],
["ERR_HTTP2_ALTSVC_INVALID_ORIGIN", TypeError],
["ERR_HTTP2_ALTSVC_LENGTH", TypeError],
["ERR_HTTP2_CONNECT_AUTHORITY", Error],

View File

@@ -132,13 +132,12 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
size++;
}
JSC::JSObject* headersObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(size, static_cast<size_t>(JSFinalObject::maxInlineCapacity)));
RETURN_IF_EXCEPTION(scope, void());
JSC::JSArray* setCookiesHeaderArray = nullptr;
JSC::JSString* setCookiesHeaderString = nullptr;
MarkedArgumentBuffer arrayValues;
args.append(headersObject);
HashMap<RefPtr<UniquedStringImpl>, JSValue, IdentifierRepHash, HashTraits<RefPtr<UniquedStringImpl>>> headersMap;
headersMap.reserveInitialCapacity(std::min(size, static_cast<size_t>(JSFinalObject::maxInlineCapacity)));
for (auto it = request->begin(); it != request->end(); ++it) {
auto pair = *it;
@@ -170,22 +169,38 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr);
RETURN_IF_EXCEPTION(scope, );
setCookiesHeaderString = nameString;
headersObject->putDirect(vm, nameIdentifier, setCookiesHeaderArray, 0);
headersMap.set(nameIdentifier.impl(), setCookiesHeaderArray);
RETURN_IF_EXCEPTION(scope, void());
}
arrayValues.append(setCookiesHeaderString);
arrayValues.append(jsValue);
setCookiesHeaderArray->push(globalObject, jsValue);
RETURN_IF_EXCEPTION(scope, void());
} else {
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
RETURN_IF_EXCEPTION(scope, void());
if (auto prev = headersMap.get(nameIdentifier.impl())) {
if (WTF::equal(nameIdentifier.impl(), "host"_s)) {
continue;
}
headersMap.set(nameIdentifier.impl(), JSValue(jsString(vm, makeString(prev.getString(globalObject), ", "_s, jsValue))));
arrayValues.append(nameString);
arrayValues.append(jsValue);
continue;
}
headersMap.set(nameIdentifier.impl(), JSValue(jsValue));
arrayValues.append(nameString);
arrayValues.append(jsValue);
RETURN_IF_EXCEPTION(scope, void());
}
}
{
JSC::JSObject* headersObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(size, static_cast<size_t>(JSFinalObject::maxInlineCapacity)));
RETURN_IF_EXCEPTION(scope, );
for (auto& entry : headersMap) {
Identifier identifier = Identifier::fromUid(vm, entry.key.get());
headersObject->putDirectMayBeIndex(globalObject, identifier, entry.value);
RETURN_IF_EXCEPTION(scope, );
}
args.append(headersObject);
}
JSC::JSArray* array;
{
@@ -557,6 +572,10 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
}
}
if (header.key == WebCore::HTTPHeaderName::TransferEncoding) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER;
}
// Prevent automatic Date header insertion when user provides one
if (header.key == WebCore::HTTPHeaderName::Date) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER;

View File

@@ -71,7 +71,7 @@ struct StringPtr {
{
auto& vm = globalObject->vm();
if (m_size != 0) {
return JSC::jsString(vm, WTF::String::fromUTF8({ m_str, m_size }));
return JSC::jsString(vm, WTF::String({ m_str, m_size }));
}
return jsEmptyString(vm);
}

View File

@@ -766,6 +766,8 @@ declare function $ERR_ASYNC_CALLBACK(name): TypeError;
declare function $ERR_AMBIGUOUS_ARGUMENT(arg, message): TypeError;
declare function $ERR_INVALID_FD_TYPE(type): TypeError;
declare function $ERR_IP_BLOCKED(ip): Error;
declare function $ERR_HTTP_CONTENT_LENGTH_MISMATCH(body, header);
declare function $ERR_SSL_NO_CIPHER_MATCH(cipher): Error;
declare function $ERR_IPC_DISCONNECTED(): Error;
declare function $ERR_SERVER_NOT_RUNNING(): Error;
@@ -795,7 +797,6 @@ declare function $ERR_TLS_RENEGOTIATION_DISABLED(): Error;
declare function $ERR_UNAVAILABLE_DURING_EXIT(): Error;
declare function $ERR_TLS_CERT_ALTNAME_FORMAT(): SyntaxError;
declare function $ERR_TLS_SNI_FROM_SERVER(): Error;
declare function $ERR_SSL_NO_CIPHER_MATCH(): Error;
declare function $ERR_INVALID_URI(): URIError;
declare function $ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED(): TypeError;
declare function $ERR_HTTP2_INFO_STATUS_NOT_ALLOWED(): RangeError;
@@ -830,6 +831,9 @@ declare function $ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA(): Error;
declare function $ERR_VM_MODULE_NOT_MODULE(): Error;
declare function $ERR_VM_MODULE_DIFFERENT_CONTEXT(): Error;
declare function $ERR_VM_MODULE_LINK_FAILURE(message: string, cause: Error): Error;
declare function $ERR_HTTP_TRAILER_INVALID(): Error;
declare function $ERR_HTTP_SOCKET_ENCODING(): Error;
declare function $ERR_HTTP_REQUEST_TIMEOUT(): Error;
declare function $ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(): TypeError;
declare function $ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(): Error;
declare function $ERR_HTTP2_CONNECT_AUTHORITY(): Error;

View File

@@ -199,14 +199,6 @@ function validateMsecs(numberlike: any, field: string) {
return numberlike;
}
class ConnResetException extends Error {
constructor(msg) {
super(msg);
this.code = "ECONNRESET";
this.name = "ConnResetException";
}
}
const METHODS = [
"ACL",
"BIND",
@@ -348,6 +340,20 @@ function hasServerResponseFinished(self, chunk, callback) {
return false;
}
let utcCache;
function utcDate() {
if (!utcCache) cache();
return utcCache;
}
function cache() {
const d = new Date();
utcCache = d.toUTCString();
setTimeout(resetCache, 1000 - d.getMilliseconds()).unref();
}
function resetCache() {
utcCache = undefined;
}
function emitErrorNt(msg, err, callback) {
if ($isCallable(callback)) {
callback(err);
@@ -359,9 +365,10 @@ function emitErrorNt(msg, err, callback) {
const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeaderSize", 1);
const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0);
const kOutHeaders = Symbol("kOutHeaders");
const kNeedDrain = Symbol("kNeedDrain");
const kBunServer = Symbol("kBunServer");
export {
ConnResetException,
Headers,
METHODS,
STATUS_CODES,
@@ -393,6 +400,7 @@ export {
kAbortController,
kAgent,
kBodyChunks,
kBunServer,
kClearTimeout,
kCloseCallback,
kDeferredTimeouts,
@@ -406,6 +414,7 @@ export {
kMaxHeaderSize,
kMaxHeadersCount,
kMethod,
kNeedDrain,
kOptions,
kOutHeaders,
kParser,
@@ -439,6 +448,7 @@ export {
timeoutTimerSymbol,
tlsSymbol,
typeSymbol,
utcDate,
validateMsecs,
webRequestOrResponse,
webRequestOrResponseHasBodyValue,

View File

@@ -17,6 +17,9 @@ class NotImplementedError extends Error {
// in the definition so that it isn't bundled unless used
hideFromStack(NotImplementedError);
}
get ["constructor"]() {
return Error;
}
}
function throwNotImplemented(feature: string, issue?: number, extra?: string): never {
@@ -78,6 +81,9 @@ class ExceptionWithHostPort extends Error {
this.port = port;
}
}
get ["constructor"]() {
return Error;
}
}
class NodeAggregateError extends AggregateError {
@@ -85,12 +91,21 @@ class NodeAggregateError extends AggregateError {
super(new SafeArrayIterator(errors), message);
this.code = errors[0]?.code;
}
get ["constructor"]() {
return AggregateError;
}
}
class ConnResetException extends Error {
constructor(msg) {
super(msg);
this.code = "ECONNRESET";
}
get ["constructor"]() {
return Error;
}
}
class ErrnoException extends Error {
errno: number;
syscall: string;
@@ -106,13 +121,12 @@ class ErrnoException extends Error {
this.code = code;
this.syscall = syscall;
}
get ["constructor"]() {
return Error;
}
}
function once(callback, { preserveReturnValue = false } = kEmptyObject) {
function once(callback, { preserveReturnValue = false } = kEmptyObject as any) {
let called = false;
let returnValue;
return function (...args) {
@@ -135,6 +149,7 @@ export default {
warnNotImplementedOnce,
ExceptionWithHostPort,
NodeAggregateError,
ConnResetException,
ErrnoException,
once,

View File

@@ -18,7 +18,7 @@ function highWaterMarkFrom(
return options.highWaterMark != null ? options.highWaterMark : isDuplex ? options[duplexKey] : null;
}
function getDefaultHighWaterMark(objectMode?: boolean): number {
function getDefaultHighWaterMark(objectMode: boolean): number {
return objectMode ? defaultHighWaterMarkObjectMode : defaultHighWaterMarkBytes;
}

View File

@@ -19,6 +19,11 @@ function urlToHttpOptions(url) {
return options;
}
function isURL(self) {
return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined);
}
export default {
urlToHttpOptions,
isURL,
};

View File

@@ -1,153 +1,487 @@
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
"use strict";
const { kEmptyObject } = require("internal/http");
const net = require("node:net");
const EventEmitter = require("node:events");
// const { AsyncResource } = require("async_hooks");
// const { async_id_symbol } = require("internal/async_hooks").symbols;
const { kEmptyObject, once } = require("internal/shared");
const { validateNumber, validateOneOf, validateString } = require("internal/validators");
const { FakeSocket } = require("internal/http/FakeSocket");
const NumberParseInt = Number.parseInt;
const ObjectKeys = Object.keys;
const ObjectValues = Object.values;
const ObjectDefineProperty = Object.defineProperty;
const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
const kRequestAsyncResource = Symbol("requestAsyncResource");
const kfakeSocket = Symbol("kfakeSocket");
// TODO(): make this configurable
const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
// New Agent code.
const NODE_HTTP_WARNING =
"WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause.";
// The largest departure from the previous implementation is that
// an Agent instance holds connections for a variable number of host:ports.
// Surprisingly, this is still API compatible as far as third parties are
// concerned. The only code that really notices the difference is the
// request object.
// Define Agent interface
interface Agent extends InstanceType<typeof EventEmitter> {
defaultPort: number;
protocol: string;
options: any;
requests: Record<string, any>;
sockets: Record<string, any>;
freeSockets: Record<string, any>;
keepAliveMsecs: number;
keepAlive: boolean;
maxSockets: number;
maxFreeSockets: number;
scheduling: string;
maxTotalSockets: any;
totalSocketCount: number;
[kfakeSocket]?: any;
// Another departure is that all code related to HTTP parsing is in
// ClientRequest.onSocket(). The Agent is now *strictly*
// concerned with managing a connection pool.
createConnection(): any;
getName(options?: any): string;
addRequest(): void;
createSocket(req: any, options: any, cb: (err: any, socket: any) => void): void;
removeSocket(): void;
keepSocketAlive(): boolean;
reuseSocket(): void;
destroy(): void;
class ReusedHandle {
type;
handle;
constructor(type, handle) {
this.type = type;
this.handle = handle;
}
}
// Define the constructor interface
interface AgentConstructor {
new (options?: any): Agent;
(options?: any): Agent;
defaultMaxSockets: number;
globalAgent: Agent;
prototype: Agent;
function freeSocketErrorListener(err) {
const socket = this;
$debug("SOCKET ERROR on FREE socket:", err.message, err.stack);
socket.destroy();
socket.emit("agentRemove");
}
function Agent(options = kEmptyObject) {
function Agent(options): void {
if (!(this instanceof Agent)) return new Agent(options);
EventEmitter.$apply(this, []);
EventEmitter.$call(this);
this.defaultPort = 80;
this.protocol = "http:";
this.options = options = { ...options, path: null };
if (options.noDelay === undefined) options.noDelay = true;
this.options = { __proto__: null, ...options };
if (this.options.noDelay === undefined) this.options.noDelay = true;
// Don't confuse net and make it think that we're connecting to a pipe
this.requests = Object.create(null);
this.sockets = Object.create(null);
this.freeSockets = Object.create(null);
this.keepAliveMsecs = options.keepAliveMsecs || 1000;
this.keepAlive = options.keepAlive || false;
this.maxSockets = options.maxSockets || Agent.defaultMaxSockets;
this.maxFreeSockets = options.maxFreeSockets || 256;
this.scheduling = options.scheduling || "lifo";
this.maxTotalSockets = options.maxTotalSockets;
this.options.path = null;
this.requests = { __proto__: null };
this.sockets = { __proto__: null };
this.freeSockets = { __proto__: null };
this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
this.keepAlive = this.options.keepAlive || false;
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
this.maxFreeSockets = this.options.maxFreeSockets || 256;
this.scheduling = this.options.scheduling || "lifo";
this.maxTotalSockets = this.options.maxTotalSockets;
this.totalSocketCount = 0;
this.defaultPort = options.defaultPort || 80;
this.protocol = options.protocol || "http:";
validateOneOf(this.scheduling, "scheduling", ["fifo", "lifo"]);
if (this.maxTotalSockets !== undefined) {
validateNumber(this.maxTotalSockets, "maxTotalSockets", 1);
} else {
this.maxTotalSockets = Infinity;
}
this.on("free", (socket, options) => {
const name = this.getName(options);
$debug("agent.on(free)", name);
// TODO(ronag): socket.destroy(err) might have been called
// before coming here and have an 'error' scheduled. In the
// case of socket.destroy() below this 'error' has no handler
// and could cause unhandled exception.
if (!socket.writable) {
socket.destroy();
return;
}
const requests = this.requests[name];
if (requests?.length) {
const req = requests.shift();
const reqAsyncRes = req[kRequestAsyncResource];
if (reqAsyncRes) {
// Run request within the original async context.
reqAsyncRes.runInAsyncScope(() => {
asyncResetHandle(socket);
setRequestSocket(this, req, socket);
});
req[kRequestAsyncResource] = null;
} else {
setRequestSocket(this, req, socket);
}
if (requests.length === 0) {
delete this.requests[name];
}
return;
}
// If there are no pending requests, then put it in
// the freeSockets pool, but only if we're allowed to do so.
const req = socket._httpMessage;
if (!req || !req.shouldKeepAlive || !this.keepAlive) {
socket.destroy();
return;
}
const freeSockets = this.freeSockets[name] || [];
const freeLen = freeSockets.length;
let count = freeLen;
if (this.sockets[name]) count += this.sockets[name].length;
if (
this.totalSocketCount > this.maxTotalSockets ||
count > this.maxSockets ||
freeLen >= this.maxFreeSockets ||
!this.keepSocketAlive(socket)
) {
socket.destroy();
return;
}
this.freeSockets[name] = freeSockets;
// socket[async_id_symbol] = -1;
socket._httpMessage = null;
this.removeSocket(socket, options);
socket.once("error", freeSocketErrorListener);
freeSockets.push(socket);
});
// Don't emit keylog events unless there is a listener for them.
this.on("newListener", maybeEnableKeylog);
}
$toClass(Agent, "Agent", EventEmitter);
// Type assertion to help TypeScript understand Agent has static properties
const AgentClass = Agent as unknown as AgentConstructor;
function maybeEnableKeylog(eventName) {
if (eventName === "keylog") {
this.removeListener("newListener", maybeEnableKeylog);
// Future sockets will listen on keylog at creation.
const agent = this;
this[kOnKeylog] = function onkeylog(keylog) {
agent.emit("keylog", keylog, this);
};
// Existing sockets will start listening on keylog now.
const sockets = ObjectValues(this.sockets);
for (let i = 0; i < sockets.length; i++) {
sockets[i].on("keylog", this[kOnKeylog]);
}
}
}
ObjectDefineProperty(AgentClass, "globalAgent", {
get: function () {
return globalAgent;
},
});
Agent.defaultMaxSockets = Infinity;
ObjectDefineProperty(AgentClass, "defaultMaxSockets", {
get: function () {
return Infinity;
},
});
Agent.prototype.createConnection = net.createConnection;
Agent.prototype.createConnection = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createConnection is a no-op, returns fake socket");
return (this[kfakeSocket] ??= new FakeSocket());
};
Agent.prototype.getName = function (options = kEmptyObject) {
// Get the key for a given set of request options
Agent.prototype.getName = function getName(options = kEmptyObject as any) {
let name = options.host || "localhost";
name += ":";
if (options.port) {
name += options.port;
}
if (options.port) name += options.port;
name += ":";
if (options.localAddress) {
name += options.localAddress;
}
if (options.localAddress) name += options.localAddress;
// Pacify parallel/test-http-agent-getname by only appending
// the ':' when options.family is set.
if (options.family === 4 || options.family === 6) {
name += `:${options.family}`;
}
if (options.socketPath) {
name += `:${options.socketPath}`;
}
if (options.family === 4 || options.family === 6) name += `:${options.family}`;
if (options.socketPath) name += `:${options.socketPath}`;
return name;
};
Agent.prototype.addRequest = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.addRequest is a no-op");
Agent.prototype.addRequest = function addRequest(req, options, port /* legacy */, localAddress /* legacy */) {
// Legacy API: addRequest(req, host, port, localAddress)
if (typeof options === "string") {
options = {
__proto__: null,
host: options,
port,
localAddress,
};
}
options = { __proto__: null, ...options, ...this.options };
if (options.socketPath) options.path = options.socketPath;
normalizeServerName(options, req);
const name = this.getName(options);
this.sockets[name] ||= [];
const freeSockets = this.freeSockets[name];
let socket;
if (freeSockets) {
while (freeSockets.length && freeSockets[0].destroyed) {
freeSockets.shift();
}
socket = this.scheduling === "fifo" ? freeSockets.shift() : freeSockets.pop();
if (!freeSockets.length) delete this.freeSockets[name];
}
const freeLen = freeSockets ? freeSockets.length : 0;
const sockLen = freeLen + this.sockets[name].length;
if (socket) {
asyncResetHandle(socket);
this.reuseSocket(socket, req);
setRequestSocket(this, req, socket);
this.sockets[name].push(socket);
} else if (sockLen < this.maxSockets && this.totalSocketCount < this.maxTotalSockets) {
$debug("call onSocket", sockLen, freeLen);
// If we are under maxSockets create a new one.
this.createSocket(req, options, (err, socket) => {
if (err) req.onSocket(socket, err);
else setRequestSocket(this, req, socket);
});
} else {
$debug("wait for socket");
// We are over limit so we'll add it to the queue.
this.requests[name] ||= [];
// Used to create sockets for pending requests from different origin
req[kRequestOptions] = options;
// Used to capture the original async context.
// req[kRequestAsyncResource] = new AsyncResource("QueuedRequest");
this.requests[name].push(req);
}
};
Agent.prototype.createSocket = function (req, options, cb) {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.createSocket returns fake socket");
cb(null, (this[kfakeSocket] ??= new FakeSocket()));
Agent.prototype.createSocket = function createSocket(req, options, cb) {
options = { __proto__: null, ...options, ...this.options };
if (options.socketPath) options.path = options.socketPath;
normalizeServerName(options, req);
const name = this.getName(options);
options._agentKey = name;
options.encoding = null;
const oncreate = once((err, s) => {
if (err) return cb(err);
this.sockets[name] ||= [];
this.sockets[name].push(s);
this.totalSocketCount++;
$debug("sockets", name, this.sockets[name].length, this.totalSocketCount);
installListeners(this, s, options);
cb(null, s);
});
// When keepAlive is true, pass the related options to createConnection
if (this.keepAlive) {
options.keepAlive = this.keepAlive;
options.keepAliveInitialDelay = this.keepAliveMsecs;
}
// console.log(Agent.prototype.createConnection, this.createConnection, this instanceof Agent); //??
const newSocket = this.createConnection(options, oncreate);
if (newSocket) oncreate(null, newSocket);
};
Agent.prototype.removeSocket = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.removeSocket is a no-op");
function normalizeServerName(options, req) {
if (!options.servername && options.servername !== "") options.servername = calculateServerName(options, req);
}
function calculateServerName(options, req) {
let servername = options.host;
const hostHeader = req.getHeader("host");
if (hostHeader) {
validateString(hostHeader, "options.headers.host");
// abc => abc
// abc:123 => abc
// [::1] => ::1
// [::1]:123 => ::1
if (hostHeader[0] === "[") {
const index = hostHeader.indexOf("]");
if (index === -1) {
// Leading '[', but no ']'. Need to do something...
servername = hostHeader;
} else {
servername = hostHeader.substring(1, index);
}
} else {
servername = hostHeader.split(":", 1)[0];
}
}
// Don't implicitly set invalid (IP) servernames.
if (net.isIP(servername)) servername = "";
return servername;
}
function installListeners(agent, s, options) {
function onFree() {
$debug("CLIENT socket onFree");
agent.emit("free", s, options);
}
s.on("free", onFree);
function onClose(err) {
$debug("CLIENT socket onClose");
// This is the only place where sockets get removed from the Agent.
// If you want to remove a socket from the pool, just close it.
// All socket errors end in a close event anyway.
agent.totalSocketCount--;
agent.removeSocket(s, options);
}
s.on("close", onClose);
function onTimeout() {
$debug("CLIENT socket onTimeout");
// Destroy if in free list.
// TODO(ronag): Always destroy, even if not in free list.
const sockets = agent.freeSockets;
if (ObjectKeys(sockets).some(name => sockets[name].includes(s))) {
return s.destroy();
}
}
s.on("timeout", onTimeout);
function onRemove() {
// We need this function for cases like HTTP 'upgrade'
// (defined by WebSockets) where we need to remove a socket from the
// pool because it'll be locked up indefinitely
$debug("CLIENT socket onRemove");
agent.totalSocketCount--;
agent.removeSocket(s, options);
s.removeListener("close", onClose);
s.removeListener("free", onFree);
s.removeListener("timeout", onTimeout);
s.removeListener("agentRemove", onRemove);
}
s.on("agentRemove", onRemove);
if (agent[kOnKeylog]) {
s.on("keylog", agent[kOnKeylog]);
}
}
Agent.prototype.removeSocket = function removeSocket(s, options) {
const name = this.getName(options);
$debug("removeSocket", name, "writable:", s.writable);
const sets = [this.sockets];
// If the socket was destroyed, remove it from the free buffers too.
if (!s.writable) sets.push(this.freeSockets);
for (let sk = 0; sk < sets.length; sk++) {
const sockets = sets[sk];
if (sockets[name]) {
const index = sockets[name].indexOf(s);
if (index !== -1) {
sockets[name].splice(index, 1);
if (sockets[name].length === 0) delete sockets[name];
}
}
}
let req;
if (this.requests[name]?.length) {
$debug("removeSocket, have a request, make a socket");
req = this.requests[name][0];
} else {
// TODO(rickyes): this logic will not be FIFO across origins.
// There might be older requests in a different origin, but
// if the origin which releases the socket has pending requests
// that will be prioritized.
const keys = ObjectKeys(this.requests);
for (let i = 0; i < keys.length; i++) {
const prop = keys[i];
// Check whether this specific origin is already at maxSockets
if (this.sockets[prop]?.length) break;
$debug("removeSocket, have a request with different origin," + " make a socket");
req = this.requests[prop][0];
options = req[kRequestOptions];
break;
}
}
if (req && options) {
req[kRequestOptions] = undefined;
// If we have pending requests and a socket gets closed make a new one
this.createSocket(req, options, (err, socket) => {
if (err) req.onSocket(socket, err);
else socket.emit("free");
});
}
};
Agent.prototype.keepSocketAlive = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.keepSocketAlive is a no-op");
return true;
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref();
let agentTimeout = this.options.timeout || 0;
let canKeepSocketAlive = true;
if (socket._httpMessage?.res) {
const keepAliveHint = socket._httpMessage.res.headers["keep-alive"];
if (keepAliveHint) {
const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
if (hint) {
// Let the timer expire before the announced timeout to reduce
// the likelihood of ECONNRESET errors
let serverHintTimeout = NumberParseInt(hint) * 1000 - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER;
serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
if (serverHintTimeout === 0) {
// Cannot safely reuse the socket because the server timeout is
// too short
canKeepSocketAlive = false;
} else if (serverHintTimeout < agentTimeout) {
agentTimeout = serverHintTimeout;
}
}
}
}
if (socket.timeout !== agentTimeout) {
socket.setTimeout(agentTimeout);
}
return canKeepSocketAlive;
};
Agent.prototype.reuseSocket = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.reuseSocket is a no-op");
Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
$debug("have free socket");
socket.removeListener("error", freeSocketErrorListener);
req.reusedSocket = true;
socket.ref();
};
Agent.prototype.destroy = function () {
$debug(`${NODE_HTTP_WARNING}\n`, "WARN: Agent.destroy is a no-op");
Agent.prototype.destroy = function destroy() {
const sets = [this.freeSockets, this.sockets];
for (let s = 0; s < sets.length; s++) {
const set = sets[s];
const keys = ObjectKeys(set);
for (let v = 0; v < keys.length; v++) {
const setName = set[keys[v]];
for (let n = 0; n < setName.length; n++) {
setName[n].destroy();
}
}
}
};
var globalAgent = new Agent();
function setRequestSocket(agent, req, socket) {
req.onSocket(socket);
const agentTimeout = agent.options.timeout || 0;
if (req.timeout === undefined || req.timeout === agentTimeout) {
return;
}
socket.setTimeout(req.timeout);
}
const http_agent_exports = {
Agent: AgentClass,
globalAgent,
NODE_HTTP_WARNING,
function asyncResetHandle(socket) {
// Guard against an uninitialized or user supplied Socket.
const handle = socket._handle;
if (handle && typeof handle.asyncReset === "function") {
// Assign the handle a new asyncId and run any destroy()/init() hooks.
handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle));
// socket[async_id_symbol] = handle.getAsyncId();
}
}
export default {
Agent,
globalAgent: new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000 }),
};
export default http_agent_exports;

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
const { checkIsHttpToken } = require("internal/validators");
const FreeList = require("internal/freelist");
const { methods, allMethods, HTTPParser } = process.binding("http_parser");
const incoming = require("node:_http_incoming");
const { IncomingMessage, readStart, readStop } = incoming;
const { IncomingMessage, readStart, readStop } = require("node:_http_incoming");
const RegExpPrototypeExec = RegExp.prototype.exec;
@@ -132,7 +130,7 @@ function parserOnBody(b) {
// Pretend this was the result of a stream._read call.
if (!stream._dumped) {
const ret = stream.push(b);
if (!ret) readStop(this.socket);
// if (!ret) readStop(this.socket);
}
}
@@ -226,7 +224,6 @@ function prepareError(err, parser, rawPacket) {
}
let warnedLenient = false;
function isLenient() {
if (insecureHTTPParser && !warnedLenient) {
warnedLenient = true;

View File

@@ -1,122 +1,65 @@
const { Readable } = require("internal/streams/readable");
// Hardcoded module "node:_http_incoming"
const { Readable, finished } = require("node:stream");
const {
kHandle,
kEmptyObject,
STATUS_CODES,
abortedSymbol,
eofInProgress,
kHandle,
noBodySymbol,
typeSymbol,
NodeHTTPIncomingRequestType,
noBodySymbol,
fakeSocketSymbol,
webRequestOrResponse,
setRequestTimeout,
emitEOFIncomingMessage,
NodeHTTPBodyReadState,
bodyStreamSymbol,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
isAbortError,
emitErrorNextTickIfErrorListenerNT,
kEmptyObject,
getIsNextIncomingMessageHTTPS,
setIsNextIncomingMessageHTTPS,
NodeHTTPBodyReadState,
emitEOFIncomingMessage,
bodyStreamSymbol,
statusMessageSymbol,
statusCodeSymbol,
webRequestOrResponse,
statusMessageSymbol,
NodeHTTPResponseAbortEvent,
STATUS_CODES,
assignHeadersFast,
setRequestTimeout,
headersTuple,
webRequestOrResponseHasBodyValue,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
kBunServer,
kAbortController,
} = require("internal/http");
const { FakeSocket } = require("internal/http/FakeSocket");
var defaultIncomingOpts = { type: "request" };
const ObjectDefineProperty = Object.defineProperty;
const kHeaders = Symbol("kHeaders");
const kHeadersDistinct = Symbol("kHeadersDistinct");
const kHeadersCount = Symbol("kHeadersCount");
const kTrailers = Symbol("kTrailers");
const kTrailersDistinct = Symbol("kTrailersDistinct");
const kTrailersCount = Symbol("kTrailersCount");
const nop = () => {};
function assignHeadersSlow(object, req) {
const headers = req.headers;
var outHeaders = Object.create(null);
const rawHeaders: string[] = [];
var i = 0;
for (let key in headers) {
var originalKey = key;
var value = headers[originalKey];
key = key.toLowerCase();
if (key !== "set-cookie") {
value = String(value);
$putByValDirect(rawHeaders, i++, originalKey);
$putByValDirect(rawHeaders, i++, value);
outHeaders[key] = value;
} else {
if ($isJSArray(value)) {
outHeaders[key] = value.slice();
for (let entry of value) {
$putByValDirect(rawHeaders, i++, originalKey);
$putByValDirect(rawHeaders, i++, entry);
}
} else {
value = String(value);
outHeaders[key] = [value];
$putByValDirect(rawHeaders, i++, originalKey);
$putByValDirect(rawHeaders, i++, value);
}
}
}
object.headers = outHeaders;
object.rawHeaders = rawHeaders;
function readStart(socket) {
if (socket && !socket._paused && socket.readable) socket.resume();
}
function assignHeaders(object, req) {
// This fast path is an 8% speedup for a "hello world" node:http server, and a 7% speedup for a "hello world" express server
if (assignHeadersFast(req, object, headersTuple)) {
const headers = $getInternalField(headersTuple, 0);
const rawHeaders = $getInternalField(headersTuple, 1);
$putInternalField(headersTuple, 0, undefined);
$putInternalField(headersTuple, 1, undefined);
object.headers = headers;
object.rawHeaders = rawHeaders;
return true;
} else {
assignHeadersSlow(object, req);
return false;
}
function readStop(socket) {
if (socket) socket.pause();
}
function onIncomingMessagePauseNodeHTTPResponse(this: IncomingMessage) {
const handle = this[kHandle];
if (handle && !this.destroyed) {
handle.pause();
}
}
/* Abstract base class for ServerRequest and ClientResponse. */
function IncomingMessage(socket) {
this[Symbol.for("meghan.kind")] = "_http_incoming";
function onIncomingMessageResumeNodeHTTPResponse(this: IncomingMessage) {
const handle = this[kHandle];
if (handle && !this.destroyed) {
const resumed = handle.resume();
if (resumed && resumed !== true) {
const bodyReadState = handle.hasBody;
if ((bodyReadState & NodeHTTPBodyReadState.done) !== 0) {
emitEOFIncomingMessage(this);
}
this.push(resumed);
}
}
}
function IncomingMessage(req, options = defaultIncomingOpts) {
this[abortedSymbol] = false;
this[eofInProgress] = false;
this._consuming = false;
this._dumped = false;
this.complete = false;
this._closed = false;
// (url, method, headers, rawHeaders, handle, hasBody)
if (req === kHandle) {
// BUN: server
// (symbol, url, method, headers, rawHeaders, handle, hasBody)
if (socket === kHandle) {
this[kBunServer] = true;
this[abortedSymbol] = false;
this[eofInProgress] = false;
this._consuming = false;
this._dumped = false;
this.complete = false;
this._closed = false;
this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse;
this.url = arguments[1];
this.method = arguments[2];
@@ -127,99 +70,186 @@ function IncomingMessage(req, options = defaultIncomingOpts) {
this[fakeSocketSymbol] = arguments[7];
Readable.$call(this);
// If there's a body, pay attention to pause/resume events
if (arguments[6]) {
this.on("pause", onIncomingMessagePauseNodeHTTPResponse);
this.on("resume", onIncomingMessageResumeNodeHTTPResponse);
}
} else {
this[noBodySymbol] = false;
Readable.$call(this);
var { [typeSymbol]: type } = options || {};
this[webRequestOrResponse] = req;
this[typeSymbol] = type;
this[bodyStreamSymbol] = undefined;
const statusText = (req as Response)?.statusText;
this[statusMessageSymbol] = statusText !== "" ? statusText || null : "";
this[statusCodeSymbol] = (req as Response)?.status || 200;
this._readableState.readingMore = true;
if (type === NodeHTTPIncomingRequestType.FetchRequest || type === NodeHTTPIncomingRequestType.FetchResponse) {
if (!assignHeaders(this, req)) {
this[fakeSocketSymbol] = req;
}
} else {
// Node defaults url and method to null.
this.url = "";
this.method = null;
this.rawHeaders = [];
}
this[noBodySymbol] =
type === NodeHTTPIncomingRequestType.FetchRequest // TODO: Add logic for checking for body on response
? requestHasNoBody(this.method, this)
: false;
if (getIsNextIncomingMessageHTTPS()) {
this.socket.encrypted = true;
setIsNextIncomingMessageHTTPS(false);
}
}
this._readableState.readingMore = true;
}
function onDataIncomingMessage(
this: import("node:http").IncomingMessage,
chunk,
isLast,
aborted: NodeHTTPResponseAbortEvent,
) {
if (aborted === NodeHTTPResponseAbortEvent.abort) {
this.destroy();
this.httpVersion = "1.1";
this.httpVersionMajor = 1;
this.httpVersionMinor = 1;
return;
}
if (chunk && !this._dumped) this.push(chunk);
this[kBunServer] = false;
let streamOptions;
if (isLast) {
emitEOFIncomingMessage(this);
if (socket) {
streamOptions = {
highWaterMark: socket.readableHighWaterMark,
};
}
Readable.$call(this, streamOptions);
this._readableState.readingMore = true;
this.socket = socket;
this.httpVersionMajor = null;
this.httpVersionMinor = null;
this.httpVersion = null;
this.complete = false;
this[kHeaders] = null;
this[kHeadersCount] = 0;
this.rawHeaders = [];
this[kTrailers] = null;
this[kTrailersCount] = 0;
this.rawTrailers = [];
this.joinDuplicateHeaders = false;
this.aborted = false;
this.upgrade = null;
// request (server) only
this.url = "";
this.method = null;
// response (client) only
this.statusCode = null;
this.statusMessage = null;
this.client = socket;
this._consuming = false;
// Flag for when we decide that this message cannot possibly be
// read by the user, so there's no point continuing to handle it.
this._dumped = false;
}
$toClass(IncomingMessage, "IncomingMessage", Readable);
const IncomingMessagePrototype = {
constructor: IncomingMessage,
__proto__: Readable.prototype,
httpVersion: "1.1",
_construct(callback) {
// TODO: streaming
const type = this[typeSymbol];
ObjectDefineProperty(IncomingMessage.prototype, "connection", {
__proto__: null,
get: function () {
if (this[kBunServer]) {
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
}
return this.socket;
},
set: function (val) {
if (this[kBunServer]) {
this[fakeSocketSymbol] = val;
return;
}
this.socket = val;
},
});
if (type === NodeHTTPIncomingRequestType.FetchResponse) {
if (!webRequestOrResponseHasBodyValue(this[webRequestOrResponse])) {
this.complete = true;
this.push(null);
ObjectDefineProperty(IncomingMessage.prototype, "headers", {
__proto__: null,
get: function () {
if (!this[kHeaders]) {
this[kHeaders] = {};
const src = this.rawHeaders;
const dst = this[kHeaders];
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
}
}
return this[kHeaders];
},
set: function (val) {
this[kHeaders] = val;
},
});
callback();
},
// Call this instead of resume() if we want to just
// dump all the data to /dev/null
_dump() {
if (!this._dumped) {
this._dumped = true;
// If there is buffered data, it may trigger 'data' events.
// Remove 'data' event listeners explicitly.
this.removeAllListeners("data");
const handle = this[kHandle];
if (handle) {
handle.ondata = undefined;
ObjectDefineProperty(IncomingMessage.prototype, "headersDistinct", {
__proto__: null,
get: function () {
if (!this[kHeadersDistinct]) {
this[kHeadersDistinct] = {};
const src = this.rawHeaders;
const dst = this[kHeadersDistinct];
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
}
this.resume();
}
return this[kHeadersDistinct];
},
_read(_size) {
set: function (val) {
this[kHeadersDistinct] = val;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "trailers", {
__proto__: null,
get: function () {
if (this[kBunServer]) {
return kEmptyObject;
}
if (!this[kTrailers]) {
this[kTrailers] = {};
const src = this.rawTrailers;
const dst = this[kTrailers];
for (let n = 0; n < this[kTrailersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
}
}
return this[kTrailers];
},
set: function (val) {
if (this[kBunServer]) {
return;
}
this[kTrailers] = val;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "trailersDistinct", {
__proto__: null,
get: function () {
if (!this[kTrailersDistinct]) {
this[kTrailersDistinct] = {};
const src = this.rawTrailers;
const dst = this[kTrailersDistinct];
for (let n = 0; n < this[kTrailersCount]; n += 2) {
this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst);
}
}
return this[kTrailersDistinct];
},
set: function (val) {
this[kTrailersDistinct] = val;
},
});
IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
if (this[kBunServer]) {
this.take;
const req = this[kHandle] || this[webRequestOrResponse];
if (req) {
setRequestTimeout(req, Math.ceil(msecs / 1000));
typeof callback === "function" && this.once("timeout", callback);
}
return this;
}
if (callback) this.on("timeout", callback);
this.socket.setTimeout(msecs);
return this;
};
IncomingMessage.prototype._read = function _read(n) {
if (this[kBunServer]) {
if (!this._consuming) {
this._readableState.readingMore = false;
this._consuming = true;
@@ -288,13 +318,25 @@ const IncomingMessagePrototype = {
this[bodyStreamSymbol] = reader;
consumeStream(this, reader);
}
return;
},
_finish() {
this.emit("prefinish");
},
_destroy: function IncomingMessage_destroy(err, cb) {
}
if (!this._consuming) {
this._readableState.readingMore = false;
this._consuming = true;
}
// We actually do almost nothing here, because the parserOnBody
// function fills up our internal buffer directly. However, we
// do need to unpause the underlying socket so that it flows.
if (this.socket.readable) readStart(this.socket);
};
// It's possible that the socket will be destroyed, and removed from
// any messages, before ever calling this. In that case, just skip
// it, since something else is destroying this connection anyway.
IncomingMessage.prototype._destroy = function _destroy(err, cb) {
if (this[kBunServer]) {
const shouldEmitAborted = !this.readableEnded || !this.complete;
if (shouldEmitAborted) {
@@ -341,95 +383,369 @@ const IncomingMessagePrototype = {
if ($isCallable(cb)) {
emitErrorNextTickIfErrorListenerNT(this, err, cb);
}
},
get aborted() {
return this[abortedSymbol];
},
set aborted(value) {
this[abortedSymbol] = value;
},
get connection() {
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
},
get statusCode() {
return this[statusCodeSymbol];
},
set statusCode(value) {
if (!(value in STATUS_CODES)) return;
this[statusCodeSymbol] = value;
},
get statusMessage() {
return this[statusMessageSymbol];
},
set statusMessage(value) {
this[statusMessageSymbol] = value;
},
get httpVersionMajor() {
const version = this.httpVersion;
if (version.startsWith("1.")) {
return 1;
}
return 0;
},
set httpVersionMajor(value) {
// noop
},
get httpVersionMinor() {
const version = this.httpVersion;
if (version.endsWith(".1")) {
return 1;
}
return 0;
},
set httpVersionMinor(value) {
// noop
},
get rawTrailers() {
return [];
},
set rawTrailers(value) {
// noop
},
get trailers() {
return kEmptyObject;
},
set trailers(value) {
// noop
},
setTimeout(msecs, callback) {
void this.take;
const req = this[kHandle] || this[webRequestOrResponse];
return;
}
if (req) {
setRequestTimeout(req, Math.ceil(msecs / 1000));
if (typeof callback === "function") this.once("timeout", callback);
if (!this.readableEnded || !this.complete) {
this.aborted = true;
this.emit("aborted");
}
// If aborted and the underlying socket is not already destroyed,
// destroy it.
// We have to check if the socket is already destroyed because finished
// does not call the callback when this method is invoked from `_http_client`
// in `test/parallel/test-http-client-spurious-aborted.js`
if (this.socket && !this.socket.destroyed && this.aborted) {
this.socket.destroy(err);
const cleanup = finished(this.socket, e => {
if (e?.code === "ERR_STREAM_PREMATURE_CLOSE") {
e = null;
}
cleanup();
process.nextTick(onError, this, e || err, cb);
});
} else {
process.nextTick(onError, this, err, cb);
}
};
IncomingMessage.prototype._addHeaderLines = function (headers, n) {
$assert(!this[kBunServer]);
if (headers?.length) {
let dest;
if (this.complete) {
this.rawTrailers = headers;
this[kTrailersCount] = n;
dest = this[kTrailers];
} else {
this.rawHeaders = headers;
this[kHeadersCount] = n;
dest = this[kHeaders];
}
return this;
},
get socket() {
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
},
set socket(value) {
this[fakeSocketSymbol] = value;
},
} satisfies typeof import("node:http").IncomingMessage.prototype;
IncomingMessage.prototype = IncomingMessagePrototype;
$setPrototypeDirect.$call(IncomingMessage, Readable);
function requestHasNoBody(method, req) {
if ("GET" === method || "HEAD" === method || "TRACE" === method || "CONNECT" === method || "OPTIONS" === method)
return true;
const headers = req?.headers;
const contentLength = headers?.["content-length"];
if (!parseInt(contentLength, 10)) return true;
if (dest) {
for (let i = 0; i < n; i += 2) {
this._addHeaderLine(headers[i], headers[i + 1], dest);
}
}
}
};
return false;
// This function is used to help avoid the lowercasing of a field name if it
// matches a 'traditional cased' version of a field name. It then returns the
// lowercased name to both avoid calling toLowerCase() a second time and to
// indicate whether the field was a 'no duplicates' field. If a field is not a
// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception
// to this is the Set-Cookie header which is indicated by a `1` byte flag, since
// it is an 'array' field and thus is treated differently in _addHeaderLines().
// TODO: perhaps http_parser could be returning both raw and lowercased versions
// of known header names to avoid us having to call toLowerCase() for those
// headers.
function matchKnownFields(field, lowercased = false) {
switch (field.length) {
case 3:
if (field === "Age" || field === "age") return "age";
break;
case 4:
if (field === "Host" || field === "host") return "host";
if (field === "From" || field === "from") return "from";
if (field === "ETag" || field === "etag") return "etag";
if (field === "Date" || field === "date") return "\u0000date";
if (field === "Vary" || field === "vary") return "\u0000vary";
break;
case 6:
if (field === "Server" || field === "server") return "server";
if (field === "Cookie" || field === "cookie") return "\u0002cookie";
if (field === "Origin" || field === "origin") return "\u0000origin";
if (field === "Expect" || field === "expect") return "\u0000expect";
if (field === "Accept" || field === "accept") return "\u0000accept";
break;
case 7:
if (field === "Referer" || field === "referer") return "referer";
if (field === "Expires" || field === "expires") return "expires";
if (field === "Upgrade" || field === "upgrade") return "\u0000upgrade";
break;
case 8:
if (field === "Location" || field === "location") return "location";
if (field === "If-Match" || field === "if-match") return "\u0000if-match";
break;
case 10:
if (field === "User-Agent" || field === "user-agent") return "user-agent";
if (field === "Set-Cookie" || field === "set-cookie") return "\u0001";
if (field === "Connection" || field === "connection") return "\u0000connection";
break;
case 11:
if (field === "Retry-After" || field === "retry-after") return "retry-after";
break;
case 12:
if (field === "Content-Type" || field === "content-type") return "content-type";
if (field === "Max-Forwards" || field === "max-forwards") return "max-forwards";
break;
case 13:
if (field === "Authorization" || field === "authorization") return "authorization";
if (field === "Last-Modified" || field === "last-modified") return "last-modified";
if (field === "Cache-Control" || field === "cache-control") return "\u0000cache-control";
if (field === "If-None-Match" || field === "if-none-match") return "\u0000if-none-match";
break;
case 14:
if (field === "Content-Length" || field === "content-length") return "content-length";
break;
case 15:
if (field === "Accept-Encoding" || field === "accept-encoding") return "\u0000accept-encoding";
if (field === "Accept-Language" || field === "accept-language") return "\u0000accept-language";
if (field === "X-Forwarded-For" || field === "x-forwarded-for") return "\u0000x-forwarded-for";
break;
case 16:
if (field === "Content-Encoding" || field === "content-encoding") return "\u0000content-encoding";
if (field === "X-Forwarded-Host" || field === "x-forwarded-host") return "\u0000x-forwarded-host";
break;
case 17:
if (field === "If-Modified-Since" || field === "if-modified-since") return "if-modified-since";
if (field === "Transfer-Encoding" || field === "transfer-encoding") return "\u0000transfer-encoding";
if (field === "X-Forwarded-Proto" || field === "x-forwarded-proto") return "\u0000x-forwarded-proto";
break;
case 19:
if (field === "Proxy-Authorization" || field === "proxy-authorization") return "proxy-authorization";
if (field === "If-Unmodified-Since" || field === "if-unmodified-since") return "if-unmodified-since";
break;
}
if (lowercased) {
return "\u0000" + field;
}
return matchKnownFields(field.toLowerCase(), true);
}
// Add the given (field, value) pair to the message
//
// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
// same header with a ', ' if the header in question supports specification of
// multiple values this way. The one exception to this is the Cookie header,
// which has multiple values joined with a '; ' instead. If a header's values
// cannot be joined in either of these ways, we declare the first instance the
// winner and drop the second. Extended header fields (those beginning with
// 'x-') are always joined.
IncomingMessage.prototype._addHeaderLine = function (field, value, dest) {
$assert(!this[kBunServer]);
field = matchKnownFields(field);
const flag = field.charCodeAt(0);
if (flag === 0 || flag === 2) {
field = field.slice(1);
// Make a delimited list
if (typeof dest[field] === "string") {
dest[field] += (flag === 0 ? ", " : "; ") + value;
} else {
dest[field] = value;
}
} else if (flag === 1) {
// Array header -- only Set-Cookie at the moment
if (dest["set-cookie"] !== undefined) {
dest["set-cookie"].push(value);
} else {
dest["set-cookie"] = [value];
}
} else if (this.joinDuplicateHeaders) {
// RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2
// https://github.com/nodejs/node/issues/45699
// allow authorization multiple fields
// Make a delimited list
if (dest[field] === undefined) {
dest[field] = value;
} else {
dest[field] += ", " + value;
}
} else if (dest[field] === undefined) {
// Drop duplicates
dest[field] = value;
}
};
IncomingMessage.prototype._addHeaderLineDistinct = function (field, value, dest) {
$assert(!this[kBunServer]);
field = field.toLowerCase();
if (!dest[field]) {
dest[field] = [value];
} else {
dest[field].push(value);
}
};
// Call this instead of resume() if we want to just
// dump all the data to /dev/null
IncomingMessage.prototype._dump = function _dump() {
if (this[kBunServer]) {
if (!this._dumped) {
this._dumped = true;
// If there is buffered data, it may trigger 'data' events.
// Remove 'data' event listeners explicitly.
this.removeAllListeners("data");
const handle = this[kHandle];
if (handle) {
handle.ondata = undefined;
}
this.resume();
}
return;
}
if (!this._dumped) {
this._dumped = true;
// If there is buffered data, it may trigger 'data' events.
// Remove 'data' event listeners explicitly.
this.removeAllListeners("data");
this.resume();
}
};
function onError(self, error, cb) {
// This is to keep backward compatible behavior.
// An error is emitted only if there are listeners attached to the event.
if (self.listenerCount("error") === 0) {
cb();
} else {
cb(error);
}
}
// BUN: server extras
ObjectDefineProperty(IncomingMessage.prototype, "socket", {
get() {
if (this[kBunServer]) {
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
}
return this.__socket;
},
set(value) {
if (this[kBunServer]) {
this[fakeSocketSymbol] = value;
return;
}
this.__socket = value;
return;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "rawTrailers", {
get() {
if (this[kBunServer]) {
return [];
}
return this.__rawTrailers;
},
set(value) {
if (this[kBunServer]) {
return;
}
this.__rawTrailers = value;
return;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "aborted", {
get() {
if (this[kBunServer]) {
return this[abortedSymbol];
}
return this.__aborted;
},
set(value) {
if (this[kBunServer]) {
this[abortedSymbol] = value;
return;
}
this.__aborted = value;
return;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "statusCode", {
get() {
if (this[kBunServer]) {
return this[statusCodeSymbol];
}
return this.__statusCode;
},
set(value) {
if (this[kBunServer]) {
if (!(value in STATUS_CODES)) return;
this[statusCodeSymbol] = value;
return;
}
this.__statusCode = value;
return;
},
});
ObjectDefineProperty(IncomingMessage.prototype, "statusMessage", {
get() {
if (this[kBunServer]) {
return this[statusMessageSymbol];
}
return this.__statusMessage;
},
set(value) {
if (this[kBunServer]) {
this[statusMessageSymbol] = value;
return;
}
this.__statusMessage = value;
return;
},
});
IncomingMessage.prototype._construct = function (callback) {
if (this[kBunServer]) {
// TODO: streaming
const type = this[typeSymbol];
if (type === NodeHTTPIncomingRequestType.FetchResponse) {
if (!webRequestOrResponseHasBodyValue(this[webRequestOrResponse])) {
this.complete = true;
this.push(null);
}
}
callback();
return;
}
callback();
};
function onIncomingMessagePauseNodeHTTPResponse(this: import("node:http").IncomingMessage) {
const handle = this[kHandle];
if (handle && !this.destroyed) {
handle.pause();
}
}
function onIncomingMessageResumeNodeHTTPResponse(this: import("node:http").IncomingMessage) {
const handle = this[kHandle];
if (handle && !this.destroyed) {
const resumed = handle.resume();
if (resumed && resumed !== true) {
const bodyReadState = handle.hasBody;
if ((bodyReadState & NodeHTTPBodyReadState.done) !== 0) {
emitEOFIncomingMessage(this);
}
this.push(resumed);
}
}
}
function onDataIncomingMessage(this: import("node:http").IncomingMessage, chunk, isLast, aborted) {
if (aborted === NodeHTTPResponseAbortEvent.abort) {
this.destroy();
return;
}
if (chunk && !this._dumped) this.push(chunk);
if (isLast) emitEOFIncomingMessage(this);
}
async function consumeStream(self, reader: ReadableStreamDefaultReader) {
var done = false,
value,
aborted = false;
var done = false;
var value;
var aborted = false;
try {
while (true) {
const result = reader.readMany();
@@ -438,19 +754,9 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) {
} else {
({ done, value } = result);
}
if (self.destroyed || (aborted = self[abortedSymbol])) {
break;
}
if (!self._dumped) {
for (var v of value) {
self.push(v);
}
}
if (self.destroyed || (aborted = self[abortedSymbol]) || done) {
break;
}
if (self.destroyed || (aborted = self[abortedSymbol])) break;
if (!self._dumped) for (var v of value) self.push(v);
if (self.destroyed || (aborted = self[abortedSymbol]) || done) break;
}
} catch (err) {
if (aborted || self.destroyed) return;
@@ -458,22 +764,13 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) {
} finally {
reader?.cancel?.().catch?.(nop);
}
if (!self.complete) {
emitEOFIncomingMessage(self);
}
}
function readStart(socket) {
if (socket && !socket._paused && socket.readable) {
socket.resume();
}
}
function readStop(socket) {
if (socket) {
socket.pause();
}
}
export { IncomingMessage, readStart, readStop };
export default {
IncomingMessage,
readStart,
readStop,
};

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ const {
setIsNextIncomingMessageHTTPS,
callCloseCallback,
emitCloseNT,
ConnResetException,
NodeHTTPResponseAbortEvent,
STATUS_CODES,
isTlsSymbol,
@@ -43,25 +42,27 @@ const {
setServerIdleTimeout,
setServerCustomOptions,
getMaxHTTPHeaderSize,
kBunServer,
} = require("internal/http");
const NumberIsNaN = Number.isNaN;
const { format } = require("internal/util/inspect");
const { ConnResetException } = require("internal/shared");
const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
const { kIncomingMessage } = require("node:_http_common");
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
const { kIncomingMessage, chunkExpression } = require("node:_http_common");
const getBunServerAllClosedPromise = $newZigFunction("node_http_binding.zig", "getBunServerAllClosedPromise", 1);
const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild", 3);
const kServerResponse = Symbol("ServerResponse");
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
const GlobalPromise = globalThis.Promise;
const kEmptyBuffer = Buffer.alloc(0);
const ObjectKeys = Object.keys;
const MathMin = Math.min;
const NumberIsNaN = Number.isNaN;
let cluster;
@@ -85,7 +86,7 @@ function setCloseCallback(self, callback) {
function assignSocketInternal(self, socket) {
if (socket._httpMessage) {
throw $ERR_HTTP_SOCKET_ASSIGNED("Socket already assigned");
throw $ERR_HTTP_SOCKET_ASSIGNED();
}
socket._httpMessage = self;
setCloseCallback(socket, onServerResponseClose);
@@ -193,6 +194,7 @@ function emitListeningNextTick(self, hostname, port) {
}
}
type Server = import("node:http").Server;
function Server(options, callback): void {
if (!(this instanceof Server)) return new Server(options, callback);
EventEmitter.$call(this);
@@ -548,6 +550,7 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
socket[kEnableStreaming](false);
const http_res = new ResponseClass(http_req, {
[kBunServer]: true,
[kHandle]: handle,
[kRejectNonStandardBodyWrites]: server.rejectNonStandardBodyWrites,
});
@@ -754,6 +757,7 @@ function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPRespons
}
}
}
// uWS::HttpParserError
enum HttpParserError {
HTTP_PARSER_ERROR_NONE = 0,
@@ -773,10 +777,10 @@ function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, r
let err;
switch (errorCode) {
case HttpParserError.HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH:
err = $HPE_UNEXPECTED_CONTENT_LENGTH("Parse Error");
err = $HPE_UNEXPECTED_CONTENT_LENGTH("Parse Error: Invalid Content-Length");
break;
case HttpParserError.HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING:
err = $HPE_INVALID_TRANSFER_ENCODING("Parse Error");
err = $HPE_INVALID_TRANSFER_ENCODING("Parse Error: Invalid Transfer-Encoding");
break;
case HttpParserError.HTTP_PARSER_ERROR_INVALID_EOF:
err = $HPE_INVALID_EOF_STATE("Parse Error");
@@ -1148,7 +1152,7 @@ function _writeHead(statusCode, reason, obj, response) {
(response.chunkedEncoding !== true || response.hasHeader("content-length")) &&
(response._trailer || response.hasHeader("trailer"))
) {
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
throw $ERR_HTTP_TRAILER_INVALID();
}
// Headers in obj should override previous headers but still
// allow explicit duplicates. To do so, we first remove any
@@ -1184,7 +1188,7 @@ function _writeHead(statusCode, reason, obj, response) {
} else {
response.removeHeader("content-length");
}
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
throw $ERR_HTTP_TRAILER_INVALID();
}
}
@@ -1194,8 +1198,8 @@ function _writeHead(statusCode, reason, obj, response) {
Object.defineProperty(NodeHTTPServerSocket, "name", { value: "Socket" });
function ServerResponse(req, options): void {
if (!(this instanceof ServerResponse)) return new ServerResponse(req, options);
OutgoingMessage.$call(this, options);
this[Symbol.for("meghan.kind")] = "_http_server";
OutgoingMessage.$call(this, { ...options, [kBunServer]: true });
this.useChunkedEncodingByDefault = true;
@@ -1206,21 +1210,17 @@ function ServerResponse(req, options): void {
this.write = ServerResponse_writeDeprecated;
this.end = ServerResponse_finalDeprecated;
}
this.req = req;
this.sendDate = true;
this._sent100 = false;
this[headerStateSymbol] = NodeHTTPHeaderState.none;
this[kPendingCallbacks] = [];
this.finished = false;
// this is matching node's behaviour
// https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/lib/_http_server.js#L192
if (req.method === "HEAD") this._hasBody = false;
if (options) {
const handle = options[kHandle];
if (handle) {
this[kHandle] = handle;
} else {
@@ -1297,6 +1297,7 @@ ServerResponse.prototype.writeProcessing = function (cb) {
ServerResponse.prototype.writeContinue = function (cb) {
this.socket[kHandle]?.response?.writeContinue();
cb?.();
this._sent100 = true;
};
// This end method is actually on the OutgoingMessage prototype in Node.js
@@ -1410,6 +1411,7 @@ Object.defineProperty(ServerResponse.prototype, "writable", {
get() {
return !this._ended || !hasServerResponseFinished(this);
},
set() {},
});
ServerResponse.prototype.write = function (chunk, encoding, callback) {
@@ -1514,13 +1516,10 @@ ServerResponse.prototype.detachSocket = function (socket) {
socket.removeListener("close", onServerResponseClose);
socket._httpMessage = null;
}
this.socket = null;
};
ServerResponse.prototype._implicitHeader = function () {
if (this.headersSent) return;
// @ts-ignore
this.writeHead(this.statusCode);
};
@@ -1573,18 +1572,19 @@ ServerResponse.prototype._send = function (data, encoding, callback, _byteLength
ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) {
if (this.headersSent) {
throw $ERR_HTTP_HEADERS_SENT("writeHead");
throw $ERR_HTTP_HEADERS_SENT("write");
}
_writeHead(statusCode, statusMessage, headers, this);
this[headerStateSymbol] = NodeHTTPHeaderState.assigned;
this._header = " ";
return this;
};
ServerResponse.prototype.assignSocket = function (socket) {
if (socket._httpMessage) {
throw $ERR_HTTP_SOCKET_ASSIGNED("Socket already assigned");
throw $ERR_HTTP_SOCKET_ASSIGNED();
}
socket._httpMessage = this;
socket.once("close", onServerResponseClose);

View File

@@ -884,7 +884,7 @@ class Http2ServerResponse extends Stream {
stream.additionalHeaders({
...headers,
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
"Link": linkHeaderValue,
Link: linkHeaderValue,
});
return true;
}

View File

@@ -1,26 +1,175 @@
// Hardcoded module "node:https"
const http = require("node:http");
const { urlToHttpOptions } = require("internal/url");
const tls = require("node:tls");
const { urlToHttpOptions, isURL } = require("internal/url");
const { kEmptyObject } = require("internal/shared");
const JSONStringify = JSON.stringify;
const ArrayPrototypeShift = Array.prototype.shift;
const ObjectAssign = Object.assign;
const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeIndexOf = Array.prototype.indexOf;
const ArrayPrototypeSplice = Array.prototype.splice;
const ArrayPrototypeUnshift = Array.prototype.unshift;
const ObjectAssign = Object.assign;
const ReflectConstruct = Reflect.construct;
function createConnection(port, host, options) {
if (port !== null && typeof port === "object") {
options = port;
} else if (host !== null && typeof host === "object") {
options = { ...host };
} else if (options === null || typeof options !== "object") {
options = {};
} else {
options = { ...options };
}
if (typeof port === "number") {
options.port = port;
}
if (typeof host === "string") {
options.host = host;
}
$debug("createConnection", options);
if (options._agentKey) {
const session = this._getSession(options._agentKey);
if (session) {
$debug("reuse session for %j", options._agentKey);
options = {
session,
...options,
};
}
}
const socket = tls.connect(options);
if (options._agentKey) {
socket.on("session", session => {
this._cacheSession(options._agentKey, session);
});
socket.once("close", err => {
if (err) this._evictSession(options._agentKey);
});
}
return socket;
}
function Agent(options): void {
if (!(this instanceof Agent)) return new Agent(options);
http.Agent.$call(this, options);
this.defaultPort = 443;
this.protocol = "https:";
this.maxCachedSessions = this.options.maxCachedSessions;
if (this.maxCachedSessions === undefined) this.maxCachedSessions = 100;
this._sessionCache = {
map: {},
list: [],
};
}
$toClass(Agent, "Agent", http.Agent);
Agent.prototype.createConnection = createConnection;
Agent.prototype.getName = function (options = kEmptyObject as any) {
let name = http.Agent.prototype.getName.$call(this, options);
name += ":";
if (options.ca) name += options.ca;
name += ":";
if (options.cert) name += options.cert;
name += ":";
if (options.clientCertEngine) name += options.clientCertEngine;
name += ":";
if (options.ciphers) name += options.ciphers;
name += ":";
if (options.key) name += options.key;
name += ":";
if (options.pfx) name += options.pfx;
name += ":";
if (options.rejectUnauthorized !== undefined) name += options.rejectUnauthorized;
name += ":";
if (options.servername && options.servername !== options.host) name += options.servername;
name += ":";
if (options.minVersion) name += options.minVersion;
name += ":";
if (options.maxVersion) name += options.maxVersion;
name += ":";
if (options.secureProtocol) name += options.secureProtocol;
name += ":";
if (options.crl) name += options.crl;
name += ":";
if (options.honorCipherOrder !== undefined) name += options.honorCipherOrder;
name += ":";
if (options.ecdhCurve) name += options.ecdhCurve;
name += ":";
if (options.dhparam) name += options.dhparam;
name += ":";
if (options.secureOptions !== undefined) name += options.secureOptions;
name += ":";
if (options.sessionIdContext) name += options.sessionIdContext;
name += ":";
if (options.sigalgs) name += JSONStringify(options.sigalgs);
name += ":";
if (options.privateKeyIdentifier) name += options.privateKeyIdentifier;
name += ":";
if (options.privateKeyEngine) name += options.privateKeyEngine;
return name;
};
Agent.prototype._getSession = function _getSession(key) {
return this._sessionCache.map[key];
};
Agent.prototype._cacheSession = function _cacheSession(key, session) {
if (this.maxCachedSessions === 0) return;
if (this._sessionCache.map[key]) {
this._sessionCache.map[key] = session;
return;
}
if (this._sessionCache.list.length >= this.maxCachedSessions) {
const oldKey = ArrayPrototypeShift.$call(this._sessionCache.list);
$debug("evicting %j", oldKey);
delete this._sessionCache.map[oldKey];
}
ArrayPrototypePush.$call(this._sessionCache.list, key);
this._sessionCache.map[key] = session;
};
Agent.prototype._evictSession = function _evictSession(key) {
const index = ArrayPrototypeIndexOf.$call(this._sessionCache.list, key);
if (index === -1) return;
ArrayPrototypeSplice.$call(this._sessionCache.list, index, 1);
delete this._sessionCache.map[key];
};
const globalAgent = new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000 });
function request(...args) {
let options = {};
let options: any = {};
if (typeof args[0] === "string") {
const urlStr = ArrayPrototypeShift.$call(args);
options = urlToHttpOptions(new URL(urlStr));
} else if (args[0] instanceof URL) {
} else if (isURL(args[0])) {
options = urlToHttpOptions(ArrayPrototypeShift.$call(args));
}
if (args[0] && typeof args[0] !== "function") {
ObjectAssign.$call(null, options, ArrayPrototypeShift.$call(args));
ObjectAssign(options, ArrayPrototypeShift.$call(args));
}
options._defaultAgent = https.globalAgent;
options._defaultAgent = globalAgent;
ArrayPrototypeUnshift.$call(args, options);
return new http.ClientRequest(...args);
@@ -32,24 +181,11 @@ function get(input, options, cb) {
return req;
}
function Agent(options) {
if (!(this instanceof Agent)) return new Agent(options);
http.Agent.$apply(this, [options]);
this.defaultPort = 443;
this.protocol = "https:";
this.maxCachedSessions = this.options.maxCachedSessions;
if (this.maxCachedSessions === undefined) this.maxCachedSessions = 100;
}
$toClass(Agent, "Agent", http.Agent);
Agent.prototype.createConnection = http.createConnection;
var https = {
export default {
Agent,
globalAgent: new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000 }),
globalAgent,
Server: http.Server,
createServer: http.createServer,
get,
request,
};
export default https;

View File

@@ -1231,21 +1231,21 @@ Object.defineProperty(Socket.prototype, "pending", {
Socket.prototype.resume = function resume() {
if (!this.connecting) {
this._handle?.resume();
this._handle?.resume?.();
}
return Duplex.prototype.resume.$call(this);
};
Socket.prototype.pause = function pause() {
if (!this.destroyed) {
this._handle?.pause();
this._handle?.pause?.();
}
return Duplex.prototype.pause.$call(this);
};
Socket.prototype.read = function read(size) {
if (!this.connecting) {
this._handle?.resume();
this._handle?.resume?.();
}
return Duplex.prototype.read.$call(this, size);
};
@@ -1255,7 +1255,7 @@ Socket.prototype._read = function _read(size) {
if (this.connecting || !socket) {
this.once("connect", () => this._read(size));
} else {
socket?.resume();
socket?.resume?.();
}
};
@@ -2095,7 +2095,7 @@ function Server(options?, connectionListener?) {
allowHalfOpen = false,
keepAlive = false,
keepAliveInitialDelay = 0,
highWaterMark = getDefaultHighWaterMark(),
highWaterMark = getDefaultHighWaterMark(false),
pauseOnConnect = false,
noDelay = false,
} = options;
@@ -2624,7 +2624,7 @@ function initSocketHandle(self) {
function closeSocketHandle(self, isException, isCleanupPending = false) {
$debug("closeSocketHandle", isException, isCleanupPending, !!self._handle);
if (self._handle) {
self._handle.close();
self._handle.close?.();
setImmediate(() => {
$debug("emit close", isCleanupPending);
self.emit("close", isException);

View File

@@ -172,19 +172,101 @@ function getValidCiphersSet() {
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
// Configurations include in the default cipher list
"HIGH",
"!aNULL",
"!eNULL",
"!EXPORT",
"!DES",
"!RC4",
"!MD5",
"!PSK",
"!SRP",
"!CAMELLIA",
]);
// https://github.com/openssl/openssl/blob/openssl-3.5.2/include/openssl/ssl.h.in#L76
const txt = [
"LOW",
"MEDIUM",
"HIGH",
"FIPS",
"aNULL",
"eNULL",
"NULL",
"kRSA",
"kDHr",
"kDHd",
"kDH",
"kEDH",
"kDHE",
"kECDHr",
"kECDHe",
"kECDH",
"kEECDH",
"kECDHE",
"kPSK",
"kRSAPSK",
"kECDHEPSK",
"kDHEPSK",
"kGOST",
"kGOST18",
"kSRP",
"aRSA",
"aDSS",
"aDH",
"aECDH",
"aECDSA",
"aPSK",
"aGOST94",
"aGOST01",
"aGOST12",
"aGOST",
"aSRP",
"DSS",
"DH",
"DHE",
"EDH",
"ADH",
"RSA",
"ECDH",
"EECDH",
"ECDHE",
"AECDH",
"ECDSA",
"PSK",
"SRP",
"DES",
"3DES",
"RC4",
"RC2",
"IDEA",
"SEED",
"AES128",
"AES256",
"AES",
"AESGCM",
"AESCCM",
"AESCCM8",
"CAMELLIA128",
"CAMELLIA256",
"CAMELLIA",
"CHACHA20",
"GOST89",
"ARIA",
"ARIAGCM",
"ARIA128",
"ARIA256",
"GOST2012-GOST8912-GOST8912",
"CBC",
"MD5",
"SHA1",
"SHA",
"GOST94",
"GOST89MAC",
"GOST12",
"GOST89MAC12",
"SHA256",
"SHA384",
"SSLv3",
"TLSv1",
"TLSv1.1",
"TLSv1.2",
"ALL",
];
for (const c of txt) _VALID_CIPHERS_SET.$add(c);
for (const c of txt) _VALID_CIPHERS_SET.$add("!" + c);
_VALID_CIPHERS_SET.$add("!EXPORT");
_VALID_CIPHERS_SET.$add("!SSLv2");
}
return _VALID_CIPHERS_SET;
}
@@ -199,10 +281,11 @@ function validateCiphers(ciphers: string, name: string = "options") {
// TODO: right now we need this because we dont create the CTX before listening/connecting
// we need to change that in the future and let BoringSSL do the validation
const ciphersSet = getValidCiphersSet();
ciphersSet.add("DEFAULT");
const requested = ciphers.split(":");
for (const r of requested) {
if (r && !ciphersSet.has(r)) {
throw $ERR_SSL_NO_CIPHER_MATCH();
throw $ERR_SSL_NO_CIPHER_MATCH(r);
}
}
}

View File

@@ -4,7 +4,7 @@ declare const self: typeof globalThis;
type WebWorker = InstanceType<typeof globalThis.Worker>;
const EventEmitter = require("node:events");
const { Readable } = require("node:stream");
const Readable = require("internal/streams/readable");
const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared");
const {

7
src/js/private.d.ts vendored
View File

@@ -265,3 +265,10 @@ declare module "node:net" {
_connections: number;
}
}
import "node:http";
declare module "node:http" {
interface IncomingMessage {
_dumped: boolean;
}
}

View File

@@ -1646,7 +1646,7 @@ export class VerdaccioRegistry {
this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", `${this.port}`], {
silent,
// Prefer using a release build of Bun since it's faster
execPath: isCI ? bunExe() : Bun.which("bun") || bunExe(),
execPath: bunExe(),
env: {
...(bunEnv as any),
NODE_NO_WARNINGS: "1",
@@ -1845,7 +1845,7 @@ export const exampleHtml = Buffer.from(
*
* @example Using disposal pattern
* ```ts
* using site = exampleSite();
* await using site = exampleSite();
* await fetch(site.url, { tls: { ca: site.ca } });
* // server automatically stopped when scope exits
* ```
@@ -1868,12 +1868,10 @@ export function exampleSite(protocol: "https" | "http" = "https") {
ca: protocol === "https" ? tls.cert : undefined,
server,
stop() {
server.stop();
return server.stop();
},
[Symbol.dispose]() {
try {
server.stop();
} catch {}
async [Symbol.asyncDispose]() {
await server.stop();
},
};
}

View File

@@ -248,7 +248,7 @@ test("unsupported protocol", async () => {
);
});
test("axios with https-proxy-agent", async () => {
test.todo("axios with https-proxy-agent", async () => {
httpProxyServer.log.length = 0;
const httpsAgent = new HttpsProxyAgent(httpProxyServer.url, {
rejectUnauthorized: false, // this should work with self-signed certs

View File

@@ -273,7 +273,7 @@ const IS_UV_FS_COPYFILE_DISABLED =
it("Bun.file -> Response", async () => {
using tmpbase = tempDir("bun-file-to-response", {});
using server = exampleSite("https");
await using server = exampleSite("https");
// ensure the file doesn't already exist
try {
fs.unlinkSync(tmpbase + "fetch.js.out");

View File

@@ -3,7 +3,7 @@ import https from "node:https";
const { expect } = createTest(import.meta.path);
// TODO: today we use a workaround to continue event, we need to fix it in the future.
using server = exampleSite();
const server = exampleSite();
let receivedContinue = false;
const req = https.request(
server.url,

View File

@@ -1,7 +1,7 @@
import { createTest, exampleSite } from "node-harness";
import https from "node:https";
const { expect } = createTest(import.meta.path);
using server = exampleSite();
const server = exampleSite();
let receivedContinue = false;
const req = https.request(server.url, { ca: server.ca, headers: { "accept-encoding": "identity" } }, res => {
let data = "";

View File

@@ -7,8 +7,8 @@ const { expect } = createTest(import.meta.path);
await using server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "br");
res.writeHead(200);
const inputStream = new stream.Readable();
inputStream.push("Hello World");

View File

@@ -7,8 +7,8 @@ const { expect } = createTest(import.meta.path);
await using server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "br");
res.writeHead(200);
const inputStream = new stream.Readable();
inputStream.push("Hello World");

View File

@@ -7,8 +7,8 @@ const { expect } = createTest(import.meta.path);
await using server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "deflate");
res.writeHead(200);
const inputStream = new stream.Readable();
inputStream.push("Hello World");

View File

@@ -7,8 +7,8 @@ const { expect } = createTest(import.meta.path);
await using server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "gzip");
res.writeHead(200);
const inputStream = new stream.Readable();
inputStream.push("Hello World");

View File

@@ -2,6 +2,7 @@ import { createTest } from "node-harness";
import { once } from "node:events";
import http from "node:http";
const { expect } = createTest(import.meta.path);
process.exit(0); // TODO: BUN does not pass in node
await using server = http.createServer((req, res) => {
if (req.headers["transfer-encoding"] === "chunked") {

View File

@@ -1,7 +1,7 @@
import { createTest, exampleSite } from "node-harness";
import http from "node:http";
const { expect } = createTest(import.meta.path);
using server = exampleSite("https");
await using server = exampleSite("https");
expect(() => http.request(server.url)).toThrow(TypeError);
expect(() => http.request(server.url)).toThrow({
code: "ERR_INVALID_PROTOCOL",

View File

@@ -6,5 +6,5 @@ const agent = new http.Agent();
const { promise, resolve } = Promise.withResolvers();
http.get({ agent, hostname: "google.com" }, resolve);
const response = await promise;
expect(response.req.port).toBe(80);
expect(response.req.agent.defaultPort).toBe(80);
expect(response.req.protocol).toBe("http:");

View File

@@ -3,7 +3,9 @@ import http from "node:http";
const { expect } = createTest(import.meta.path);
const { promise, resolve } = Promise.withResolvers();
http.request("http://google.com/", resolve).end();
const req = http.request("http://google.com/", resolve);
req.end();
const response = await promise;
expect(response.req.port).toBe(80);
expect(response.req.agent.defaultPort).toBe(80);
expect(response.req.protocol).toBe("http:");
req.destroy();

View File

@@ -19,7 +19,7 @@ const req = http.get(
resolve,
);
const { socket } = req;
await promise;
const socket = req.res.socket;
expect(socket._httpMessage).toBe(req);
socket.destroy();

View File

@@ -23,11 +23,10 @@ expect(
["req", "finish"],
["req", "response"],
"STATUS: 200",
// TODO: not totally right:
["res", "resume"],
["req", "close"],
["res", "readable"],
["res", "end"],
["req", "close"],
["res", "close"],
]);
expect(await exited).toBe(0);

View File

@@ -6,5 +6,5 @@ const agent = new https.Agent();
const { promise, resolve } = Promise.withResolvers();
https.get({ agent, hostname: "google.com" }, resolve);
const response = await promise;
expect(response.req.port).toBe(443);
expect(response.req.agent.defaultPort).toBe(443);
expect(response.req.protocol).toBe("https:");

View File

@@ -1,9 +0,0 @@
import { createTest } from "node-harness";
import https from "node:https";
const { expect } = createTest(import.meta.path);
const { promise, resolve } = Promise.withResolvers();
https.request("https://google.com/", resolve).end();
const response = await promise;
expect(response.req.port).toBe(443);
expect(response.req.protocol).toBe("https:");

View File

@@ -3,7 +3,8 @@ import { once } from "node:events";
import { createServer, request } from "node:http";
describe("node:http client timeout", () => {
it("should emit timeout event when timeout is reached", async () => {
// test passes but hangs, as it does in node. rework to exit gracefully
it.todo("should emit timeout event when timeout is reached", async () => {
const server = createServer((req, res) => {
// Intentionally not sending response to trigger timeout
}).listen(0);
@@ -20,14 +21,14 @@ describe("node:http client timeout", () => {
});
let timeoutEventEmitted = false;
let destroyCalled = false;
let closeCalled = false;
req.on("timeout", () => {
timeoutEventEmitted = true;
});
req.on("close", () => {
destroyCalled = true;
closeCalled = true;
});
req.end();
@@ -36,8 +37,8 @@ describe("node:http client timeout", () => {
await new Promise(resolve => setTimeout(resolve, 100));
expect(timeoutEventEmitted).toBe(true);
expect(destroyCalled).toBe(true);
expect(req.destroyed).toBe(true);
expect(closeCalled).toBe(false);
expect(req.destroyed).toBe(false);
} finally {
server.close();
}
@@ -73,7 +74,7 @@ describe("node:http client timeout", () => {
await new Promise(resolve => setTimeout(resolve, 100));
expect(timeoutEventEmitted).toBe(false);
expect(req.destroyed).toBe(false);
expect(req.destroyed).toBe(true);
} finally {
server.close();
}

View File

@@ -4,7 +4,7 @@ import { createServer, request } from "node:http";
export async function run() {
const { promise, resolve, reject } = Promise.withResolvers();
using server = exampleSite("http");
await using server = exampleSite("http");
const proxyServer = createServer(function (req, res) {
// Use URL object instead of deprecated url.parse
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);

View File

@@ -852,18 +852,17 @@ describe("node:http", () => {
it("should emit a socket event when connecting", async done => {
runTest(done, async (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}`, {});
req.on("socket", function onRequestSocket(socket) {
req.destroy();
done();
});
const { promise, resolve } = Promise.withResolvers();
req.on("socket", resolve);
req.end();
await promise;
});
});
});
describe("https.request with custom tls options", () => {
it("supports custom tls args", async () => {
using httpsServer = exampleSite();
await using httpsServer = exampleSite();
const { promise, resolve, reject } = Promise.withResolvers();
const options: https.RequestOptions = {
@@ -979,7 +978,7 @@ describe("node:http", () => {
});
describe("ClientRequest.signal", () => {
it("should attempt to make a standard GET request and abort", async () => {
it.todo("should attempt to make a standard GET request and abort", async () => {
let server_port;
let server_host;
const {

View File

@@ -0,0 +1,50 @@
// 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 http = require('http');
let serverRes;
const server = http.Server(common.mustCall((req, res) => {
serverRes = res;
res.writeHead(200);
res.write('Part of my res.');
}));
server.listen(0, common.mustCall(() => {
http.get({
port: server.address().port,
headers: { connection: 'keep-alive' }
}, common.mustCall((res) => {
server.close();
serverRes.destroy();
res.resume();
res.on('end', common.mustNotCall());
res.on('aborted', common.mustCall());
res.on('error', common.expectsError({
code: 'ECONNRESET'
}));
res.on('close', common.mustCall());
res.socket.on('close', common.mustCall());
}));
}));

View File

@@ -0,0 +1,95 @@
// 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');
let complete;
const server = http.createServer(common.mustCall((req, res) => {
// We should not see the queued /thatotherone request within the server
// as it should be aborted before it is sent.
assert.strictEqual(req.url, '/');
res.writeHead(200);
res.write('foo');
complete ??= function() {
res.end();
};
}));
server.listen(0, common.mustCall(() => {
const agent = new http.Agent({ maxSockets: 1 });
assert.strictEqual(Object.keys(agent.sockets).length, 0);
const options = {
hostname: 'localhost',
port: server.address().port,
method: 'GET',
path: '/',
agent: agent
};
const req1 = http.request(options);
req1.on('response', (res1) => {
assert.strictEqual(Object.keys(agent.sockets).length, 1);
assert.strictEqual(Object.keys(agent.requests).length, 0);
const req2 = http.request({
method: 'GET',
host: 'localhost',
port: server.address().port,
path: '/thatotherone',
agent: agent
});
assert.strictEqual(Object.keys(agent.sockets).length, 1);
assert.strictEqual(Object.keys(agent.requests).length, 1);
// TODO(jasnell): This event does not appear to currently be triggered.
// is this handler actually required?
req2.on('error', (err) => {
// This is expected in response to our explicit abort call
assert.strictEqual(err.code, 'ECONNRESET');
});
req2.end();
req2.abort();
assert.strictEqual(Object.keys(agent.sockets).length, 1);
assert.strictEqual(Object.keys(agent.requests).length, 1);
res1.on('data', (chunk) => complete());
res1.on('end', common.mustCall(() => {
setTimeout(common.mustCall(() => {
assert.strictEqual(Object.keys(agent.sockets).length, 0);
assert.strictEqual(Object.keys(agent.requests).length, 0);
server.close();
}), 100);
}));
});
req1.end();
}));

View File

@@ -0,0 +1,37 @@
'use strict';
require('../common');
// This test ensures that `addRequest`'s Legacy API accepts `localAddress`
// correctly instead of accepting `path`.
// https://github.com/nodejs/node/issues/5051
const assert = require('assert');
const agent = require('http').globalAgent;
// Small stub just so we can call addRequest directly
const req = {
getHeader: () => {}
};
agent.maxSockets = 0;
// `localAddress` is used when naming requests / sockets while using the Legacy
// API. Port 8080 is hardcoded since this does not create a network connection.
agent.addRequest(req, 'localhost', 8080, '127.0.0.1');
assert.strictEqual(Object.keys(agent.requests).length, 1);
assert.strictEqual(
Object.keys(agent.requests)[0],
'localhost:8080:127.0.0.1');
// `path` is *not* used when naming requests / sockets.
// Port 8080 is hardcoded since this does not create a network connection
agent.addRequest(req, {
host: 'localhost',
port: 8080,
localAddress: '127.0.0.1',
path: '/foo'
});
assert.strictEqual(Object.keys(agent.requests).length, 1);
assert.strictEqual(
Object.keys(agent.requests)[0],
'localhost:8080:127.0.0.1');

View File

@@ -0,0 +1,73 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Agent = http.Agent;
const { getEventListeners, once } = require('events');
const agent = new Agent();
const server = http.createServer();
server.listen(0, common.mustCall(async () => {
const port = server.address().port;
const host = 'localhost';
const options = {
port: port,
host: host,
_agentKey: agent.getName({ port, host })
};
async function postCreateConnection() {
const ac = new AbortController();
const { signal } = ac;
const connection = agent.createConnection({ ...options, signal });
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
const [err] = await once(connection, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function preCreateConnection() {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const connection = agent.createConnection({ ...options, signal });
const [err] = await once(connection, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function agentAsParam() {
const ac = new AbortController();
const { signal } = ac;
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
const [err] = await once(request, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function agentAsParamPreAbort() {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
const [err] = await once(request, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
await postCreateConnection();
await preCreateConnection();
await agentAsParam();
await agentAsParamPreAbort();
server.close();
}));

View File

@@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const agent = new http.Agent();
const _err = new Error('kaboom');
agent.createSocket = function(req, options, cb) {
cb(_err);
};
const req = http
.request({
agent
})
.on('error', common.mustCall((err) => {
assert.strictEqual(err, _err);
}))
.on('close', common.mustCall(() => {
assert.strictEqual(req.destroyed, true);
}));

View File

@@ -0,0 +1,74 @@
// 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 Countdown = require('../common/countdown');
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
}, 2)).listen(0, common.mustCall(() => {
const agent = new http.Agent({ maxSockets: 1 });
agent.on('free', common.mustCall(3));
const requestOptions = {
agent: agent,
host: 'localhost',
port: server.address().port,
path: '/'
};
const request1 = http.get(requestOptions, common.mustCall((response) => {
// Assert request2 is queued in the agent
const key = agent.getName(requestOptions);
assert.strictEqual(agent.requests[key].length, 1);
response.resume();
response.on('end', common.mustCall(() => {
request1.socket.destroy();
request1.socket.once('close', common.mustCall(() => {
// Assert request2 was removed from the queue
assert(!agent.requests[key]);
process.nextTick(() => {
// Assert that the same socket was not assigned to request2,
// since it was destroyed.
assert.notStrictEqual(request1.socket, request2.socket);
assert(!request2.socket.destroyed, 'the socket is destroyed');
});
}));
}));
}));
const request2 = http.get(requestOptions, common.mustCall((response) => {
assert(!request2.socket.destroyed);
assert(request1.socket.destroyed);
// Assert not reusing the same socket, since it was destroyed.
assert.notStrictEqual(request1.socket, request2.socket);
const countdown = new Countdown(2, () => server.close());
request2.socket.on('close', common.mustCall(() => countdown.dec()));
response.on('end', common.mustCall(() => countdown.dec()));
response.resume();
}));
}));

View File

@@ -0,0 +1,49 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Agent = http.Agent;
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 2));
server.listen(0, () => {
const agent = new Agent({ keepAlive: true });
const requestParams = {
host: 'localhost',
port: server.address().port,
agent: agent,
path: '/'
};
const socketKey = agent.getName(requestParams);
http.get(requestParams, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(() => {
process.nextTick(common.mustCall(() => {
const freeSockets = agent.freeSockets[socketKey];
// Expect a free socket on socketKey
assert.strictEqual(freeSockets.length, 1);
// Generate a random error on the free socket
const freeSocket = freeSockets[0];
freeSocket.emit('error', new Error('ECONNRESET: test'));
http.get(requestParams, done);
}));
}));
}));
function done() {
// Expect the freeSockets pool to be empty
assert.strictEqual(Object.keys(agent.freeSockets).length, 0);
agent.destroy();
server.close();
}
});

View File

@@ -0,0 +1,56 @@
'use strict';
const common = require('../common');
const Countdown = require('../common/countdown');
// This test ensures that the `maxSockets` value for `http.Agent` is respected.
// https://github.com/nodejs/node/issues/4050
const assert = require('assert');
const http = require('http');
const MAX_SOCKETS = 2;
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: MAX_SOCKETS,
maxFreeSockets: 2
});
const server = http.createServer(
common.mustCall((req, res) => {
res.end('hello world');
}, 6)
);
const countdown = new Countdown(6, () => server.close());
function get(path, callback) {
return http.get(
{
host: 'localhost',
port: server.address().port,
agent: agent,
path: path
},
callback
);
}
server.listen(
0,
common.mustCall(() => {
for (let i = 0; i < 6; i++) {
const request = get('/1', common.mustCall());
request.on(
'response',
common.mustCall(() => {
request.abort();
const sockets = agent.sockets[Object.keys(agent.sockets)[0]];
assert(sockets.length <= MAX_SOCKETS);
countdown.dec();
})
);
}
})
);

View File

@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 2,
maxFreeSockets: 2
});
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 2));
server.keepAliveTimeout = 0;
function get(path, callback) {
return http.get({
host: 'localhost',
port: server.address().port,
agent: agent,
path: path
}, callback);
}
const countdown = new Countdown(2, () => {
const freepool = agent.freeSockets[Object.keys(agent.freeSockets)[0]];
assert.strictEqual(freepool.length, 2,
`expect keep 2 free sockets, but got ${freepool.length}`);
agent.destroy();
server.close();
});
function dec() {
process.nextTick(() => countdown.dec());
}
function onGet(res) {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(dec));
}
server.listen(0, common.mustCall(() => {
get('/1', common.mustCall(onGet));
get('/2', common.mustCall(onGet));
}));

View File

@@ -0,0 +1,111 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Countdown = require('../common/countdown');
assert.throws(() => new http.Agent({
maxTotalSockets: 'test',
}), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "maxTotalSockets" argument must be of type number. ' +
"Received type string ('test')",
});
[-1, 0, NaN].forEach((item) => {
assert.throws(() => new http.Agent({
maxTotalSockets: item,
}), {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
});
});
assert.ok(new http.Agent({
maxTotalSockets: Infinity,
}));
function start(param = {}) {
const { maxTotalSockets, maxSockets } = param;
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxTotalSockets,
maxSockets,
maxFreeSockets: 3
});
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 6));
const server2 = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
}, 6));
server.keepAliveTimeout = 0;
server2.keepAliveTimeout = 0;
const countdown = new Countdown(12, () => {
assert.strictEqual(getRequestCount(), 0);
agent.destroy();
server.close();
server2.close();
});
function handler(s) {
for (let i = 0; i < 6; i++) {
http.get({
host: 'localhost',
port: s.address().port,
agent,
path: `/${i}`,
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.on('end', common.mustCall(() => {
for (const key of Object.keys(agent.sockets)) {
assert(agent.sockets[key].length <= maxSockets);
}
assert(getTotalSocketsCount() <= maxTotalSockets);
countdown.dec();
}));
}));
}
}
function getTotalSocketsCount() {
let num = 0;
for (const key of Object.keys(agent.sockets)) {
num += agent.sockets[key].length;
}
return num;
}
function getRequestCount() {
let num = 0;
for (const key of Object.keys(agent.requests)) {
num += agent.requests[key].length;
}
return num;
}
server.listen(0, common.mustCall(() => handler(server)));
server2.listen(0, common.mustCall(() => handler(server2)));
}
// If maxTotalSockets is larger than maxSockets,
// then the origin check will be skipped
// when the socket is removed.
[{
maxTotalSockets: 2,
maxSockets: 3,
}, {
maxTotalSockets: 3,
maxSockets: 2,
}, {
maxTotalSockets: 2,
maxSockets: 2,
}].forEach(start);

View File

@@ -0,0 +1,123 @@
'use strict';
const common = require('../common');
if (common.isWindows) return; // TODO: BUN
const assert = require('assert');
const http = require('http');
const net = require('net');
const agent = new http.Agent({
keepAlive: true,
maxFreeSockets: Infinity,
maxSockets: Infinity,
maxTotalSockets: Infinity,
});
const server = net.createServer({
pauseOnConnect: true,
}, (sock) => {
// Do not read anything from `sock`
sock.pause();
sock.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: Keep-Alive\r\n\r\n');
});
server.listen(0, common.mustCall(() => {
sendFstReq(server.address().port);
}));
function sendFstReq(serverPort) {
const req = http.request({
agent,
host: '127.0.0.1',
port: serverPort,
}, (res) => {
res.on('data', noop);
res.on('end', common.mustCall(() => {
// Agent's socket reusing code is registered to process.nextTick(),
// and will be run after this function, make sure it take effect.
setImmediate(sendSecReq, serverPort, req.socket.localPort);
}));
});
// Make the `req.socket` non drained, i.e. has some data queued to write to
// and accept by the kernel. In Linux and Mac, we only need to call `req.end(aLargeBuffer)`.
// However, in Windows, the mechanism of acceptance is loose, the following code is a workaround
// for Windows.
/**
* https://docs.microsoft.com/en-US/troubleshoot/windows/win32/data-segment-tcp-winsock says
*
* Winsock uses the following rules to indicate a send completion to the application
* (depending on how the send is invoked, the completion notification could be the
* function returning from a blocking call, signaling an event, or calling a notification
* function, and so forth):
* - If the socket is still within SO_SNDBUF quota, Winsock copies the data from the application
* send and indicates the send completion to the application.
* - If the socket is beyond SO_SNDBUF quota and there's only one previously buffered send still
* in the stack kernel buffer, Winsock copies the data from the application send and indicates
* the send completion to the application.
* - If the socket is beyond SO_SNDBUF quota and there's more than one previously buffered send
* in the stack kernel buffer, Winsock copies the data from the application send. Winsock doesn't
* indicate the send completion to the application until the stack completes enough sends to put
* back the socket within SO_SNDBUF quota or only one outstanding send condition.
*/
req.on('socket', () => {
req.socket.on('connect', () => {
// Print tcp send buffer information
console.log(process.report.getReport().libuv.filter((handle) => handle.type === 'tcp'));
const dataLargerThanTCPSendBuf = Buffer.alloc(1024 * 1024 * 64, 0);
req.write(dataLargerThanTCPSendBuf);
req.uncork();
if (process.platform === 'win32') {
assert.ok(req.socket.writableLength === 0);
}
req.write(dataLargerThanTCPSendBuf);
req.uncork();
if (process.platform === 'win32') {
assert.ok(req.socket.writableLength === 0);
}
req.end(dataLargerThanTCPSendBuf);
assert.ok(req.socket.writableLength > 0);
});
});
}
function sendSecReq(serverPort, fstReqCliPort) {
// Make the second request, which should be sent on a new socket
// because the first socket is not drained and hence can not be reused
const req = http.request({
agent,
host: '127.0.0.1',
port: serverPort,
}, (res) => {
res.on('data', noop);
res.on('end', common.mustCall(() => {
setImmediate(sendThrReq, serverPort, req.socket.localPort);
}));
});
req.on('socket', common.mustCall((sock) => {
assert.notStrictEqual(sock.localPort, fstReqCliPort);
}));
req.end();
}
function sendThrReq(serverPort, secReqCliPort) {
// Make the third request, the agent should reuse the second socket we just made
const req = http.request({
agent,
host: '127.0.0.1',
port: serverPort,
}, noop);
req.on('socket', common.mustCall((sock) => {
assert.strictEqual(sock.localPort, secReqCliPort);
process.exit(0);
}));
}
function noop() { }

View File

@@ -0,0 +1,149 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
function createServer(count) {
return http.createServer(common.mustCallAtLeast((req, res) => {
// Return the remote port number used for this connection.
res.end(req.socket.remotePort.toString(10));
}), count);
}
function makeRequest(url, agent, callback) {
http
.request(url, { agent }, (res) => {
let data = '';
res.setEncoding('ascii');
res.on('data', (c) => {
data += c;
});
res.on('end', () => {
process.nextTick(callback, data);
});
})
.end();
}
function bulkRequest(url, agent, done) {
const ports = [];
let count = agent.maxSockets;
for (let i = 0; i < agent.maxSockets; i++) {
makeRequest(url, agent, callback);
}
function callback(port) {
count -= 1;
ports.push(port);
if (count === 0) {
done(ports);
}
}
}
function defaultTest() {
const server = createServer(8);
server.listen(0, onListen);
function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5
});
bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
server.close();
agent.destroy();
});
});
});
});
}
}
function fifoTest() {
const server = createServer(8);
server.listen(0, onListen);
function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'fifo'
});
bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[0], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[2], port);
server.close();
agent.destroy();
});
});
});
});
}
}
function lifoTest() {
const server = createServer(8);
server.listen(0, onListen);
function onListen() {
const url = `http://localhost:${server.address().port}`;
const agent = new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'lifo'
});
bulkRequest(url, agent, (ports) => {
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
makeRequest(url, agent, (port) => {
assert.strictEqual(ports[ports.length - 1], port);
server.close();
agent.destroy();
});
});
});
});
}
}
function badSchedulingOptionTest() {
try {
new http.Agent({
keepAlive: true,
maxSockets: 5,
scheduling: 'filo'
});
} catch (err) {
assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE');
assert.strictEqual(
err.message,
"The argument 'scheduling' must be one of: 'fifo', 'lifo'. " +
"Received 'filo'"
);
}
}
defaultTest();
fifoTest();
lifoTest();
badSchedulingOptionTest();

View File

@@ -0,0 +1,23 @@
'use strict';
const { mustCall } = require('../common');
const { strictEqual } = require('assert');
const { Agent, get } = require('http');
// Test that the listener that forwards the `'timeout'` event from the socket to
// the `ClientRequest` instance is added to the socket when the `timeout` option
// of the `Agent` is set.
const request = get({
agent: new Agent({ timeout: 50 }),
lookup: () => {}
});
request.on('socket', mustCall((socket) => {
strictEqual(socket.timeout, 50);
const listeners = socket.listeners('timeout');
strictEqual(listeners.length, 2);
strictEqual(listeners[1], request.timeoutCb);
}));

View File

@@ -0,0 +1,134 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
{
// Ensure reuse of successful sockets.
const agent = new http.Agent({ keepAlive: true });
const server = http.createServer((req, res) => {
res.end();
});
server.listen(0, common.mustCall(() => {
let socket;
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
socket = res.socket;
assert(socket);
res.resume();
socket.on('free', common.mustCall(() => {
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
assert.strictEqual(socket, res.socket);
assert(socket);
agent.destroy();
server.close();
}));
}));
}));
}));
}
{
// Ensure that timed-out sockets are not reused.
const agent = new http.Agent({ keepAlive: true, timeout: 50 });
const server = http.createServer((req, res) => {
res.end();
});
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
const socket = res.socket;
assert(socket);
res.resume();
socket.on('free', common.mustCall(() => {
socket.on('timeout', common.mustCall(() => {
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
assert.notStrictEqual(socket, res.socket);
assert.strictEqual(socket.destroyed, true);
agent.destroy();
server.close();
}));
}));
}));
}));
}));
}
{
// Ensure that destroyed sockets are not reused.
const agent = new http.Agent({ keepAlive: true });
const server = http.createServer((req, res) => {
res.end();
});
server.listen(0, common.mustCall(() => {
let socket;
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
socket = res.socket;
assert(socket);
res.resume();
socket.on('free', common.mustCall(() => {
socket.destroy();
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
assert.notStrictEqual(socket, res.socket);
assert(socket);
agent.destroy();
server.close();
}));
}));
}));
}));
}
{
// Ensure custom keepSocketAlive timeout is respected
const CUSTOM_TIMEOUT = 60;
const AGENT_TIMEOUT = 50;
class CustomAgent extends http.Agent {
keepSocketAlive(socket) {
if (!super.keepSocketAlive(socket)) {
return false;
}
socket.setTimeout(CUSTOM_TIMEOUT);
return true;
}
}
const agent = new CustomAgent({ keepAlive: true, timeout: AGENT_TIMEOUT });
const server = http.createServer((req, res) => {
res.end();
});
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port, agent })
.on('response', common.mustCall((res) => {
const socket = res.socket;
assert(socket);
res.resume();
socket.on('free', common.mustCall(() => {
socket.on('timeout', common.mustCall(() => {
assert.strictEqual(socket.timeout, CUSTOM_TIMEOUT);
agent.destroy();
server.close();
}));
}));
}));
}));
}

View File

@@ -0,0 +1,88 @@
// 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 Countdown = require('../common/countdown');
const assert = require('assert');
const http = require('http');
const N = 4;
const M = 4;
const server = http.Server(common.mustCall(function(req, res) {
res.writeHead(200);
res.end('hello world\n');
}, (N * M))); // N * M = good requests (the errors will not be counted)
function makeRequests(outCount, inCount, shouldFail) {
const countdown = new Countdown(
outCount * inCount,
common.mustCall(() => server.close())
);
let onRequest = common.mustNotCall(); // Temporary
const p = new Promise((resolve) => {
onRequest = common.mustCall((res) => {
if (countdown.dec() === 0) {
resolve();
}
if (!shouldFail)
res.resume();
}, outCount * inCount);
});
server.listen(0, () => {
const port = server.address().port;
for (let i = 0; i < outCount; i++) {
setTimeout(() => {
for (let j = 0; j < inCount; j++) {
const req = http.get({ port: port, path: '/' }, onRequest);
if (shouldFail)
req.on('error', common.mustCall(onRequest));
else
req.on('error', (e) => assert.fail(e));
}
}, i);
}
});
return p;
}
const test1 = makeRequests(N, M);
const test2 = () => {
// Should not explode if can not create sockets.
// Ref: https://github.com/nodejs/node/issues/13045
// Ref: https://github.com/nodejs/node/issues/13831
http.Agent.prototype.createConnection = function createConnection(_, cb) {
process.nextTick(cb, new Error('nothing'));
};
return makeRequests(N, M, true);
};
test1
.then(test2)
.catch((e) => {
// This is currently the way to fail a test with a Promise.
console.error(e);
process.exit(1);
}
);

View File

@@ -0,0 +1,139 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
const { getEventListeners } = require('events');
{
// abort
const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello');
}));
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustCall((res) => {
res.on('data', (data) => {
req.abort();
assert.strictEqual(req.aborted, true);
assert.strictEqual(req.destroyed, true);
server.close();
});
}));
req.on('error', common.mustNotCall());
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
}));
}
{
// destroy + res
const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello');
}));
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustCall((res) => {
res.on('data', (data) => {
req.destroy();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
server.close();
});
}));
req.on('error', common.mustNotCall());
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
}));
}
{
// destroy
const server = http.createServer(common.mustNotCall());
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ECONNRESET');
server.close();
}));
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
req.destroy();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}
{
// Destroy post-abort sync with AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
server.close();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
controller.abort();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}
{
// Use post-abort async AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
}));
req.on('close', common.mustCall(() => {
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
server.close();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
process.nextTick(() => controller.abort());
}));
}
{
// Use pre-aborted AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
controller.abort();
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
server.close();
}));
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}

View File

@@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
let socketsCreated = 0;
class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socketsCreated++;
return socket;
}
}
const server = http.createServer((req, res) => res.end());
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const agent = new Agent({
keepAlive: true,
maxSockets: 1
});
const req = http.get({ agent, port }, common.mustCall((res) => {
res.resume();
res.on('end', () => {
res.destroy();
http.get({ agent, port }, common.mustCall((res) => {
res.resume();
assert.strictEqual(socketsCreated, 1);
agent.destroy();
server.close();
}));
});
}));
req.end();
}));

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
for (const destroyer of ['destroy', 'abort']) {
let socketsCreated = 0;
class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socketsCreated++;
return socket;
}
}
const server = http.createServer((req, res) => res.end());
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const agent = new Agent({
keepAlive: true,
maxSockets: 1
});
http.get({ agent, port }, (res) => res.resume());
const req = http.get({ agent, port }, common.mustNotCall());
req[destroyer]();
if (destroyer === 'destroy') {
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ECONNRESET');
}));
} else {
req.on('error', common.mustNotCall());
}
http.get({ agent, port }, common.mustCall((res) => {
res.resume();
assert.strictEqual(socketsCreated, 1);
agent.destroy();
server.close();
}));
}));
}

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
if (common.isWindows) return; // TODO: BUN
const assert = require('assert');
const http = require('http');
let socketsCreated = 0;
class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socketsCreated++;
return socket;
}
}
const server = http.createServer((req, res) => res.end());
const socketPath = common.PIPE;
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
server.listen(socketPath, common.mustCall(() => {
const agent = new Agent({
keepAlive: true,
maxSockets: 1
});
http.get({ agent, socketPath }, (res) => res.resume());
const req = http.get({ agent, socketPath }, common.mustNotCall());
req.abort();
http.get({ agent, socketPath }, common.mustCall((res) => {
res.resume();
assert.strictEqual(socketsCreated, 1);
agent.destroy();
server.close();
}));
}));

View File

@@ -0,0 +1,19 @@
'use strict';
const common = require('../common');
const http = require('http');
const net = require('net');
const server = http.createServer(common.mustNotCall());
server.listen(0, common.mustCall(() => {
const req = http.get({
createConnection(options, oncreate) {
const socket = net.createConnection(options, oncreate);
socket.once('close', () => server.close());
return socket;
},
port: server.address().port
});
req.abort();
}));

View File

@@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
if (common.isWindows) return; // TODO: BUN
const http = require('http');
const server = http.createServer(common.mustNotCall());
class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socket.once('close', () => server.close());
return socket;
}
}
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
server.listen(common.PIPE, common.mustCall(() => {
const req = http.get({
agent: new Agent(),
socketPath: common.PIPE
});
req.abort();
}));

View File

@@ -0,0 +1,32 @@
'use strict';
const common = require('../common');
const http = require('http');
const net = require('net');
function createConnection() {
const socket = new net.Socket();
process.nextTick(function() {
socket.destroy(new Error('Oops'));
});
return socket;
}
{
const req = http.get({ createConnection });
req.on('error', common.expectsError({ name: 'Error', message: 'Oops' }));
req.abort();
}
{
class CustomAgent extends http.Agent {}
CustomAgent.prototype.createConnection = createConnection;
const req = http.get({ agent: new CustomAgent() });
req.on('error', common.expectsError({ name: 'Error', message: 'Oops' }));
req.abort();
}

View File

@@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
const http = require('http');
{
let serverRes;
const server = http.Server(function(req, res) {
res.write('Part of my res.');
serverRes = res;
});
server.listen(0, common.mustCall(function() {
http.get({
port: this.address().port,
headers: { connection: 'keep-alive' }
}, common.mustCall(function(res) {
server.close();
serverRes.destroy();
res.on('aborted', common.mustCall());
res.on('error', common.expectsError({
code: 'ECONNRESET'
}));
}));
}));
}
{
// Don't crash of no 'error' handler.
let serverRes;
const server = http.Server(function(req, res) {
res.write('Part of my res.');
serverRes = res;
});
server.listen(0, common.mustCall(function() {
http.get({
port: this.address().port,
headers: { connection: 'keep-alive' }
}, common.mustCall(function(res) {
server.close();
serverRes.destroy();
res.on('aborted', common.mustCall());
}));
}));
}

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello');
}));
const keepAliveAgent = new http.Agent({ keepAlive: true });
server.listen(0, common.mustCall(() => {
const req = http.get({
port: server.address().port,
agent: keepAliveAgent
});
req
.on('response', common.mustCall((res) => {
res
.on('close', common.mustCall(() => {
assert.strictEqual(req.destroyed, true);
server.close();
keepAliveAgent.destroy();
}))
.on('data', common.mustCall());
}))
.end();
}));

View File

@@ -0,0 +1,71 @@
// 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 Countdown = require('../common/countdown');
let name;
const max = 3;
const agent = new http.Agent();
const server = http.Server(common.mustCall((req, res) => {
if (req.url === '/0') {
setTimeout(common.mustCall(() => {
res.writeHead(200);
res.end('Hello, World!');
}), 100);
} else {
res.writeHead(200);
res.end('Hello, World!');
}
}, max));
server.listen(0, common.mustCall(() => {
name = agent.getName({ port: server.address().port });
for (let i = 0; i < max; ++i)
request(i);
}));
const countdown = new Countdown(max, () => {
assert(!(name in agent.sockets));
assert(!(name in agent.requests));
server.close();
});
function request(i) {
const req = http.get({
port: server.address().port,
path: `/${i}`,
agent
}, function(res) {
const socket = req.socket;
socket.on('close', common.mustCall(() => {
countdown.dec();
if (countdown.remaining > 0) {
assert.strictEqual(agent.sockets[name].includes(socket),
false);
}
}));
res.resume();
});
}

View File

@@ -0,0 +1,31 @@
'use strict';
const common = require('../common');
// This test ensures that the `'close'` event is emitted after the `'error'`
// event when a request is made and the socket is closed before we started to
// receive a response.
const assert = require('assert');
const http = require('http');
const server = http.createServer(common.mustNotCall());
server.listen(0, common.mustCall(() => {
const req = http.get({ port: server.address().port }, common.mustNotCall());
let errorEmitted = false;
req.on('error', common.mustCall((err) => {
errorEmitted = true;
assert.strictEqual(err.constructor, Error);
assert.strictEqual(err.message, 'socket hang up');
assert.strictEqual(err.code, 'ECONNRESET');
}));
req.on('close', common.mustCall(() => {
assert.strictEqual(req.destroyed, true);
assert.strictEqual(errorEmitted, true);
server.close();
}));
req.destroy();
}));

View File

@@ -0,0 +1,70 @@
// 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 { once } = require('events');
const expectedHeaders = {
'DELETE': ['host', 'connection'],
'GET': ['host', 'connection'],
'HEAD': ['host', 'connection'],
'OPTIONS': ['host', 'connection'],
'POST': ['host', 'connection', 'content-length'],
'PUT': ['host', 'connection', 'content-length'],
'TRACE': ['host', 'connection']
};
const expectedMethods = Object.keys(expectedHeaders);
const server = http.createServer(common.mustCall((req, res) => {
res.end();
assert(Object.hasOwn(expectedHeaders, req.method),
`${req.method} was an unexpected method`);
const requestHeaders = Object.keys(req.headers);
for (const header of requestHeaders) {
assert.ok(
expectedHeaders[req.method].includes(header.toLowerCase()),
`${header} should not exist for method ${req.method}`
);
}
assert.strictEqual(
requestHeaders.length,
expectedHeaders[req.method].length,
`some headers were missing for method: ${req.method}`
);
}, expectedMethods.length));
server.listen(0, common.mustCall(() => {
Promise.all(expectedMethods.map(async (method) => {
const request = http.request({
method: method,
port: server.address().port
}).end();
return once(request, 'response');
})).then(common.mustCall(() => { server.close(); }));
}));

View File

@@ -0,0 +1,32 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const response = Buffer.from('HTTP/1.1 200 OK\r\n' +
'Content-Length: 6\r\n' +
'Transfer-Encoding: Chunked\r\n' +
'\r\n' +
'6\r\nfoobar' +
'0\r\n');
const server = net.createServer(common.mustCall((conn) => {
conn.write(response);
}));
server.listen(0, common.mustCall(() => {
const req = http.get(`http://localhost:${server.address().port}/`);
req.end();
req.on('error', common.mustCall((err) => {
const reason = "Transfer-Encoding can't be present with Content-Length";
assert.strictEqual(err.message, `Parse Error: ${reason}`);
assert(err.bytesParsed < response.length);
assert(err.bytesParsed >= response.indexOf('Transfer-Encoding'));
assert.strictEqual(err.code, 'HPE_INVALID_TRANSFER_ENCODING');
assert.strictEqual(err.reason, reason);
assert.deepStrictEqual(err.rawPacket, response);
server.close();
}));
}));

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const http = require('http');
const { finished } = require('stream');
{
// Test abort before finished.
const server = http.createServer(function(req, res) {
res.write('asd');
});
server.listen(0, common.mustCall(function() {
http.request({
port: this.address().port
})
.on('response', (res) => {
res.on('readable', () => {
res.destroy();
});
finished(res, common.mustCall(() => {
server.close();
}));
})
.end();
}));
}

View File

@@ -0,0 +1,32 @@
'use strict';
const common = require('../common');
const { createServer, get } = require('http');
const assert = require('assert');
const server = createServer(common.mustCall((req, res) => {
res.writeHead(200);
res.write('Part of res.');
}));
function onUncaught(error) {
assert.strictEqual(error.message, 'Destroy test');
server.close();
}
process.on('uncaughtException', common.mustCall(onUncaught));
server.listen(0, () => {
get({
port: server.address().port
}, common.mustCall((res) => {
const err = new Error('Destroy test');
assert.strictEqual(res.errored, null);
res.destroy(err);
assert.strictEqual(res.closed, false);
assert.strictEqual(res.errored, err);
res.on('close', () => {
assert.strictEqual(res.closed, true);
});
}));
});

View File

@@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
const http = require('http');
const server = http.createServer((req, res) => {
res.end();
}).listen(0, common.mustCall(() => {
const agent = new http.Agent({
maxSockets: 1,
keepAlive: true
});
const port = server.address().port;
const post = http.request({
agent,
method: 'POST',
port,
}, common.mustCall((res) => {
res.resume();
}));
// What happens here is that the server `end`s the response before we send
// `something`, and the client thought that this is a green light for sending
// next GET request
post.write(Buffer.alloc(16 * 1024, 'X'));
setTimeout(() => {
post.end('something');
}, 100);
http.request({
agent,
method: 'GET',
port,
}, common.mustCall((res) => {
server.close();
res.connection.end();
})).end();
}));

View File

@@ -0,0 +1,56 @@
// 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.isWindows) return; // TODO: BUN
const http = require('http');
const net = require('net');
const assert = require('assert');
const Countdown = require('../common/countdown');
const countdown = new Countdown(2, () => server.close());
const payloads = [
'HTTP/1.1 302 Object Moved\r\nContent-Length: 0\r\n\r\nhi world',
'bad http = should trigger parse error',
];
// Create a TCP server
const server =
net.createServer(common.mustCall((c) => c.end(payloads.shift()), 2));
server.listen(0, common.mustCall(() => {
for (let i = 0; i < 2; i++) {
const req = http.get({
port: server.address().port,
path: '/'
}).on('error', common.mustCall((e) => {
assert.strictEqual(req.socket.listenerCount('data'), 0);
assert.strictEqual(req.socket.listenerCount('end'), 1);
common.expectsError({
code: 'HPE_INVALID_CONSTANT',
message: 'Parse Error: Expected HTTP/, RTSP/ or ICE/'
})(e);
countdown.dec();
}));
}
}));

View File

@@ -0,0 +1,71 @@
// 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 Duplex = require('stream').Duplex;
class FakeAgent extends http.Agent {
createConnection() {
const s = new Duplex();
let once = false;
s._read = function() {
if (once)
return this.push(null);
once = true;
this.push('HTTP/1.1 200 Ok\r\nTransfer-Encoding: chunked\r\n\r\n');
this.push('b\r\nhello world\r\n');
this.readable = false;
this.push('0\r\n\r\n');
};
// Blackhole
s._write = function(data, enc, cb) {
cb();
};
s.destroy = s.destroySoon = function() {
this.writable = false;
};
return s;
}
}
let received = '';
const req = http.request({
agent: new FakeAgent()
}, common.mustCall(function requestCallback(res) {
res.on('data', function dataCallback(chunk) {
received += chunk;
});
res.on('end', common.mustCall(function endCallback() {
assert.strictEqual(received, 'hello world');
}));
}));
req.end();

View File

@@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const http = require('http');
const net = require('net');
const assert = require('assert');
const reqstr = 'HTTP/1.1 200 OK\r\n' +
'Content-Length: 1\r\n' +
'Transfer-Encoding: chunked\r\n\r\n';
const server = net.createServer((socket) => {
socket.write(reqstr);
});
server.listen(0, () => {
// The callback should not be called because the server is sending
// both a Content-Length header and a Transfer-Encoding: chunked
// header, which is a violation of the HTTP spec.
const req = http.get({ port: server.address().port }, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_INVALID_TRANSFER_ENCODING');
server.close();
}));
});

View File

@@ -0,0 +1,25 @@
'use strict';
const common = require('../common');
const http = require('http');
const net = require('net');
const assert = require('assert');
const reqstr = 'HTTP/1.1 200 OK\r\n' +
'Foo: Bar\r' +
'Content-Length: 1\r\n\r\n';
const server = net.createServer((socket) => {
socket.write(reqstr);
});
server.listen(0, () => {
// The callback should not be called because the server is sending a
// header field that ends only in \r with no following \n
const req = http.get({ port: server.address().port }, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_LF_EXPECTED');
server.close();
}));
});

View File

@@ -0,0 +1,33 @@
'use strict';
// Test https://github.com/nodejs/node/issues/25499 fix.
const { mustCall } = require('../common');
const { Agent, createServer, get } = require('http');
const { strictEqual } = require('assert');
const server = createServer(mustCall((req, res) => {
res.end();
}));
server.listen(0, () => {
const agent = new Agent({ keepAlive: true, maxSockets: 1 });
const port = server.address().port;
let socket;
const req = get({ agent, port }, (res) => {
res.on('end', () => {
strictEqual(req.setTimeout(0), req);
strictEqual(socket.listenerCount('timeout'), 1);
agent.destroy();
server.close();
});
res.resume();
});
req.on('socket', (sock) => {
socket = sock;
});
});

View File

@@ -0,0 +1,49 @@
'use strict';
// Test that the `'timeout'` event is emitted exactly once if the `timeout`
// option and `request.setTimeout()` are used together.
const { expectsError, mustCall } = require('../common');
const { strictEqual } = require('assert');
const { createServer, get } = require('http');
const server = createServer(() => {
// Never respond.
});
server.listen(0, mustCall(() => {
const req = get({
port: server.address().port,
timeout: 2000,
});
req.setTimeout(1000);
req.on('socket', mustCall((socket) => {
strictEqual(socket.timeout, 2000);
socket.on('connect', mustCall(() => {
strictEqual(socket.timeout, 1000);
// Reschedule the timer to not wait 1 sec and make the test finish faster.
socket.setTimeout(10);
strictEqual(socket.timeout, 10);
}));
}));
req.on('error', expectsError({
name: 'Error',
code: 'ECONNRESET',
message: 'socket hang up'
}));
req.on('close', mustCall(() => {
strictEqual(req.destroyed, true);
server.close();
}));
req.on('timeout', mustCall(() => {
strictEqual(req.socket.listenerCount('timeout'), 1);
req.destroy();
}));
}));

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
const agent = new http.Agent({ keepAlive: true });
const server = http.createServer((req, res) => {
res.end('');
});
// Maximum allowed value for timeouts
const timeout = 2 ** 31 - 1;
const options = {
agent,
method: 'GET',
port: undefined,
host: common.localhostIPv4,
path: '/',
timeout: timeout
};
server.listen(0, options.host, common.mustCall(() => {
options.port = server.address().port;
doRequest(common.mustCall((numListeners) => {
assert.strictEqual(numListeners, 3);
doRequest(common.mustCall((numListeners) => {
assert.strictEqual(numListeners, 3);
server.close();
agent.destroy();
}));
}));
}));
function doRequest(cb) {
http.request(options, common.mustCall((response) => {
const sockets = agent.sockets[`${options.host}:${options.port}:`];
assert.strictEqual(sockets.length, 1);
const socket = sockets[0];
const numListeners = socket.listeners('timeout').length;
response.resume();
response.once('end', common.mustCall(() => {
process.nextTick(cb, numListeners);
}));
})).end();
}

View File

@@ -0,0 +1,23 @@
'use strict';
// Test that the request `timeout` option has precedence over the agent
// `timeout` option.
const { mustCall } = require('../common');
const { Agent, get } = require('http');
const { strictEqual } = require('assert');
const request = get({
agent: new Agent({ timeout: 50 }),
lookup: () => {},
timeout: 100
});
request.on('socket', mustCall((socket) => {
strictEqual(socket.timeout, 100);
const listeners = socket.listeners('timeout');
strictEqual(listeners.length, 2);
strictEqual(listeners[1], request.timeoutCb);
}));

View File

@@ -0,0 +1,63 @@
// 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');
let nchunks = 0;
const options = {
method: 'GET',
port: undefined,
host: '127.0.0.1',
path: '/'
};
const server = http.createServer(function(req, res) {
res.writeHead(200, { 'Content-Length': '2' });
res.write('*');
server.once('timeout', common.mustCall(function() { res.end('*'); }));
});
server.listen(0, options.host, function() {
options.port = this.address().port;
const req = http.request(options, onresponse);
req.end();
function onresponse(res) {
req.setTimeout(50, common.mustCall(function() {
assert.strictEqual(nchunks, 1); // Should have received the first chunk
server.emit('timeout');
}));
res.on('data', common.mustCall(function(data) {
assert.strictEqual(String(data), '*');
nchunks++;
}, 2));
res.on('end', common.mustCall(function() {
assert.strictEqual(nchunks, 2);
server.close();
}));
}
});

View File

@@ -0,0 +1,90 @@
// 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 http = require('http');
const net = require('net');
const assert = require('assert');
function commonHttpGet(fn) {
if (typeof fn === 'function') {
fn = common.mustCall(fn);
}
return new Promise((resolve, reject) => {
http.get({ createConnection: fn }, (res) => {
resolve(res);
}).on('error', (err) => {
reject(err);
});
});
}
const server = http.createServer(common.mustCall(function(req, res) {
res.end();
}, 4)).listen(0, '127.0.0.1', async () => {
await commonHttpGet(createConnection);
await commonHttpGet(createConnectionAsync);
await commonHttpGet(createConnectionBoth1);
await commonHttpGet(createConnectionBoth2);
// Errors
await assert.rejects(() => commonHttpGet(createConnectionError), {
message: 'sync'
});
await assert.rejects(() => commonHttpGet(createConnectionAsyncError), {
message: 'async'
});
server.close();
});
function createConnection() {
return net.createConnection(server.address().port, '127.0.0.1');
}
function createConnectionAsync(options, cb) {
setImmediate(function() {
cb(null, net.createConnection(server.address().port, '127.0.0.1'));
});
}
function createConnectionBoth1(options, cb) {
const socket = net.createConnection(server.address().port, '127.0.0.1');
setImmediate(function() {
cb(null, socket);
});
return socket;
}
function createConnectionBoth2(options, cb) {
const socket = net.createConnection(server.address().port, '127.0.0.1');
cb(null, socket);
return socket;
}
function createConnectionError(options, cb) {
throw new Error('sync');
}
function createConnectionAsyncError(options, cb) {
process.nextTick(cb, new Error('async'));
}

View File

@@ -0,0 +1,60 @@
// 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 fixtures = require('../common/fixtures');
const http = require('http');
const https = require('https');
const assert = require('assert');
const hostExpect = 'localhost';
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
for (const { mod, createServer } of [
{ mod: http, createServer: http.createServer },
{ mod: https, createServer: https.createServer.bind(null, options) },
]) {
const server = createServer(common.mustCall((req, res) => {
assert.strictEqual(req.headers.host, hostExpect);
assert.strictEqual(req.headers['x-port'], `${server.address().port}`);
res.writeHead(200);
res.end('ok');
server.close();
})).listen(0, common.mustCall(() => {
mod.globalAgent.defaultPort = server.address().port;
mod.get({
host: 'localhost',
rejectUnauthorized: false,
headers: {
'x-port': server.address().port
}
}, common.mustCall((res) => {
res.resume();
}));
}));
}

View File

@@ -0,0 +1,79 @@
// 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');
// Verify that ECONNRESET is raised when writing to a http request
// where the server has ended the socket.
const assert = require('assert');
const http = require('http');
const kResponseDestroyed = Symbol('kResponseDestroyed');
const server = http.createServer(function(req, res) {
req.on('data', common.mustCall(function() {
res.destroy();
server.emit(kResponseDestroyed);
}));
});
server.listen(0, function() {
const req = http.request({
port: this.address().port,
path: '/',
method: 'POST'
});
server.once(kResponseDestroyed, common.mustCall(function() {
req.write('hello');
}));
req.on('error', common.mustCall(function(er) {
assert.strictEqual(req.res, null);
switch (er.code) {
// This is the expected case
case 'ECONNRESET':
break;
// On Windows, this sometimes manifests as ECONNABORTED
case 'ECONNABORTED':
break;
// This test is timing sensitive so an EPIPE is not out of the question.
// It should be infrequent, given the 50 ms timeout, but not impossible.
case 'EPIPE':
break;
default:
// Write to a torn down client should RESET or ABORT
assert.fail(`Unexpected error code ${er.code}`);
}
assert.strictEqual(req.outputData.length, 0);
server.close();
}));
req.on('response', common.mustNotCall());
req.write('hello', common.mustSucceed());
});

View File

@@ -0,0 +1,31 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(common.mustCall(function(req, res) {
assert.deepStrictEqual(req.rawHeaders, [
'host', `${common.localhostIPv4}:${server.address().port}`,
'foo', 'bar',
'test', 'value',
'foo', 'baz',
]);
res.end('ok');
server.close();
}));
server.listen(0, common.localhostIPv4, function() {
http.request({
method: 'POST',
host: common.localhostIPv4,
port: this.address().port,
setDefaultHeaders: false,
headers: [
'host', `${common.localhostIPv4}:${server.address().port}`,
'foo', 'bar',
'test', 'value',
'foo', 'baz',
]
}).end();
});

View File

@@ -1,5 +1,6 @@
'use strict';
const { expectsError, mustCall } = require('../common');
if ('Bun' in globalThis) require('../common').skip("TODO: BUN: test was edited and never worked");
const assert = require('assert');
const { createServer, maxHeaderSize } = require('http');
const { createConnection } = require('net');
@@ -16,22 +17,23 @@ const PAYLOAD = PAYLOAD_GET + CRLF + DUMMY_HEADER_NAME + DUMMY_HEADER_VALUE;
const server = createServer();
server.on('connection', mustCall((socket) => {
socket.on('error', expectsError({
name: 'Error',
message: 'Parse Error: Header overflow',
code: 'HPE_HEADER_OVERFLOW',
// those can be inconsistent depending if everything is sended in one go or not
// bytesParsed: PAYLOAD.length,
// rawPacket: Buffer.from(PAYLOAD)
bytesParsed: PAYLOAD.length,
rawPacket: Buffer.from(PAYLOAD)
}));
// The data is not sent from the client to ensure that it is received as a
// single chunk.
socket.push(PAYLOAD);
}));
server.listen(0, mustCall(() => {
const c = createConnection(server.address().port);
let received = '';
c.write(PAYLOAD);
c.on('data', mustCall((data) => {
received += data.toString();
}));

View File

@@ -0,0 +1,41 @@
'use strict';
// When using the object form of http.request and using an IPv6 address
// as a hostname, and using a non-standard port, the Host header
// is improperly formatted.
// Issue: https://github.com/nodejs/node/issues/5308
// As per https://tools.ietf.org/html/rfc7230#section-5.4 and
// https://tools.ietf.org/html/rfc3986#section-3.2.2
// the IPv6 address should be enclosed in square brackets
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const requests = [
{ host: 'foo:1234', headers: { expectedhost: 'foo:1234:80' } },
{ host: '::1', headers: { expectedhost: '[::1]:80' } },
];
function createLocalConnection(options) {
options.host = undefined;
options.port = this.port;
options.path = undefined;
return net.createConnection(options);
}
http.createServer(common.mustCall(function(req, res) {
this.requests ||= 0;
assert.strictEqual(req.headers.host, req.headers.expectedhost);
res.end();
if (++this.requests === requests.length)
this.close();
}, requests.length)).listen(0, function() {
const address = this.address();
for (let i = 0; i < requests.length; ++i) {
requests[i].createConnection =
common.mustCall(createLocalConnection.bind(address));
http.get(requests[i]);
}
});

View File

@@ -15,6 +15,7 @@ vals.forEach((v) => {
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "options.hostname" property must be of type string, undefined, or null.' + received
}
);
@@ -23,6 +24,7 @@ vals.forEach((v) => {
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "options.host" property must be of type string, undefined, or null.' + received
}
);
});

Some files were not shown because too many files have changed in this diff Show More