fix(node/http): test-http-outgoing-proto

This commit is contained in:
Don Isaac
2025-03-20 16:37:38 -07:00
parent f1cd5abfaa
commit f208be2d52
2 changed files with 201 additions and 6 deletions

View File

@@ -41,6 +41,7 @@ const eofInProgress = Symbol("eofInProgress");
const fakeSocketSymbol = Symbol("fakeSocket");
const firstWriteSymbol = Symbol("firstWrite");
const headersSymbol = Symbol("headers");
const kUniqueHeaders = Symbol("kUniqueHeaders");
const isTlsSymbol = Symbol("is_tls");
const kClearTimeout = Symbol("kClearTimeout");
const kfakeSocket = Symbol("kfakeSocket");
@@ -66,7 +67,7 @@ const kEmptyObject = Object.freeze(Object.create(null));
const { kDeprecatedReplySymbol } = require("internal/http");
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
const { isTypedArray, isArrayBuffer } = require("node:util/types");
const { isTypedArray, isArrayBuffer, isUint8Array } = require("node:util/types");
const { Duplex, Readable, Stream } = require("node:stream");
const { isPrimary } = require("internal/cluster/isPrimary");
const { kAutoDestroyed } = require("internal/shared");
@@ -74,6 +75,7 @@ const { urlToHttpOptions } = require("internal/url");
const { validateFunction, checkIsHttpToken, validateLinkHeaderValue, validateObject } = require("internal/validators");
const { isIPv6 } = require("node:net");
const ObjectKeys = Object.keys;
const ArrayIsArray = Array.isArray;
const {
getHeader,
@@ -130,13 +132,17 @@ function checkInvalidHeaderChar(val: string) {
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
}
const validateHeaderName = (name, label?) => {
function validateHeaderName(name: unknown, label?: string): asserts name is string {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
throw $ERR_INVALID_HTTP_TOKEN(label || "Header name", name);
}
};
}
const validateHeaderValue = (name, value) => {
/**
* @param name header name. used in error messages
* @param value header value to validate
*/
const validateHeaderValue = (name: string, value: unknown) => {
if (value === undefined) {
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
@@ -1627,6 +1633,12 @@ const OutgoingMessagePrototype = {
encoding = undefined;
}
hasServerResponseFinished(this, chunk, callback);
if (chunk === null) {
throw $ERR_STREAM_NULL_VALUES();
} else if (typeof chunk !== "string" && !isUint8Array(chunk)) {
throw $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "Uint8Array"], chunk);
}
if (chunk) {
const len = Buffer.byteLength(chunk, encoding || (typeof chunk === "string" ? "utf8" : "buffer"));
if (len > 0) {
@@ -1634,6 +1646,7 @@ const OutgoingMessagePrototype = {
this.outputData.push(chunk);
}
}
return this.writableHighWaterMark >= this.outputSize;
},
@@ -1664,8 +1677,12 @@ const OutgoingMessagePrototype = {
headers.delete(name);
},
setHeader(name, value) {
setHeader(name: string, value: unknown) {
if (this._header) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
setHeader(headers, name, value);
return this;
@@ -1687,7 +1704,48 @@ const OutgoingMessagePrototype = {
},
addTrailers(headers) {
throw new Error("not implemented");
this._trailer = "";
const keys = ObjectKeys(headers);
const isArray = ArrayIsArray(headers);
for (let i = 0; i < keys.length; i++) {
let field, value;
const key = keys[i];
if (isArray) {
const header = headers[key];
field = header[0];
value = header[1];
} else {
field = key;
value = headers[key];
}
validateHeaderName(field, "Trailer name");
// Check if the field must be sent several times
const isArrayValue = ArrayIsArray(value);
if (
isArrayValue &&
value.length > 1 &&
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase()))
) {
for (let j = 0, l = value.length; j < l; j++) {
if (checkInvalidHeaderChar(value[j])) {
$debug('Trailer "%s"[%d] contains invalid characters', field, j);
throw $ERR_INVALID_CHAR("trailer content", field);
}
this._trailer += field + ": " + value[j] + "\r\n";
}
} else {
if (isArrayValue) {
value = value.join("; ");
}
if (checkInvalidHeaderChar(value)) {
$debug('Trailer "%s" contains invalid characters', field);
throw $ERR_INVALID_CHAR("trailer content", field);
}
this._trailer += field + ": " + value + "\r\n";
}
}
},
setTimeout(msecs, callback) {
@@ -3120,6 +3178,7 @@ function ClientRequest(input, options, cb) {
}
// this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
this[kUniqueHeaders] = new Set();
const { signal: _signal, ...optsWithoutSignal } = options;
this[kOptions] = optsWithoutSignal;

View File

@@ -0,0 +1,136 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const OutgoingMessage = http.OutgoingMessage;
const ClientRequest = http.ClientRequest;
const ServerResponse = http.ServerResponse;
assert.strictEqual(
typeof ClientRequest.prototype._implicitHeader, 'function');
assert.strictEqual(
typeof ServerResponse.prototype._implicitHeader, 'function');
// validateHeader
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader();
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["undefined"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('test');
}, {
code: 'ERR_HTTP_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "test"'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader(404);
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["404"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader.call({ _header: 'test' }, 'test', 'value');
}, {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
message: 'Cannot set headers after they are sent to the client'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('200', 'あ');
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: 'Invalid character in header content ["200"]'
});
// // write
// {
// const outgoingMessage = new OutgoingMessage();
// assert.throws(
// () => {
// outgoingMessage.write('');
// },
// {
// code: 'ERR_METHOD_NOT_IMPLEMENTED',
// name: 'Error',
// message: 'The _implicitHeader() method is not implemented'
// }
// );
// }
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array.'
+ ' Received undefined'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, 1);
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received type number (1)'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, null);
}, {
code: 'ERR_STREAM_NULL_VALUES',
name: 'TypeError'
});
// addTrailers()
// The `Error` comes from the JavaScript engine so confirm that it is a
// `TypeError` but do not check the message. It will be different in different
// JavaScript engines.
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers();
}, TypeError);
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 'あ': 'value' });
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Trailer name must be a valid HTTP token ["あ"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 404: 'あ' });
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: 'Invalid character in trailer content ["404"]'
});
{
const outgoingMessage = new OutgoingMessage();
assert.strictEqual(outgoingMessage.destroyed, false);
outgoingMessage.destroy();
assert.strictEqual(outgoingMessage.destroyed, true);
}