Compare commits

...

21 Commits

Author SHA1 Message Date
Ashcon Partovi
df509cc783 partially fix: test-http-server.js 2025-03-20 15:23:11 -07:00
Ashcon Partovi
1fe01a8750 fix: test-set-incoming-message-header.js 2025-03-20 15:15:45 -07:00
Ashcon Partovi
e9ff7a618a fix: test-http-response-no-headers.js 2025-03-20 15:09:17 -07:00
Ashcon Partovi
aaebe43299 fix: test-http-write-head.js 2025-03-20 15:08:32 -07:00
Ashcon Partovi
04000a9822 fix: test-http-client-request-options.js 2025-03-20 13:14:50 -07:00
Ashcon Partovi
0cd7cd6528 fix: test-http-correct-hostname.js 2025-03-20 12:56:11 -07:00
Ashcon Partovi
9a1a080d49 fix: deviate from fetch() spec and allow status codes up to 999 2025-03-20 12:48:58 -07:00
Ashcon Partovi
62de1caa02 fix: test-http-server-multiheaders2.js 2025-03-19 17:01:00 -07:00
Ashcon Partovi
124fe6cbac fix: test-http-server-multiheaders.js 2025-03-19 17:00:14 -07:00
Ashcon Partovi
93bac50410 fix: test-http-outgoing-internal-headernames-setter.js 2025-03-19 16:03:48 -07:00
Ashcon Partovi
91b16deebf fix: test-http-request-dont-override-options.js 2025-03-19 15:41:01 -07:00
Ashcon Partovi
f5af325b24 fix: test-http-agent-getname.js 2025-03-19 15:34:18 -07:00
Ashcon Partovi
3010388b2d fix: test-http-wget.js 2025-03-19 15:21:37 -07:00
Ashcon Partovi
d89a6eb948 fix: test-http-listening.js 2025-03-19 15:18:58 -07:00
Ashcon Partovi
7ebdf39223 fix: test-http-byteswritten.js 2025-03-19 15:15:29 -07:00
Ashcon Partovi
99f667685e fix: test-http-outgoing-settimeout.js 2025-03-19 14:45:14 -07:00
Ashcon Partovi
c0716aebfe fix: test-http-header-obstext.js 2025-03-19 14:29:07 -07:00
Ashcon Partovi
b58a8ea979 fix: test-http-response-setheaders.js 2025-03-19 14:23:18 -07:00
Ashcon Partovi
e054c11e10 fix: test-http-timeout-overflow.js 2025-03-19 13:48:49 -07:00
Ashcon Partovi
53cbffe733 fix: test-http-request-methods.js 2025-03-19 13:31:17 -07:00
Ashcon Partovi
e462ba3b64 fix: test-http-default-port.js 2025-03-19 11:51:12 -07:00
30 changed files with 1645 additions and 82 deletions

View File

@@ -485,6 +485,13 @@ namespace uWS
}
headers++;
/* Check for empty headers (only for HTTP 1.0 requests) */
if (isAncientHTTP && *postPaddedBuffer == '\r' && postPaddedBuffer[1] == '\n') {
/* End of headers immediately after request line */
headers->key = std::string_view(nullptr, 0);
return (unsigned int) ((postPaddedBuffer + 2) - start);
}
for (unsigned int i = 1; i < UWS_HTTP_MAX_HEADERS_COUNT - 1; i++) {
/* Lower case and consume the field name */
preliminaryKey = postPaddedBuffer;
@@ -599,8 +606,8 @@ namespace uWS
req->bf.add(h->key);
}
/* Break if no host header (but we can have empty string which is different from nullptr) */
if (!req->getHeader("host").data()) {
/* Host header is required for HTTP/1.1 but not for HTTP/1.0 */
if (!isAncientHTTP && !req->getHeader("host").data()) {
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
}
@@ -609,8 +616,16 @@ namespace uWS
* the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt
* to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and
* ought to be handled as an error. */
std::string_view transferEncodingString = req->getHeader("transfer-encoding");
std::string_view contentLengthString = req->getHeader("content-length");
/* Skip header checks for HTTP 1.0 requests with no headers */
std::string_view transferEncodingString;
std::string_view contentLengthString;
/* Only try to get headers if we have any */
HttpRequest::Header *firstHeader = req->headers + 1; // Skip request line (first header)
if (firstHeader->key.length() > 0) {
transferEncodingString = req->getHeader("transfer-encoding");
contentLengthString = req->getHeader("content-length");
}
auto transferEncodingStringLen = transferEncodingString.length();
auto contentLengthStringLen = contentLengthString.length();

View File

@@ -6559,7 +6559,7 @@ pub const NodeHTTPResponse = struct {
if (status_code_value != .undefined) {
break :brk globalObject.validateIntegerRange(status_code_value, i32, 200, .{
.min = 100,
.max = 599,
.max = 999,
}) catch return error.JSError;
}

View File

@@ -10,6 +10,7 @@
#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/IteratorOperations.h>
#include "wtf/URL.h"
#include "JSFetchHeaders.h"
#include "JSDOMExceptionHandling.h"
@@ -44,6 +45,7 @@ JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress);
BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex);
JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex);
JSC_DECLARE_HOST_FUNCTION(jsHTTPProcessArrayHeaders);
// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function
static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
@@ -1282,6 +1284,160 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPGetHeader, (JSGlobalObject * globalObject, CallFr
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPProcessArrayHeaders, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue headersArrayValue = callFrame->argument(0);
JSValue targetValue = callFrame->argument(1);
if (UNLIKELY(!isArray(globalObject, headersArrayValue) || !targetValue.isObject())) {
return JSValue::encode(jsUndefined());
}
auto* headersArray = jsCast<JSArray*>(headersArrayValue);
auto* targetObject = targetValue.getObject();
struct HeaderEntry {
String originalName;
Vector<String> values;
};
HashMap<String, HeaderEntry> headerMap;
auto addHeaderEntry = [&](const String& name, const String& value) {
String lowercaseName = name.convertToASCIILowercase();
auto result = headerMap.ensure(lowercaseName, [&name] {
HeaderEntry entry;
entry.originalName = name;
return entry;
});
result.iterator->value.values.append(value);
};
auto joinHeaderValues = [](const Vector<String>& values, ASCIILiteral delimiter) -> String {
if (UNLIKELY(values.isEmpty()))
return emptyString();
if (UNLIKELY(values.size() == 1))
return values[0];
StringBuilder builder;
builder.append(values[0]);
for (size_t i = 1; i < values.size(); i++) {
builder.append(delimiter);
builder.append(values[i]);
}
return builder.toString();
};
bool isNestedArray = false;
if (headersArray->length() > 0) {
JSValue firstItem = headersArray->getIndex(globalObject, 0);
isNestedArray = isArray(globalObject, firstItem);
}
if (isNestedArray) {
// Process array of form [['key', 'value'], ['key2', 'value2']]
forEachInIterable(globalObject, headersArrayValue, [&](JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue entryValue) {
auto scope = DECLARE_THROW_SCOPE(vm);
if (UNLIKELY(!isArray(globalObject, entryValue)))
return;
auto* entryArray = jsCast<JSArray*>(entryValue);
if (UNLIKELY(entryArray->length() < 2))
return;
JSValue nameValue = entryArray->getIndex(globalObject, 0);
JSValue valueValue = entryArray->getIndex(globalObject, 1);
if (UNLIKELY(!nameValue.isString() || !valueValue.isString()))
return;
String name = nameValue.toWTFString(globalObject);
if (UNLIKELY(scope.exception()))
return;
String value = valueValue.toWTFString(globalObject);
if (UNLIKELY(scope.exception()))
return;
addHeaderEntry(name, value);
});
} else {
// Process array of form ['key', 'value', 'key2', 'value2']
unsigned arrayLength = headersArray->length();
if (arrayLength % 2 == 0) {
for (unsigned i = 0; i < arrayLength; i += 2) {
JSValue nameValue = headersArray->getIndex(globalObject, i);
JSValue valueValue = headersArray->getIndex(globalObject, i + 1);
if (UNLIKELY(!nameValue.isString() || !valueValue.isString()))
continue;
auto entryScope = DECLARE_THROW_SCOPE(vm);
String name = nameValue.toWTFString(globalObject);
if (UNLIKELY(entryScope.exception()))
continue;
String value = valueValue.toWTFString(globalObject);
if (UNLIKELY(entryScope.exception()))
continue;
addHeaderEntry(name, value);
}
}
}
// Set of header names that should only use the first value when multiple values are sent
// These are based on RFC2616 and the multipleForbidden list in Node.js HTTP tests
static const HashSet<String> singleValueHeaders = {
"host"_s,
"content-type"_s,
"user-agent"_s,
"referer"_s,
"authorization"_s,
"proxy-authorization"_s,
"if-modified-since"_s,
"if-unmodified-since"_s,
"from"_s,
"location"_s,
"max-forwards"_s
};
for (auto& entry : headerMap) {
const String& lowercaseName = entry.key;
const HeaderEntry& headerEntry = entry.value;
const Vector<String>& values = headerEntry.values;
if (UNLIKELY(values.isEmpty()))
continue;
String headerName = headerEntry.originalName;
String headerValue;
// Headers that should only use the first value
if (singleValueHeaders.contains(lowercaseName)) {
headerValue = values[0];
} else if (lowercaseName == "cookie") {
// Cookie headers use semicolon+space as separator
headerValue = joinHeaderValues(values, ASCIILiteral("; "));
} else {
// All other headers use comma+space as separator
headerValue = joinHeaderValues(values, ASCIILiteral(", "));
}
targetObject->putDirect(vm, Identifier::fromString(vm, headerName), jsString(vm, headerValue), 0);
}
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
@@ -1353,6 +1509,9 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject)
obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "assignEventCallback"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "assignEventCallback"_s, jsHTTPAssignEventCallback, ImplementationVisibility::Public), 0);
obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "processArrayHeaders"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "processArrayHeaders"_s, jsHTTPProcessArrayHeaders, ImplementationVisibility::Public), 0);
obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setRequestTimeout"_s)),

View File

@@ -1760,7 +1760,8 @@ void WebCore__FetchHeaders__copyTo(WebCore__FetchHeaders* headers, StringPointer
*values = { i, value.length() };
i += value.length();
} else {
ASSERT_WITH_MESSAGE(value.containsOnlyASCII(), "Header value must be ASCII. This should already be validated before calling this function.");
// HTTP headers can contain non-ASCII characters according to RFC 7230
// Non-ASCII content should be properly encoded
WTF::CString valueCString = value.utf8();
memcpy(&buf[i], valueCString.data(), valueCString.length());
*values = { i, static_cast<uint32_t>(valueCString.length()) };

View File

@@ -714,11 +714,13 @@ pub const Response = struct {
if (response_init.fastGet(globalThis, .status)) |status_value| {
const number = status_value.coerceToInt64(globalThis);
if ((200 <= number and number < 600) or number == 101) {
// Even though the fetch spec says the range is [200, 599], there are some websites
// that use status codes up to 999, like linkedin.com, so allow it.
if ((200 <= number and number <= 999) or number == 101) {
result.status_code = @as(u16, @truncate(@as(u32, @intCast(number))));
} else {
if (!globalThis.hasException()) {
const err = globalThis.createRangeErrorInstance("The status provided ({d}) must be 101 or in the range of [200, 599]", .{number});
const err = globalThis.createRangeErrorInstance("The status provided ({d}) must be 101 or in the range of [200, 999]", .{number});
return globalThis.throwValue(err);
}
return error.JSError;

View File

@@ -15,6 +15,9 @@ function urlToHttpOptions(url) {
if (url.username || url.password) {
options.auth = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
}
if ("headers" in url) {
options.headers = url.headers;
}
return options;
}

View File

@@ -16,9 +16,9 @@ const enum NodeHTTPIncomingRequestType {
NodeHTTPResponse,
}
const enum NodeHTTPHeaderState {
none,
assigned,
sent,
none = 0,
assigned = 1 << 0,
sent = 1 << 1,
}
const enum NodeHTTPBodyReadState {
none,
@@ -61,6 +61,7 @@ const kInternalSocketData = Symbol.for("::bunternal::");
const serverSymbol = Symbol.for("::bunternal::");
const kPendingCallbacks = Symbol("pendingCallbacks");
const kRequest = Symbol("request");
const kTrailers = Symbol("kTrailers");
const kEmptyObject = Object.freeze(Object.create(null));
@@ -90,6 +91,7 @@ const {
webRequestOrResponseHasBodyValue,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
drainMicrotasks,
processArrayHeaders,
} = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as {
getHeader: (headers: Headers, name: string) => string | undefined;
setHeader: (headers: Headers, name: string, value: string) => void;
@@ -97,6 +99,7 @@ const {
assignEventCallback: (req: Request, callback: (event: number) => void) => void;
setRequestTimeout: (req: Request, timeout: number) => void;
setServerIdleTimeout: (server: any, timeout: number) => void;
processArrayHeaders: (headersArray: any[], targetObj: Record<string, string>) => void;
Response: (typeof globalThis)["Response"];
Request: (typeof globalThis)["Request"];
Headers: (typeof globalThis)["Headers"];
@@ -130,7 +133,7 @@ function checkInvalidHeaderChar(val: string) {
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
}
const validateHeaderName = (name, label) => {
const validateHeaderName = name => {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
throw $ERR_INVALID_HTTP_TOKEN(`The arguments Header name is invalid. Received ${name}`);
}
@@ -193,6 +196,16 @@ function validateMsecs(numberlike: any, field: string) {
throw $ERR_INVALID_ARG_TYPE(field, "number", numberlike);
}
// Ensure that msecs fits into signed int32
const TIMEOUT_MAX = 2 ** 31 - 1;
if (numberlike > TIMEOUT_MAX) {
process.emitWarning(
`${numberlike} does not fit into a 32-bit signed integer.` + `\nTimer duration was truncated to ${TIMEOUT_MAX}.`,
"TimeoutOverflowWarning",
);
return TIMEOUT_MAX;
}
return numberlike;
}
@@ -559,7 +572,7 @@ Agent.prototype.createConnection = function () {
};
Agent.prototype.getName = function (options = kEmptyObject) {
let name = `http:${options.host || "localhost"}:`;
let name = `${options.host || "localhost"}:`;
if (options.port) name += options.port;
name += ":";
if (options.localAddress) name += options.localAddress;
@@ -617,7 +630,7 @@ const Server = function Server(options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
} else if (options == null || typeof options === "object") {
} else if (options == null || (typeof options === "object" && !Array.isArray(options))) {
options = { ...options };
this[tlsSymbol] = null;
let key = options.key;
@@ -676,7 +689,7 @@ const Server = function Server(options, callback) {
this[tlsSymbol] = null;
}
} else {
throw new Error("bun-http-polyfill: invalid arguments");
throw $ERR_INVALID_ARG_TYPE("options", "object or function", options);
}
this[optionsSymbol] = options;
@@ -755,6 +768,7 @@ const ServerPrototype = {
return;
}
this[serverSymbol] = undefined;
this.listening = false;
if (typeof optionalCallback === "function") this.once("close", optionalCallback);
server.stop();
},
@@ -1237,6 +1251,7 @@ function IncomingMessage(req, defaultIncomingOpts) {
this._dumped = false;
this.complete = false;
this._closed = false;
this[kTrailers] = null;
// (url, method, headers, rawHeaders, handle, hasBody)
if (req === kHandle) {
@@ -1500,10 +1515,34 @@ const IncomingMessagePrototype = {
// noop
},
get trailers() {
return kEmptyObject;
if (!this[kTrailers]) {
this[kTrailers] = Object.create(null);
}
return this[kTrailers];
},
set trailers(value) {
// noop
this[kTrailers] = value;
},
_addHeaderLines(headers, n) {
if (headers?.length) {
let dest;
if (this.complete) {
dest = this.trailers;
} else {
dest = this.headers;
}
if (dest) {
for (let i = 0; i < n; i += 2) {
this._addHeaderLine(headers[i], headers[i + 1], dest);
}
}
}
},
_addHeaderLine(field, value, dest) {
if (dest[field] === undefined) {
dest[field] = value;
}
},
setTimeout(msecs, callback) {
this.take;
@@ -1602,6 +1641,23 @@ const OutgoingMessagePrototype = {
usesChunkedEncodingByDefault: true,
_closed: false,
get _headerNames() {
process.emitWarning("OutgoingMessage.prototype._headerNames is deprecated", "DeprecationWarning", "DEP0066");
const headers = this[headersSymbol];
if (!headers) return null;
const out = Object.create(null);
for (const key of headers.keys()) {
out[key.toLowerCase()] = key;
}
return out;
},
set _headerNames(val) {
process.emitWarning("OutgoingMessage.prototype._headerNames is deprecated", "DeprecationWarning", "DEP0066");
},
appendHeader(name, value) {
var headers = (this[headersSymbol] ??= new Headers());
headers.append(name, value);
@@ -1658,7 +1714,7 @@ const OutgoingMessagePrototype = {
},
removeHeader(name) {
if (this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) {
throw $ERR_HTTP_HEADERS_SENT("Cannot remove header after headers have been sent.");
}
const headers = this[headersSymbol];
@@ -1668,11 +1724,48 @@ const OutgoingMessagePrototype = {
setHeader(name, value) {
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
setHeader(headers, name, value);
return this;
},
setHeaders(headers) {
if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
if (!headers || Array.isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);
}
// Headers object joins multiple cookies with a comma when using
// the getter to retrieve the value,
// unless iterating over the headers directly.
// We also cannot safely split by comma.
// To avoid setHeader overwriting the previous value we push
// set-cookie values in array and set them all at once.
const cookies = [];
for (const [key, value] of headers) {
if (key === "set-cookie") {
if (Array.isArray(value)) {
cookies.push(...value);
} else {
cookies.push(value);
}
continue;
}
this.setHeader(key, value);
}
if (cookies.length) {
this.setHeader("set-cookie", cookies);
}
return this;
},
hasHeader(name) {
const headers = this[headersSymbol];
if (!headers) return false;
@@ -1701,6 +1794,10 @@ const OutgoingMessagePrototype = {
// even if it will be rescheduled we don't want to leak an existing timer.
clearTimeout(this[timeoutTimerSymbol]);
if (callback) {
this.on("timeout", callback);
}
if (msecs === 0) {
if (callback != null) {
if (!$isCallable(callback)) validateFunction(callback, "callback");
@@ -1711,9 +1808,13 @@ const OutgoingMessagePrototype = {
} else {
this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref();
if (callback != null) {
if (!$isCallable(callback)) validateFunction(callback, "callback");
this.once("timeout", callback);
// Node.js compatibility: also delegate to socket if available
if (!this[fakeSocketSymbol]) {
this.once("socket", function socketSetTimeoutOnConnect(socket) {
socket.setTimeout(msecs);
});
} else {
this[fakeSocketSymbol].setTimeout(msecs);
}
}
@@ -1730,7 +1831,11 @@ const OutgoingMessagePrototype = {
},
set socket(value) {
const prev = this[fakeSocketSymbol];
this[fakeSocketSymbol] = value;
if (!prev && value) {
this.emit("socket", value);
}
},
get chunkedEncoding() {
@@ -1797,7 +1902,7 @@ function onNodeHTTPServerSocketTimeout() {
if (!reqTimeout && !resTimeout && !serverTimeout) this.destroy();
}
function onTimeout() {
function handleRequestTimeout() {
this[timeoutTimerSymbol] = undefined;
this[kAbortController]?.abort();
const handle = this[kHandle];
@@ -1920,9 +2025,7 @@ const ServerResponsePrototype = {
_removedContLen: false,
_hasBody: true,
get headersSent() {
return (
this[headerStateSymbol] === NodeHTTPHeaderState.sent || this[headerStateSymbol] === NodeHTTPHeaderState.assigned
);
return this[headerStateSymbol] >= NodeHTTPHeaderState.assigned;
},
set headersSent(value) {
this[headerStateSymbol] = value ? NodeHTTPHeaderState.sent : NodeHTTPHeaderState.none;
@@ -1993,6 +2096,12 @@ const ServerResponsePrototype = {
}
}
// Update bytesWritten on the socket to ensure res.connection.bytesWritten works
if (chunk && this.socket) {
const byteLength = chunk instanceof Buffer ? chunk.length : Buffer.byteLength(chunk, encoding || "utf8");
this.socket.bytesWritten += byteLength;
}
if (handle) {
const headerState = this[headerStateSymbol];
callWriteHeadIfObservable(this, headerState);
@@ -2097,6 +2206,12 @@ const ServerResponsePrototype = {
result = handle.write(chunk, encoding);
}
// Update bytesWritten on the socket to ensure res.connection.bytesWritten works
if (chunk && this.socket) {
const byteLength = chunk instanceof Buffer ? chunk.length : Buffer.byteLength(chunk, encoding || "utf8");
this.socket.bytesWritten += byteLength;
}
if (result < 0) {
if (callback) {
// The write was buffered due to backpressure.
@@ -2186,6 +2301,13 @@ const ServerResponsePrototype = {
} else {
handle.write(data, encoding, callback);
}
// Update bytesWritten on the socket to ensure res.connection.bytesWritten works
if (data && this.socket) {
const dataByteLength =
byteLength || (data instanceof Buffer ? data.length : Buffer.byteLength(data, encoding || "utf8"));
this.socket.bytesWritten += dataByteLength;
}
},
writeHead(statusCode, statusMessage, headers) {
@@ -2193,6 +2315,43 @@ const ServerResponsePrototype = {
_writeHead(statusCode, statusMessage, headers, this);
updateHasBody(this, statusCode);
this[headerStateSymbol] = NodeHTTPHeaderState.assigned;
} else {
throw $ERR_HTTP_HEADERS_SENT("write");
}
return this;
},
setHeaders(headers) {
if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
if (!headers || Array.isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);
}
// Headers object joins multiple cookies with a comma when using
// the getter to retrieve the value,
// unless iterating over the headers directly.
// We also cannot safely split by comma.
// To avoid setHeader overwriting the previous value we push
// set-cookie values in array and set them all at once.
const cookies = [];
for (const [key, value] of headers) {
if (key === "set-cookie") {
if (Array.isArray(value)) {
cookies.push(...value);
} else {
cookies.push(value);
}
continue;
}
this.setHeader(key, value);
}
if (cookies.length) {
this.setHeader("set-cookie", cookies);
}
return this;
@@ -2649,7 +2808,15 @@ function ClientRequest(input, options, cb) {
url = path;
proxy = `${protocol}//${host}${this[kUseDefaultPort] ? "" : ":" + this[kPort]}`;
} else {
url = `${protocol}//${host}${this[kUseDefaultPort] ? "" : ":" + this[kPort]}${path}`;
// Always include the port when globalAgent.defaultPort has been explicitly changed
// or when the port is not the standard default (80 for http, 443 for https)
const includePort =
!this[kUseDefaultPort] ||
(this[kAgent] &&
this[kPort] === this[kAgent].defaultPort &&
((protocol === "http:" && this[kPort] !== 80) || (protocol === "https:" && this[kPort] !== 443)));
url = `${protocol}//${host}${includePort ? ":" + this[kPort] : ""}${path}`;
// support agent proxy url/string for http/https
try {
// getters can throw
@@ -2922,9 +3089,13 @@ function ClientRequest(input, options, cb) {
}
}
const defaultPort = options.defaultPort || this[kAgent].defaultPort;
const port = (this[kPort] = options.port || defaultPort || 80);
this[kUseDefaultPort] = this[kPort] === defaultPort;
// Ensure we use the latest defaultPort value from the agent
const defaultPort = options.defaultPort || (this[kAgent] && this[kAgent].defaultPort) || 80;
const port = (this[kPort] = options.port || defaultPort);
// When port is explicitly specified, we need to include it in the URL
// When port is equal to the agent's default port, we can omit it
this[kUseDefaultPort] = (this[kPort] === 80 && defaultPort === 80) || (this[kPort] === 443 && defaultPort === 443);
const host =
(this[kHost] =
options.host =
@@ -3071,56 +3242,74 @@ function ClientRequest(input, options, cb) {
const { headers } = options;
const headersArray = $isJSArray(headers);
if (!headersArray) {
if (headers) {
for (let key in headers) {
this.setHeader(key, headers[key]);
}
if (headersArray) {
// Use the native implementation to process array headers efficiently
// This will correctly handle array style headers:
// - Join multiple header values with commas (except cookies with semicolons)
// - Only use the first host header value
const processedHeaders = {};
processArrayHeaders(headers, processedHeaders);
// Now set all processed headers on this request
for (const key in processedHeaders) {
this.setHeader(key, processedHeaders[key]);
}
// if (host && !this.getHeader("host") && setHost) {
// let hostHeader = host;
// // For the Host header, ensure that IPv6 addresses are enclosed
// // in square brackets, as defined by URI formatting
// // https://tools.ietf.org/html/rfc3986#section-3.2.2
// const posColon = StringPrototypeIndexOf.$call(hostHeader, ":");
// if (
// posColon !== -1 &&
// StringPrototypeIncludes.$call(hostHeader, ":", posColon + 1) &&
// StringPrototypeCharCodeAt.$call(hostHeader, 0) !== 91 /* '[' */
// ) {
// hostHeader = `[${hostHeader}]`;
// }
// if (port && +port !== defaultPort) {
// hostHeader += ":" + port;
// }
// this.setHeader("Host", hostHeader);
// }
var auth = options.auth;
if (auth && !this.getHeader("Authorization")) {
this.setHeader("Authorization", "Basic " + Buffer.from(auth).toString("base64"));
} else if (headers) {
// Handle headers as an object
for (let key in headers) {
this.setHeader(key, headers[key]);
}
// if (this.getHeader("expect")) {
// if (this._header) {
// throw new ERR_HTTP_HEADERS_SENT("render");
// }
// this._storeHeader(
// this.method + " " + this.path + " HTTP/1.1\r\n",
// this[kOutHeaders],
// );
// }
// } else {
// this._storeHeader(
// this.method + " " + this.path + " HTTP/1.1\r\n",
// options.headers,
// );
}
// Always set the Host header if not already set
if (host && !this.getHeader("host")) {
let hostHeader = host;
// For the Host header, ensure that IPv6 addresses are enclosed
// in square brackets, as defined by URI formatting
// https://tools.ietf.org/html/rfc3986#section-3.2.2
const posColon = StringPrototypeIndexOf.$call(hostHeader, ":");
if (
posColon !== -1 &&
StringPrototypeIncludes.$call(hostHeader, ":", posColon + 1) &&
StringPrototypeCharCodeAt.$call(hostHeader, 0) !== 91 /* '[' */
) {
hostHeader = `[${hostHeader}]`;
}
// Only include the port in the Host header if it's not the default port for the protocol
// Also check the agent.defaultPort as some tests set it programmatically
const defaultPort =
options.defaultPort || (this[kAgent] && this[kAgent].defaultPort) || (protocol === "https:" ? 443 : 80);
if (port && +port !== defaultPort) {
hostHeader += ":" + port;
}
this.setHeader("Host", hostHeader);
}
var auth = options.auth;
if (auth && !this.getHeader("Authorization")) {
this.setHeader("Authorization", "Basic " + Buffer.from(auth).toString("base64"));
}
// if (this.getHeader("expect")) {
// if (this._header) {
// throw new ERR_HTTP_HEADERS_SENT("render");
// }
// this._storeHeader(
// this.method + " " + this.path + " HTTP/1.1\r\n",
// this[kOutHeaders],
// );
// }
// } else {
// this._storeHeader(
// this.method + " " + this.path + " HTTP/1.1\r\n",
// options.headers,
// );
// this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
const { signal: _signal, ...optsWithoutSignal } = options;
@@ -3188,6 +3377,20 @@ const ClientRequestPrototype = {
constructor: ClientRequest,
__proto__: OutgoingMessage.prototype,
clearTimeout(callback) {
const timeoutTimer = this[kTimeoutTimer];
if (timeoutTimer) {
clearTimeout(timeoutTimer);
this[kTimeoutTimer] = undefined;
if (callback) {
this.removeListener("timeout", callback);
} else {
this.removeAllListeners("timeout");
}
}
return this;
},
get path() {
return this[kPath];
},
@@ -3418,7 +3621,7 @@ function _writeHead(statusCode, reason, obj, response) {
let k;
if (Array.isArray(obj)) {
if (obj.length % 2 !== 0) {
throw new Error("raw headers must have an even number of elements");
throw $ERR_INVALID_ARG_VALUE("headers", obj, "must be an object or an array with even number of elements");
}
for (let n = 0; n < obj.length; n += 2) {

View File

@@ -125,6 +125,16 @@ const testCases: TestCase[] = [
description: "Valid GET request with HTTP/1.0",
expectedStatus: [[200, 299]],
},
{
request: "GET / HTTP/1.0\r\n\r\n",
description: "Valid GET request with HTTP/1.0 and no Host header",
expectedStatus: [[200, 299]],
},
{
request: "GET / HTTP/1.0\r\n\r\n",
description: "Valid GET request with HTTP/1.0 and no headers",
expectedStatus: [[200, 299]],
},
{
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request for a proxy URL",
@@ -155,6 +165,11 @@ const testCases: TestCase[] = [
description: "Valid GET request for an HTTP proxy URL",
expectedStatus: [[200, 299]],
},
{
request: "GET HTTP://example.com/ HTTP/1.1\r\n\r\n",
description: "Invalid GET request with HTTP/1.1 and no headers",
expectedStatus: [[400, 499]],
},
{
request: "GET HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid GET request target (space)",

View File

@@ -303,7 +303,7 @@ describe.todoIf(isBroken && isIntelMacOS)(
},
);
[200, 200n, 303, 418, 599, 599n].forEach(statusCode => {
[200, 200n, 303, 418, 599, 599n, 999, 999n].forEach(statusCode => {
it(`should response with HTTP status code (${statusCode})`, async () => {
await runTest(
{
@@ -320,7 +320,7 @@ describe.todoIf(isBroken && isIntelMacOS)(
});
});
[-200, 42, 100, 102, 12345, Math.PI, 999, 600, 199, 199n, 600n, 100n, 102n].forEach(statusCode => {
[-200, 42, 100, 102, 12345, Math.PI, 199, 199n, 100n, 102n].forEach(statusCode => {
it(`should error on invalid HTTP status code (${statusCode})`, async () => {
await runTest(
{

View File

@@ -0,0 +1,55 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const tmpdir = require('../common/tmpdir');
const agent = new http.Agent();
// Default to localhost
assert.strictEqual(
agent.getName({
port: 80,
localAddress: '192.168.1.1'
}),
'localhost:80:192.168.1.1'
);
// empty argument
assert.strictEqual(
agent.getName(),
'localhost::'
);
// empty options
assert.strictEqual(
agent.getName({}),
'localhost::'
);
// pass all arguments
assert.strictEqual(
agent.getName({
host: '0.0.0.0',
port: 80,
localAddress: '192.168.1.1'
}),
'0.0.0.0:80:192.168.1.1'
);
// unix socket
const socketPath = tmpdir.resolve('foo', 'bar');
assert.strictEqual(
agent.getName({
socketPath
}),
`localhost:::${socketPath}`
);
for (const family of [0, null, undefined, 'bogus'])
assert.strictEqual(agent.getName({ family }), 'localhost::');
for (const family of [4, 6])
assert.strictEqual(agent.getName({ family }), `localhost:::${family}`);

View File

@@ -0,0 +1,55 @@
// 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 body = 'hello world\n';
const httpServer = http.createServer(common.mustCall(function(req, res) {
httpServer.close();
res.on('finish', common.mustCall(function() {
assert.strictEqual(typeof req.connection.bytesWritten, 'number');
assert(req.connection.bytesWritten > 0);
}));
res.writeHead(200, { 'Content-Type': 'text/plain' });
// Write 1.5mb to cause some requests to buffer
// Also, mix up the encodings a bit.
const chunk = '7'.repeat(1024);
const bchunk = Buffer.from(chunk);
for (let i = 0; i < 1024; i++) {
res.write(chunk);
res.write(bchunk);
res.write(chunk, 'hex');
}
// Get .bytesWritten while buffer is not empty
assert(res.connection.bytesWritten > 0);
res.end(body);
}));
httpServer.listen(0, function() {
http.get({ port: this.address().port });
});

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const http = require('node:http');
const headers = { foo: 'Bar' };
const server = http.createServer(common.mustCall((req, res) => {
assert.strictEqual(req.url, '/ping?q=term');
assert.strictEqual(req.headers?.foo, headers.foo);
req.resume();
req.on('end', () => {
res.writeHead(200);
res.end('pong');
});
}));
server.listen(0, common.localhostIPv4, () => {
const { address, port } = server.address();
const url = new URL(`http://${address}:${port}/ping?q=term`);
url.headers = headers;
const clientReq = http.request(url);
clientReq.on('close', common.mustCall(() => {
server.close();
}));
clientReq.end();
});

View File

@@ -0,0 +1,23 @@
/* eslint-disable node-core/crypto-check */
// Flags: --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const modules = { http };
if (common.hasCrypto) {
const https = require('https');
modules.https = https;
}
Object.keys(modules).forEach((module) => {
const doNotCall = common.mustNotCall(
`${module}.request should not connect to ${module}://example.com%60x.example.com`
);
const req = modules[module].request(`${module}://example.com%60x.example.com`, doNotCall);
assert.equal(req.headers.host, 'example.com`x.example.com');
req.abort();
});

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,21 @@
'use strict';
const common = require('../common');
// This test ensures that the http-parser can handle UTF-8 characters
// in the http header.
const http = require('http');
const assert = require('assert');
const server = http.createServer(common.mustCall((req, res) => {
res.end('ok');
}));
server.listen(0, () => {
http.get({
port: server.address().port,
headers: { 'Test': 'Düsseldorf' }
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
server.close();
}));
});

View File

@@ -0,0 +1,16 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer();
assert.strictEqual(server.listening, false);
server.listen(0, common.mustCall(() => {
assert.strictEqual(server.listening, true);
server.close(common.mustCall(() => {
assert.strictEqual(server.listening, false);
}));
}));

View File

@@ -0,0 +1,15 @@
'use strict';
const common = require('../common');
const { OutgoingMessage } = require('http');
const warn = 'OutgoingMessage.prototype._headerNames is deprecated';
common.expectWarning('DeprecationWarning', warn, 'DEP0066');
{
// Tests for _headerNames set method
const outgoingMessage = new OutgoingMessage();
outgoingMessage._headerNames = {
'x-flow-id': '61bba6c5-28a3-4eab-9241-2ecaa6b6a1fd'
};
}

View File

@@ -0,0 +1,30 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { OutgoingMessage } = require('http');
{
// Tests for settimeout method with socket
const expectedMsecs = 42;
const outgoingMessage = new OutgoingMessage();
outgoingMessage.socket = {
setTimeout: common.mustCall((msecs) => {
assert.strictEqual(msecs, expectedMsecs);
})
};
outgoingMessage.setTimeout(expectedMsecs);
}
{
// Tests for settimeout method without socket
const expectedMsecs = 23;
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setTimeout(expectedMsecs);
outgoingMessage.emit('socket', {
setTimeout: common.mustCall((msecs) => {
assert.strictEqual(msecs, expectedMsecs);
})
});
}

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(common.mustCall(function(req, res) {
res.writeHead(200);
res.end('ok');
}));
server.listen(0, function() {
const agent = new http.Agent();
agent.defaultPort = this.address().port;
// Options marked as explicitly undefined for readability
// in this test, they should STAY undefined as options should not
// be mutable / modified
const options = {
host: undefined,
hostname: common.localhostIPv4,
port: undefined,
defaultPort: undefined,
path: undefined,
method: undefined,
agent: agent
};
http.request(options, function(res) {
res.resume();
server.close();
assert.strictEqual(options.host, undefined);
assert.strictEqual(options.hostname, common.localhostIPv4);
assert.strictEqual(options.port, undefined);
assert.strictEqual(options.defaultPort, undefined);
assert.strictEqual(options.path, undefined);
assert.strictEqual(options.method, undefined);
}).end();
});

View File

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

View File

@@ -0,0 +1,48 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const expected = {
'1.0': 'I AM THE WALRUS',
'1.1': ''
};
function test(httpVersion, callback) {
const server = net.createServer(function(conn) {
const reply = `HTTP/${httpVersion} 200 OK\r\n\r\n${expected[httpVersion]}`;
conn.end(reply);
});
server.listen(0, '127.0.0.1', common.mustCall(function() {
const options = {
host: '127.0.0.1',
port: this.address().port
};
const req = http.get(options, common.mustCall(function(res) {
let body = '';
res.on('data', function(data) {
body += data;
});
res.on('aborted', common.mustNotCall());
res.on('end', common.mustCall(function() {
assert.strictEqual(body, expected[httpVersion]);
server.close();
if (callback) process.nextTick(callback);
}));
}));
req.on('error', function(err) {
throw err;
});
}));
}
test('1.0', function() {
test('1.1');
});

View File

@@ -0,0 +1,174 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.writeHead(200); // Headers already sent
const headers = new globalThis.Headers({ foo: '1' });
assert.throws(() => {
res.setHeaders(headers);
}, {
code: 'ERR_HTTP_HEADERS_SENT'
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.foo, undefined);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
assert.throws(() => {
res.setHeaders(['foo', '1']);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders({ foo: '1' });
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(null);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(undefined);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders('test');
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(1);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.foo, undefined);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new globalThis.Headers({ foo: '1', bar: '2' });
res.setHeaders(headers);
res.writeHead(200);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new globalThis.Headers({ foo: '1', bar: '2' });
res.setHeaders(headers);
res.writeHead(200, ['foo', '3']);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '3'); // Override by writeHead
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Map([['foo', '1'], ['bar', '2']]);
res.setHeaders(headers);
res.writeHead(200);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Headers();
headers.append('Set-Cookie', 'a=b');
headers.append('Set-Cookie', 'c=d');
res.setHeaders(headers);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert(Array.isArray(res.headers['set-cookie']));
assert.strictEqual(res.headers['set-cookie'].length, 2);
assert.strictEqual(res.headers['set-cookie'][0], 'a=b');
assert.strictEqual(res.headers['set-cookie'][1], 'c=d');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Map();
headers.set('Set-Cookie', ['a=b', 'c=d']);
res.setHeaders(headers);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert(Array.isArray(res.headers['set-cookie']));
assert.strictEqual(res.headers['set-cookie'].length, 2);
assert.strictEqual(res.headers['set-cookie'][0], 'a=b');
assert.strictEqual(res.headers['set-cookie'][1], 'c=d');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

View File

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

View File

@@ -0,0 +1,80 @@
// 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';
// Verify that the HTTP server implementation handles multiple instances
// of the same header as per RFC2616: joining the handful of fields by ', '
// that support it, and dropping duplicates for other fields.
require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
assert.strictEqual(req.headers.accept, 'abc, def, ghijklmnopqrst');
assert.strictEqual(req.headers.host, 'foo');
assert.strictEqual(req.headers['www-authenticate'], 'foo, bar, baz');
assert.strictEqual(req.headers['proxy-authenticate'], 'foo, bar, baz');
assert.strictEqual(req.headers['x-foo'], 'bingo');
assert.strictEqual(req.headers['x-bar'], 'banjo, bango');
assert.strictEqual(req.headers['sec-websocket-protocol'], 'chat, share');
assert.strictEqual(req.headers['sec-websocket-extensions'],
'foo; 1, bar; 2, baz');
assert.strictEqual(req.headers.constructor, 'foo, bar, baz');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('EOF');
server.close();
});
server.listen(0, function() {
http.get({
host: 'localhost',
port: this.address().port,
path: '/',
headers: [
['accept', 'abc'],
['accept', 'def'],
['Accept', 'ghijklmnopqrst'],
['host', 'foo'],
['Host', 'bar'],
['hOst', 'baz'],
['www-authenticate', 'foo'],
['WWW-Authenticate', 'bar'],
['WWW-AUTHENTICATE', 'baz'],
['proxy-authenticate', 'foo'],
['Proxy-Authenticate', 'bar'],
['PROXY-AUTHENTICATE', 'baz'],
['x-foo', 'bingo'],
['x-bar', 'banjo'],
['x-bar', 'bango'],
['sec-websocket-protocol', 'chat'],
['sec-websocket-protocol', 'share'],
['sec-websocket-extensions', 'foo; 1'],
['sec-websocket-extensions', 'bar; 2'],
['sec-websocket-extensions', 'baz'],
['constructor', 'foo'],
['constructor', 'bar'],
['constructor', 'baz'],
]
});
});

View File

@@ -0,0 +1,107 @@
// 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';
// Verify that the HTTP server implementation handles multiple instances
// of the same header as per RFC2616: joining the handful of fields by ', '
// that support it, and dropping duplicates for other fields.
require('../common');
const assert = require('assert');
const http = require('http');
const multipleAllowed = [
'Accept',
'Accept-Charset',
'Accept-Encoding',
'Accept-Language',
'Cookie',
'DAV', // GH-2750
'Pragma', // GH-715
'Link', // GH-1187
'WWW-Authenticate', // GH-1083
'Proxy-Authenticate', // GH-4052
'Sec-Websocket-Extensions', // GH-2764
'Sec-Websocket-Protocol', // GH-2764
'Via', // GH-6660
// not a special case, just making sure it's parsed correctly
'X-Forwarded-For',
// Make sure that unspecified headers is treated as multiple
'Some-Random-Header',
'X-Some-Random-Header',
];
const multipleForbidden = [
'Content-Type',
'User-Agent',
'Referer',
'Host',
'Authorization',
'Proxy-Authorization',
'If-Modified-Since',
'If-Unmodified-Since',
'From',
'Location',
'Max-Forwards',
// Special case, tested differently
// 'Content-Length',
];
const server = http.createServer(function(req, res) {
for (const header of multipleForbidden) {
assert.strictEqual(req.headers[header.toLowerCase()], 'foo',
`header parsed incorrectly: ${header}`);
}
for (const header of multipleAllowed) {
const sep = (header.toLowerCase() === 'cookie' ? '; ' : ', ');
assert.strictEqual(req.headers[header.toLowerCase()], `foo${sep}bar`,
`header parsed incorrectly: ${header}`);
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('EOF');
server.close();
});
function makeHeader(value) {
return function(header) {
return [header, value];
};
}
const headers = []
.concat(multipleAllowed.map(makeHeader('foo')))
.concat(multipleForbidden.map(makeHeader('foo')))
.concat(multipleAllowed.map(makeHeader('bar')))
.concat(multipleForbidden.map(makeHeader('bar')));
server.listen(0, function() {
http.get({
host: 'localhost',
port: this.address().port,
path: '/',
headers: headers,
});
});

View File

@@ -0,0 +1,51 @@
// 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 server = http.createServer(common.mustCall(function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
}));
server.listen(0, function() {
function callback() {}
const req = http.request({
port: this.address().port,
path: '/',
agent: false
}, function(res) {
req.clearTimeout(callback);
res.on('end', common.mustCall(function() {
server.close();
}));
res.resume();
});
// Overflow signed int32
req.setTimeout(0xffffffff, callback);
req.end();
});

View File

@@ -0,0 +1,78 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
const http = require('http');
// `wget` sends an HTTP/1.0 request with Connection: Keep-Alive
//
// Sending back a chunked response to an HTTP/1.0 client would be wrong,
// so what has to happen in this case is that the connection is closed
// by the server after the entity body if the Content-Length was not
// sent.
//
// If the Content-Length was sent, we can probably safely honor the
// keep-alive request, even though HTTP 1.0 doesn't say that the
// connection can be kept open. Presumably any client sending this
// header knows that it is extending HTTP/1.0 and can handle the
// response. We don't test that here however, just that if the
// content-length is not provided, that the connection is in fact
// closed.
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('hello ');
res.write('world\n');
res.end();
});
server.listen(0);
server.on('listening', common.mustCall(() => {
const c = net.createConnection(server.address().port);
let server_response = '';
c.setEncoding('utf8');
c.on('connect', () => {
c.write('GET / HTTP/1.0\r\n' +
'Connection: Keep-Alive\r\n\r\n');
});
c.on('data', (chunk) => {
console.log(chunk);
server_response += chunk;
});
c.on('end', common.mustCall(() => {
assert.ok(server_response.includes('hello '));
assert.ok(server_response.includes('world\n'));
console.log('got end');
c.end();
}));
c.on('close', common.mustCall(() => {
console.log('got close');
server.close();
}));
}));

View File

@@ -0,0 +1,103 @@
// 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');
// Verify that ServerResponse.writeHead() works as setHeader.
// Issue 5036 on github.
const s = http.createServer(common.mustCall((req, res) => {
res.setHeader('test', '1');
// toLowerCase() is used on the name argument, so it must be a string.
// Non-String header names should throw
assert.throws(
() => res.setHeader(0xf00, 'bar'),
{
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
}
);
// Undefined value should throw, via 979d0ca8
assert.throws(
() => res.setHeader('foo', undefined),
{
code: 'ERR_HTTP_INVALID_HEADER_VALUE',
name: 'TypeError',
}
);
assert.throws(() => {
res.writeHead(200, ['invalid', 'headers', 'args']);
}, {
code: 'ERR_INVALID_ARG_VALUE'
});
res.writeHead(200, { Test: '2' });
assert.throws(() => {
res.writeHead(100, {});
}, {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
});
res.end();
}));
s.listen(0, common.mustCall(runTest));
function runTest() {
http.get({ port: this.address().port }, common.mustCall((response) => {
response.on('end', common.mustCall(() => {
assert.strictEqual(response.headers.test, '2');
s.close();
}));
response.resume();
}));
}
{
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(220, [ 'test', '1' ]); // 220 is not a standard status code
assert.strictEqual(res.statusMessage, 'unknown');
assert.throws(() => res.writeHead(200, [ 'test2', '2' ]), {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.test, '1');
assert.strictEqual('test2' in res.headers, false);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

View File

@@ -0,0 +1,27 @@
'use strict';
require('../common');
const { IncomingMessage } = require('http');
const assert = require('assert');
// Headers setter function set a header correctly
{
const im = new IncomingMessage();
im.headers = { key: 'value' };
assert.deepStrictEqual(im.headers, { key: 'value' });
}
// Trailers setter function set a header correctly
{
const im = new IncomingMessage();
im.trailers = { key: 'value' };
assert.deepStrictEqual(im.trailers, { key: 'value' });
}
// _addHeaderLines function set a header correctly
{
const im = new IncomingMessage();
im.headers = { key1: 'value1' };
im._addHeaderLines(['key2', 'value2'], 2);
assert.deepStrictEqual(im.headers, { key1: 'value1', key2: 'value2' });
}

View File

@@ -1331,12 +1331,12 @@ describe("Response", () => {
it("should work with bigint", () => {
var r = new Response("hello status", { status: 200n });
expect(r.status).toBe(200);
r = new Response("hello status", { status: 599n });
expect(r.status).toBe(599);
r = new Response("hello status", { status: 999n });
expect(r.status).toBe(999);
r = new Response("hello status", { status: BigInt(200) });
expect(r.status).toBe(200);
r = new Response("hello status", { status: BigInt(599) });
expect(r.status).toBe(599);
r = new Response("hello status", { status: BigInt(999) });
expect(r.status).toBe(999);
});
testBlobInterface(data => new Response(data), true);
});