diff --git a/ext/node/polyfills/_http_outgoing.ts b/ext/node/polyfills/_http_outgoing.ts index a6edc1144f..3c253f5a6c 100644 --- a/ext/node/polyfills/_http_outgoing.ts +++ b/ext/node/polyfills/_http_outgoing.ts @@ -60,690 +60,663 @@ const nop = () => {}; const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i; -export class OutgoingMessage extends Stream { - // deno-lint-ignore no-explicit-any - outputData: any[]; - outputSize: number; - writable: boolean; - destroyed: boolean; - - _last: boolean; - chunkedEncoding: boolean; - shouldKeepAlive: boolean; - maxRequestsOnConnectionReached: boolean; - _defaultKeepAlive: boolean; - useChunkedEncodingByDefault: boolean; - sendDate: boolean; - _removedConnection: boolean; - _removedContLen: boolean; - _removedTE: boolean; - - _contentLength: number | null; - _hasBody: boolean; - _trailer: string; - [kNeedDrain]: boolean; - - finished: boolean; - _headerSent: boolean; - [kCorked]: number; - _closed: boolean; - - // TODO(crowlKats): use it - socket: null; - _header: string | null; - [kOutHeaders]: null | Record; - - _keepAliveTimeout: number; - _onPendingData: () => void; - - constructor() { - super(); - - // Queue that holds all currently pending data, until the response will be - // assigned to the socket (until it will its turn in the HTTP pipeline). - this.outputData = []; - - // `outputSize` is an approximate measure of how much data is queued on this - // response. `_onPendingData` will be invoked to update similar global - // per-connection counter. That counter will be used to pause/unpause the - // TCP socket and HTTP Parser and thus handle the backpressure. - this.outputSize = 0; - - this.writable = true; - this.destroyed = false; - - this._last = false; - this.chunkedEncoding = false; - this.shouldKeepAlive = true; - this.maxRequestsOnConnectionReached = false; - this._defaultKeepAlive = true; - this.useChunkedEncodingByDefault = true; - this.sendDate = false; - this._removedConnection = false; - this._removedContLen = false; - this._removedTE = false; - - this._contentLength = null; - this._hasBody = true; - this._trailer = ""; - this[kNeedDrain] = false; - - this.finished = false; - this._headerSent = false; - this[kCorked] = 0; - this._closed = false; - - this.socket = null; - this._header = null; - this[kOutHeaders] = null; - - this._keepAliveTimeout = 0; - - this._onPendingData = nop; - } - - get writableFinished() { - return ( - this.finished && - this.outputSize === 0 && - (!this.socket || this.socket.writableLength === 0) - ); - } - - get writableObjectMode() { - return false; - } - - get writableLength() { - return this.outputSize + (this.socket ? this.socket.writableLength : 0); - } - - get writableHighWaterMark() { - return this.socket ? this.socket.writableHighWaterMark : HIGH_WATER_MARK; - } - - get writableCorked() { - const corked = this.socket ? this.socket.writableCorked : 0; - return corked + this[kCorked]; - } - - get connection() { - return this.socket; - } - - set connection(val) { - this.socket = val; - } - - get writableEnded() { - return this.finished; - } - - get writableNeedDrain() { - return !this.destroyed && !this.finished && this[kNeedDrain]; - } - - cork() { - if (this.socket) { - this.socket.cork(); - } else { - this[kCorked]++; - } - } - - uncork() { - if (this.socket) { - this.socket.uncork(); - } else if (this[kCorked]) { - this[kCorked]--; - } - } - - setTimeout(msecs: number, callback?: (...args: unknown[]) => void) { - if (callback) { - this.on("timeout", callback); - } - - if (!this.socket) { - // deno-lint-ignore no-explicit-any - this.once("socket", function socketSetTimeoutOnConnect(socket: any) { - socket.setTimeout(msecs); - }); - } else { - this.socket.setTimeout(msecs); - } - return this; - } - - // 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. - destroy(error: unknown) { - if (this.destroyed) { - return this; - } - this.destroyed = true; - - if (this.socket) { - this.socket.destroy(error); - } else { - // deno-lint-ignore no-explicit-any - this.once("socket", function socketDestroyOnConnect(socket: any) { - socket.destroy(error); - }); - } - - return this; - } - - setHeader(name: string, value: string) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT("set"); - } - validateHeaderName(name); - validateHeaderValue(name, value); - - let headers = this[kOutHeaders]; - if (headers === null) { - this[kOutHeaders] = headers = Object.create(null); - } - - name = name.toString(); - headers[name.toLowerCase()] = [name, String(value)]; - return this; - } - - appendHeader(name, value) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT("append"); - } - validateHeaderName(name); - validateHeaderValue(name, value); - - name = name.toString(); - - const field = name.toLowerCase(); - const headers = this[kOutHeaders]; - if (headers === null || !headers[field]) { - return this.setHeader(name, value); - } - - // Prepare the field for appending, if required - if (!Array.isArray(headers[field][1])) { - headers[field][1] = [headers[field][1]]; - } - - const existingValues = headers[field][1]; - if (Array.isArray(value)) { - for (let i = 0, length = value.length; i < length; i++) { - existingValues.push(value[i].toString()); - } - } else { - existingValues.push(value.toString()); - } - - return this; - } - - // Returns a shallow copy of the current outgoing headers. - getHeaders() { - const headers = this[kOutHeaders]; - const ret = Object.create(null); - if (headers) { - const keys = Object.keys(headers); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const val = headers[key][1]; - ret[key] = val; - } - } - return ret; - } - - hasHeader(name: string) { - validateString(name, "name"); - return this[kOutHeaders] !== null && - !!this[kOutHeaders][name.toLowerCase()]; - } - - removeHeader(name: string) { - validateString(name, "name"); - - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT("remove"); - } - - const key = name.toLowerCase(); - - switch (key) { - case "connection": - this._removedConnection = true; - break; - case "content-length": - this._removedContLen = true; - break; - case "transfer-encoding": - this._removedTE = true; - break; - case "date": - this.sendDate = false; - break; - } - - if (this[kOutHeaders] !== null) { - delete this[kOutHeaders][key]; - } - } - - getHeader(name: string) { - validateString(name, "name"); - - const headers = this[kOutHeaders]; - if (headers === null) { - return; - } - - const entry = headers[name.toLowerCase()]; - return entry && entry[1]; - } - - // Returns an array of the names of the current outgoing headers. - getHeaderNames() { - return this[kOutHeaders] !== null ? Object.keys(this[kOutHeaders]) : []; - } - - // Returns an array of the names of the current outgoing raw headers. - getRawHeaderNames() { - const headersMap = this[kOutHeaders]; - if (headersMap === null) return []; - - const values = Object.values(headersMap); - const headers = Array(values.length); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0, l = values.length; i < l; i++) { - // deno-lint-ignore no-explicit-any - headers[i] = (values as any)[i][0]; - } - - return headers; - } - - write( - chunk: string | Uint8Array | Buffer, - encoding: string | null, - callback: () => void, - ): boolean { - if (typeof encoding === "function") { - callback = encoding; - encoding = null; - } - return this.write_(chunk, encoding, callback, false); - } - - write_( - chunk: string | Uint8Array | Buffer, - encoding: string | null, - callback: () => void, - fromEnd: boolean, - ): boolean { - // Ignore lint to keep the code as similar to Nodejs as possible - // deno-lint-ignore no-this-alias - const msg = this; - - if (chunk === null) { - throw new ERR_STREAM_NULL_VALUES(); - } else if (typeof chunk !== "string" && !isUint8Array(chunk)) { - throw new ERR_INVALID_ARG_TYPE( - "chunk", - ["string", "Buffer", "Uint8Array"], - chunk, - ); - } - - let len: number; - - if (!msg._header) { - if (fromEnd) { - len ??= typeof chunk === "string" - ? Buffer.byteLength(chunk, encoding) - : chunk.byteLength; - msg._contentLength = len; - } - msg._implicitHeader(); - } - - return msg._send(chunk, encoding, callback); - } - - // deno-lint-ignore no-explicit-any - addTrailers(_headers: any) { - // TODO(crowlKats): finish it - notImplemented("OutgoingMessage.addTrailers"); - } - - // deno-lint-ignore no-explicit-any - end(_chunk: any, _encoding: any, _callback: any) { - notImplemented("OutgoingMessage.end"); - } - - flushHeaders() { - if (!this._header) { - this._implicitHeader(); - } - - // Force-flush the headers. - this._send(""); - } - - pipe() { - // OutgoingMessage should be write-only. Piping from it is disabled. - this.emit("error", new ERR_STREAM_CANNOT_PIPE()); - } - - _implicitHeader() { - throw new ERR_METHOD_NOT_IMPLEMENTED("_implicitHeader()"); - } - - _finish() { - assert(this.socket); - this.emit("prefinish"); - } - - // This logic is probably a bit confusing. Let me explain a bit: - // - // In both HTTP servers and clients it is possible to queue up several - // outgoing messages. This is easiest to imagine in the case of a client. - // Take the following situation: - // - // req1 = client.request('GET', '/'); - // req2 = client.request('POST', '/'); - // - // When the user does - // - // req2.write('hello world\n'); - // - // it's possible that the first request has not been completely flushed to - // the socket yet. Thus the outgoing messages need to be prepared to queue - // up data internally before sending it on further to the socket's queue. - // - // This function, outgoingFlush(), is called by both the Server and Client - // to attempt to flush any pending messages out to the socket. - _flush() { - const socket = this.socket; - - if (socket && socket.writable) { - // There might be remaining data in this.output; write it out - const ret = this._flushOutput(socket); - - if (this.finished) { - // This is a queue to the server or client to bring in the next this. - this._finish(); - } else if (ret && this[kNeedDrain]) { - this[kNeedDrain] = false; - this.emit("drain"); - } - } - } - - _flushOutput(socket: Socket) { - while (this[kCorked]) { - this[kCorked]--; - socket.cork(); - } - - const outputLength = this.outputData.length; - if (outputLength <= 0) { - return undefined; - } - - const outputData = this.outputData; - socket.cork(); - let ret; - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < outputLength; i++) { - const { data, encoding, callback } = outputData[i]; - ret = socket.write(data, encoding, callback); - } - socket.uncork(); - - this.outputData = []; - this._onPendingData(-this.outputSize); - this.outputSize = 0; - - return ret; - } - - // deno-lint-ignore no-explicit-any - _send(data: any, encoding?: string | null, callback?: () => void) { - if (!this._headerSent && this._header !== null) { - this._writeHeader(); - this._headerSent = true; - } - return this._writeRaw(data, encoding, callback); - } - - _writeHeader() { - throw new ERR_METHOD_NOT_IMPLEMENTED("_writeHeader()"); - } - - _writeRaw( - // deno-lint-ignore no-explicit-any - data: any, - encoding?: string | null, - callback?: () => void, - ) { - if (typeof data === "string") { - data = Buffer.from(data, encoding); - } - if (data instanceof Buffer) { - data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - } - if (data.buffer.byteLength > 0) { - this._bodyWriter.write(data).then(() => { - callback?.(); - this.emit("drain"); - }).catch((e) => { - this._requestSendError = e; - }); - } - return false; - } - - _renderHeaders() { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT("render"); - } - - const headersMap = this[kOutHeaders]; - // deno-lint-ignore no-explicit-any - const headers: any = {}; - - if (headersMap !== null) { - const keys = Object.keys(headersMap); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0, l = keys.length; i < l; i++) { - const key = keys[i]; - headers[headersMap[key][0]] = headersMap[key][1]; - } - } - return headers; - } - - _storeHeader(firstLine: string, _headers: never) { - // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' - // in the case of response it is: 'HTTP/1.1 200 OK\r\n' - const state = { - connection: false, - contLen: false, - te: false, - date: false, - expect: false, - trailer: false, - header: firstLine, - }; - - const headers = this[kOutHeaders]; - if (headers) { - // headers is null-prototype object, so ignore the guard lint - // deno-lint-ignore guard-for-in - for (const key in headers) { - const entry = headers[key]; - this._matchHeader(state, entry[0], entry[1]); - } - } - - // Date header - if (this.sendDate && !state.date) { - this.setHeader("Date", utcDate()); - } - - // Force the connection to close when the response is a 204 No Content or - // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" - // header. - // - // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but - // node.js used to send out a zero chunk anyway to accommodate clients - // that don't have special handling for those responses. - // - // It was pointed out that this might confuse reverse proxies to the point - // of creating security liabilities, so suppress the zero chunk and force - // the connection to close. - if ( - this.chunkedEncoding && (this.statusCode === 204 || - this.statusCode === 304) - ) { - debug( - this.statusCode + " response should not use chunked encoding," + - " closing connection.", - ); - this.chunkedEncoding = false; - this.shouldKeepAlive = false; - } - - // TODO(osddeitf): this depends on agent and underlying socket - // keep-alive logic - // if (this._removedConnection) { - // this._last = true; - // this.shouldKeepAlive = false; - // } else if (!state.connection) { - // const shouldSendKeepAlive = this.shouldKeepAlive && - // (state.contLen || this.useChunkedEncodingByDefault || this.agent); - // if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) { - // this.setHeader('Connection', 'close'); - // } else if (shouldSendKeepAlive) { - // this.setHeader('Connection', 'keep-alive'); - // if (this._keepAliveTimeout && this._defaultKeepAlive) { - // const timeoutSeconds = Math.floor(this._keepAliveTimeout / 1000); - // let max = ''; - // if (~~this._maxRequestsPerSocket > 0) { - // max = `, max=${this._maxRequestsPerSocket}`; - // } - // this.setHeader('Keep-Alive', `timeout=${timeoutSeconds}${max}`); - // } - // } else { - // this._last = true; - // this.setHeader('Connection', 'close'); - // } - // } - - if (!state.contLen && !state.te) { - if (!this._hasBody) { - // Make sure we don't end the 0\r\n\r\n at the end of the message. - this.chunkedEncoding = false; - } else if (!this.useChunkedEncodingByDefault) { - this._last = true; - } else if ( - !state.trailer && - !this._removedContLen && - typeof this._contentLength === "number" - ) { - this.setHeader("Content-Length", this._contentLength); - } else if (!this._removedTE) { - this.setHeader("Transfer-Encoding", "chunked"); - this.chunkedEncoding = true; - } else { - // We should only be able to get here if both Content-Length and - // Transfer-Encoding are removed by the user. - // See: test/parallel/test-http-remove-header-stays-removed.js - debug("Both Content-Length and Transfer-Encoding are removed"); - } - } - - // Test non-chunked message does not have trailer header set, - // message will be terminated by the first empty line after the - // header fields, regardless of the header fields present in the - // message, and thus cannot contain a message body or 'trailers'. - if (this.chunkedEncoding !== true && state.trailer) { - throw new ERR_HTTP_TRAILER_INVALID(); - } - - const { header } = state; - this._header = header + "\r\n"; - this._headerSent = false; - - // Wait until the first body chunk, or close(), is sent to flush, - // UNLESS we're sending Expect: 100-continue. - if (state.expect) this._send(""); - } - - _matchHeader( - // deno-lint-ignore no-explicit-any - state: any, - field: string, - // deno-lint-ignore no-explicit-any - value: any, - ) { - // Ignore lint to keep the code as similar to Nodejs as possible - // deno-lint-ignore no-this-alias - const self = this; - if (field.length < 4 || field.length > 17) { - return; - } - field = field.toLowerCase(); - switch (field) { - case "connection": - state.connection = true; - self._removedConnection = false; - if (RE_CONN_CLOSE.exec(value) !== null) { - self._last = true; - } else { - self.shouldKeepAlive = true; - } - break; - case "transfer-encoding": - state.te = true; - self._removedTE = false; - if (RE_TE_CHUNKED.exec(value) !== null) { - self.chunkedEncoding = true; - } - break; - case "content-length": - state.contLen = true; - self._contentLength = value; - self._removedContLen = false; - break; - case "date": - case "expect": - case "trailer": - state[field] = true; - break; - case "keep-alive": - self._defaultKeepAlive = false; - break; - } - } - - // deno-lint-ignore no-explicit-any - [EE.captureRejectionSymbol](err: any, _event: any) { - this.destroy(err); - } +export function OutgoingMessage() { + Stream.call(this); + + // Queue that holds all currently pending data, until the response will be + // assigned to the socket (until it will its turn in the HTTP pipeline). + this.outputData = []; + + // `outputSize` is an approximate measure of how much data is queued on this + // response. `_onPendingData` will be invoked to update similar global + // per-connection counter. That counter will be used to pause/unpause the + // TCP socket and HTTP Parser and thus handle the backpressure. + this.outputSize = 0; + + this.writable = true; + this.destroyed = false; + + this._last = false; + this.chunkedEncoding = false; + this.shouldKeepAlive = true; + this.maxRequestsOnConnectionReached = false; + this._defaultKeepAlive = true; + this.useChunkedEncodingByDefault = true; + this.sendDate = false; + this._removedConnection = false; + this._removedContLen = false; + this._removedTE = false; + + this._contentLength = null; + this._hasBody = true; + this._trailer = ""; + this[kNeedDrain] = false; + + this.finished = false; + this._headerSent = false; + this[kCorked] = 0; + this._closed = false; + + this.socket = null; + this._header = null; + this[kOutHeaders] = null; + + this._keepAliveTimeout = 0; + + this._onPendingData = nop; + + this._bodyWriter = null; } +Object.setPrototypeOf(OutgoingMessage.prototype, Stream.prototype); +Object.setPrototypeOf(OutgoingMessage, Stream); + +Object.defineProperties( + OutgoingMessage.prototype, + Object.getOwnPropertyDescriptors({ + get writableFinished() { + return ( + this.finished && + this.outputSize === 0 && + (!this.socket || this.socket.writableLength === 0) + ); + }, + + get writableObjectMode() { + return false; + }, + + get writableLength() { + return this.outputSize + (this.socket ? this.socket.writableLength : 0); + }, + + get writableHighWaterMark() { + return this.socket ? this.socket.writableHighWaterMark : HIGH_WATER_MARK; + }, + + get writableCorked() { + const corked = this.socket ? this.socket.writableCorked : 0; + return corked + this[kCorked]; + }, + + get connection() { + return this.socket; + }, + + set connection(val) { + this.socket = val; + }, + + get writableEnded() { + return this.finished; + }, + + get writableNeedDrain() { + return !this.destroyed && !this.finished && this[kNeedDrain]; + }, + + cork() { + if (this.socket) { + this.socket.cork(); + } else { + this[kCorked]++; + } + }, + + uncork() { + if (this.socket) { + this.socket.uncork(); + } else if (this[kCorked]) { + this[kCorked]--; + } + }, + + setTimeout(msecs: number, callback?: (...args: unknown[]) => void) { + if (callback) { + this.on("timeout", callback); + } + + if (!this.socket) { + // deno-lint-ignore no-explicit-any + this.once("socket", function socketSetTimeoutOnConnect(socket: any) { + socket.setTimeout(msecs); + }); + } else { + this.socket.setTimeout(msecs); + } + return this; + }, + + // 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. + destroy(error: unknown) { + if (this.destroyed) { + return this; + } + this.destroyed = true; + + if (this.socket) { + this.socket.destroy(error); + } else { + // deno-lint-ignore no-explicit-any + this.once("socket", function socketDestroyOnConnect(socket: any) { + socket.destroy(error); + }); + } + + return this; + }, + + setHeader(name: string, value: string) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT("set"); + } + validateHeaderName(name); + validateHeaderValue(name, value); + + let headers = this[kOutHeaders]; + if (headers === null) { + this[kOutHeaders] = headers = Object.create(null); + } + + name = name.toString(); + headers[name.toLowerCase()] = [name, String(value)]; + return this; + }, + + appendHeader(name, value) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT("append"); + } + validateHeaderName(name); + validateHeaderValue(name, value); + + name = name.toString(); + + const field = name.toLowerCase(); + const headers = this[kOutHeaders]; + if (headers === null || !headers[field]) { + return this.setHeader(name, value); + } + + // Prepare the field for appending, if required + if (!Array.isArray(headers[field][1])) { + headers[field][1] = [headers[field][1]]; + } + + const existingValues = headers[field][1]; + if (Array.isArray(value)) { + for (let i = 0, length = value.length; i < length; i++) { + existingValues.push(value[i].toString()); + } + } else { + existingValues.push(value.toString()); + } + + return this; + }, + + // Returns a shallow copy of the current outgoing headers. + getHeaders() { + const headers = this[kOutHeaders]; + const ret = Object.create(null); + if (headers) { + const keys = Object.keys(headers); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + const val = headers[key][1]; + ret[key] = val; + } + } + return ret; + }, + + hasHeader(name: string) { + validateString(name, "name"); + return this[kOutHeaders] !== null && + !!this[kOutHeaders][name.toLowerCase()]; + }, + + removeHeader(name: string) { + validateString(name, "name"); + + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT("remove"); + } + + const key = name.toLowerCase(); + + switch (key) { + case "connection": + this._removedConnection = true; + break; + case "content-length": + this._removedContLen = true; + break; + case "transfer-encoding": + this._removedTE = true; + break; + case "date": + this.sendDate = false; + break; + } + + if (this[kOutHeaders] !== null) { + delete this[kOutHeaders][key]; + } + }, + + getHeader(name: string) { + validateString(name, "name"); + + const headers = this[kOutHeaders]; + if (headers === null) { + return; + } + + const entry = headers[name.toLowerCase()]; + return entry && entry[1]; + }, + + // Returns an array of the names of the current outgoing headers. + getHeaderNames() { + return this[kOutHeaders] !== null ? Object.keys(this[kOutHeaders]) : []; + }, + + // Returns an array of the names of the current outgoing raw headers. + getRawHeaderNames() { + const headersMap = this[kOutHeaders]; + if (headersMap === null) return []; + + const values = Object.values(headersMap); + const headers = Array(values.length); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0, l = values.length; i < l; i++) { + // deno-lint-ignore no-explicit-any + headers[i] = (values as any)[i][0]; + } + + return headers; + }, + + write( + chunk: string | Uint8Array | Buffer, + encoding: string | null, + callback: () => void, + ): boolean { + if (typeof encoding === "function") { + callback = encoding; + encoding = null; + } + return this.write_(chunk, encoding, callback, false); + }, + + write_( + chunk: string | Uint8Array | Buffer, + encoding: string | null, + callback: () => void, + fromEnd: boolean, + ): boolean { + // Ignore lint to keep the code as similar to Nodejs as possible + // deno-lint-ignore no-this-alias + const msg = this; + + if (chunk === null) { + throw new ERR_STREAM_NULL_VALUES(); + } else if (typeof chunk !== "string" && !isUint8Array(chunk)) { + throw new ERR_INVALID_ARG_TYPE( + "chunk", + ["string", "Buffer", "Uint8Array"], + chunk, + ); + } + + let len: number; + + if (!msg._header) { + if (fromEnd) { + len ??= typeof chunk === "string" + ? Buffer.byteLength(chunk, encoding) + : chunk.byteLength; + msg._contentLength = len; + } + msg._implicitHeader(); + } + + return msg._send(chunk, encoding, callback); + }, + + // deno-lint-ignore no-explicit-any + addTrailers(_headers: any) { + // TODO(crowlKats): finish it + notImplemented("OutgoingMessage.addTrailers"); + }, + + // deno-lint-ignore no-explicit-any + end(_chunk: any, _encoding: any, _callback: any) { + notImplemented("OutgoingMessage.end"); + }, + + flushHeaders() { + if (!this._header) { + this._implicitHeader(); + } + + // Force-flush the headers. + this._send(""); + }, + + pipe() { + // OutgoingMessage should be write-only. Piping from it is disabled. + this.emit("error", new ERR_STREAM_CANNOT_PIPE()); + }, + + _implicitHeader() { + throw new ERR_METHOD_NOT_IMPLEMENTED("_implicitHeader()"); + }, + + _finish() { + assert(this.socket); + this.emit("prefinish"); + }, + + // This logic is probably a bit confusing. Let me explain a bit: + // + // In both HTTP servers and clients it is possible to queue up several + // outgoing messages. This is easiest to imagine in the case of a client. + // Take the following situation: + // + // req1 = client.request('GET', '/'); + // req2 = client.request('POST', '/'); + // + // When the user does + // + // req2.write('hello world\n'); + // + // it's possible that the first request has not been completely flushed to + // the socket yet. Thus the outgoing messages need to be prepared to queue + // up data internally before sending it on further to the socket's queue. + // + // This function, outgoingFlush(), is called by both the Server and Client + // to attempt to flush any pending messages out to the socket. + _flush() { + const socket = this.socket; + + if (socket && socket.writable) { + // There might be remaining data in this.output; write it out + const ret = this._flushOutput(socket); + + if (this.finished) { + // This is a queue to the server or client to bring in the next this. + this._finish(); + } else if (ret && this[kNeedDrain]) { + this[kNeedDrain] = false; + this.emit("drain"); + } + } + }, + + _flushOutput(socket: Socket) { + while (this[kCorked]) { + this[kCorked]--; + socket.cork(); + } + + const outputLength = this.outputData.length; + if (outputLength <= 0) { + return undefined; + } + + const outputData = this.outputData; + socket.cork(); + let ret; + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < outputLength; i++) { + const { data, encoding, callback } = outputData[i]; + ret = socket.write(data, encoding, callback); + } + socket.uncork(); + + this.outputData = []; + this._onPendingData(-this.outputSize); + this.outputSize = 0; + + return ret; + }, + + // deno-lint-ignore no-explicit-any + _send(data: any, encoding?: string | null, callback?: () => void) { + if (!this._headerSent && this._header !== null) { + this._writeHeader(); + this._headerSent = true; + } + return this._writeRaw(data, encoding, callback); + }, + + _writeHeader() { + throw new ERR_METHOD_NOT_IMPLEMENTED("_writeHeader()"); + }, + + _writeRaw( + // deno-lint-ignore no-explicit-any + data: any, + encoding?: string | null, + callback?: () => void, + ) { + if (typeof data === "string") { + data = Buffer.from(data, encoding); + } + if (data instanceof Buffer) { + data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } + if (data.buffer.byteLength > 0) { + this._bodyWriter.write(data).then(() => { + callback?.(); + this.emit("drain"); + }).catch((e) => { + this._requestSendError = e; + }); + } + return false; + }, + + _renderHeaders() { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT("render"); + } + + const headersMap = this[kOutHeaders]; + // deno-lint-ignore no-explicit-any + const headers: any = {}; + + if (headersMap !== null) { + const keys = Object.keys(headersMap); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + headers[headersMap[key][0]] = headersMap[key][1]; + } + } + return headers; + }, + + _storeHeader(firstLine: string, _headers: never) { + // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' + // in the case of response it is: 'HTTP/1.1 200 OK\r\n' + const state = { + connection: false, + contLen: false, + te: false, + date: false, + expect: false, + trailer: false, + header: firstLine, + }; + + const headers = this[kOutHeaders]; + if (headers) { + // headers is null-prototype object, so ignore the guard lint + // deno-lint-ignore guard-for-in + for (const key in headers) { + const entry = headers[key]; + this._matchHeader(state, entry[0], entry[1]); + } + } + + // Date header + if (this.sendDate && !state.date) { + this.setHeader("Date", utcDate()); + } + + // Force the connection to close when the response is a 204 No Content or + // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" + // header. + // + // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but + // node.js used to send out a zero chunk anyway to accommodate clients + // that don't have special handling for those responses. + // + // It was pointed out that this might confuse reverse proxies to the point + // of creating security liabilities, so suppress the zero chunk and force + // the connection to close. + if ( + this.chunkedEncoding && (this.statusCode === 204 || + this.statusCode === 304) + ) { + debug( + this.statusCode + " response should not use chunked encoding," + + " closing connection.", + ); + this.chunkedEncoding = false; + this.shouldKeepAlive = false; + } + + // TODO(osddeitf): this depends on agent and underlying socket + // keep-alive logic + // if (this._removedConnection) { + // this._last = true; + // this.shouldKeepAlive = false; + // } else if (!state.connection) { + // const shouldSendKeepAlive = this.shouldKeepAlive && + // (state.contLen || this.useChunkedEncodingByDefault || this.agent); + // if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) { + // this.setHeader('Connection', 'close'); + // } else if (shouldSendKeepAlive) { + // this.setHeader('Connection', 'keep-alive'); + // if (this._keepAliveTimeout && this._defaultKeepAlive) { + // const timeoutSeconds = Math.floor(this._keepAliveTimeout / 1000); + // let max = ''; + // if (~~this._maxRequestsPerSocket > 0) { + // max = `, max=${this._maxRequestsPerSocket}`; + // } + // this.setHeader('Keep-Alive', `timeout=${timeoutSeconds}${max}`); + // } + // } else { + // this._last = true; + // this.setHeader('Connection', 'close'); + // } + // } + + if (!state.contLen && !state.te) { + if (!this._hasBody) { + // Make sure we don't end the 0\r\n\r\n at the end of the message. + this.chunkedEncoding = false; + } else if (!this.useChunkedEncodingByDefault) { + this._last = true; + } else if ( + !state.trailer && + !this._removedContLen && + typeof this._contentLength === "number" + ) { + this.setHeader("Content-Length", this._contentLength); + } else if (!this._removedTE) { + this.setHeader("Transfer-Encoding", "chunked"); + this.chunkedEncoding = true; + } else { + // We should only be able to get here if both Content-Length and + // Transfer-Encoding are removed by the user. + // See: test/parallel/test-http-remove-header-stays-removed.js + debug("Both Content-Length and Transfer-Encoding are removed"); + } + } + + // Test non-chunked message does not have trailer header set, + // message will be terminated by the first empty line after the + // header fields, regardless of the header fields present in the + // message, and thus cannot contain a message body or 'trailers'. + if (this.chunkedEncoding !== true && state.trailer) { + throw new ERR_HTTP_TRAILER_INVALID(); + } + + const { header } = state; + this._header = header + "\r\n"; + this._headerSent = false; + + // Wait until the first body chunk, or close(), is sent to flush, + // UNLESS we're sending Expect: 100-continue. + if (state.expect) this._send(""); + }, + + _matchHeader( + // deno-lint-ignore no-explicit-any + state: any, + field: string, + // deno-lint-ignore no-explicit-any + value: any, + ) { + // Ignore lint to keep the code as similar to Nodejs as possible + // deno-lint-ignore no-this-alias + const self = this; + if (field.length < 4 || field.length > 17) { + return; + } + field = field.toLowerCase(); + switch (field) { + case "connection": + state.connection = true; + self._removedConnection = false; + if (RE_CONN_CLOSE.exec(value) !== null) { + self._last = true; + } else { + self.shouldKeepAlive = true; + } + break; + case "transfer-encoding": + state.te = true; + self._removedTE = false; + if (RE_TE_CHUNKED.exec(value) !== null) { + self.chunkedEncoding = true; + } + break; + case "content-length": + state.contLen = true; + self._contentLength = value; + self._removedContLen = false; + break; + case "date": + case "expect": + case "trailer": + state[field] = true; + break; + case "keep-alive": + self._defaultKeepAlive = false; + break; + } + }, + + // deno-lint-ignore no-explicit-any + [EE.captureRejectionSymbol](err: any, _event: any) { + this.destroy(err); + }, + }), +); + Object.defineProperty(OutgoingMessage.prototype, "_headers", { get: deprecate( // deno-lint-ignore no-explicit-any