diff --git a/bench/package.json b/bench/package.json index 11b0ab69bd1af..69be623fdf1f2 100644 --- a/bench/package.json +++ b/bench/package.json @@ -12,6 +12,7 @@ "eventemitter3": "^5.0.0", "execa": "^8.0.1", "fast-glob": "3.3.1", + "fastify": "^5.0.0", "fdir": "^6.1.0", "mitata": "^0.1.6", "string-width": "7.1.0", diff --git a/bench/snippets/express-hello.mjs b/bench/snippets/express-hello.mjs new file mode 100644 index 0000000000000..4c9aea40c7287 --- /dev/null +++ b/bench/snippets/express-hello.mjs @@ -0,0 +1,13 @@ +import express from "express"; + +const app = express(); +const port = 3000; + +var i = 0; +app.get("/", (req, res) => { + res.send("Hello World!" + i++); +}); + +app.listen(port, () => { + console.log(`Express app listening at http://localhost:${port}`); +}); diff --git a/bench/snippets/fastify.mjs b/bench/snippets/fastify.mjs new file mode 100644 index 0000000000000..576a124558ca7 --- /dev/null +++ b/bench/snippets/fastify.mjs @@ -0,0 +1,20 @@ +import Fastify from "fastify"; + +const fastify = Fastify({ + logger: false, +}); + +fastify.get("/", async (request, reply) => { + return { hello: "world" }; +}); + +const start = async () => { + try { + await fastify.listen({ port: 3000 }); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +}; + +start(); diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 9395e190ea0a2..5a22601d68dbd 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -230,6 +230,8 @@ void us_loop_run(struct us_loop_t *loop) { } } +extern int Bun__JSC_onBeforeWait(void*); +extern void Bun__JSC_onAfterWait(void*); void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout) { if (loop->num_polls == 0) @@ -246,9 +248,13 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout /* Emit pre callback */ us_internal_loop_pre(loop); + int needs_after_wait = 0; + if (loop->data.jsc_vm) { + needs_after_wait = Bun__JSC_onBeforeWait(loop->data.jsc_vm); + } + /* Fetch ready polls */ #ifdef LIBUS_USE_EPOLL - loop->num_ready_polls = bun_epoll_pwait2(loop->fd, loop->ready_polls, 1024, timeout); #else do { @@ -256,6 +262,10 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout } while (IS_EINTR(loop->num_ready_polls)); #endif + if (needs_after_wait) { + Bun__JSC_onAfterWait(loop->data.jsc_vm); + } + /* Iterate ready polls, dispatching them by type */ for (loop->current_ready_poll = 0; loop->current_ready_poll < loop->num_ready_polls; loop->current_ready_poll++) { struct us_poll_t *poll = GET_READY_POLL(loop, loop->current_ready_poll); diff --git a/packages/bun-usockets/src/internal/loop_data.h b/packages/bun-usockets/src/internal/loop_data.h index 1f0a3adb76790..ddbaee9ffc76b 100644 --- a/packages/bun-usockets/src/internal/loop_data.h +++ b/packages/bun-usockets/src/internal/loop_data.h @@ -44,6 +44,7 @@ struct us_internal_loop_data_t { char parent_tag; /* We do not care if this flips or not, it doesn't matter */ size_t iteration_nr; + void* jsc_vm; }; #endif // LOOP_DATA_H diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 9bd315822b7b0..e4b7845f239e9 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -48,6 +48,7 @@ void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct loop->data.parent_tag = 0; loop->data.closed_context_head = 0; + loop->data.jsc_vm = 0; loop->data.wakeup_async = us_internal_create_async(loop, 1, 0); us_internal_async_set(loop->data.wakeup_async, (void (*)(struct us_internal_async *)) wakeup_cb); diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index be91e146d6832..3495673012eb1 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -592,6 +592,10 @@ struct TemplatedApp { return std::move(*this); } + void setOnClose(HttpContextData::OnSocketClosedCallback onClose) { + httpContext->getSocketContextData()->onSocketClosed = onClose; + } + TemplatedApp &&run() { uWS::run(); return std::move(*this); diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 0081779bdaf08..9fccbd8d81a21 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -128,7 +128,10 @@ struct HttpContext { if (httpResponseData->onAborted) { httpResponseData->onAborted((HttpResponse *)s, httpResponseData->userData); } - + + if (httpResponseData->socketData && httpContextData->onSocketClosed) { + httpContextData->onSocketClosed(httpResponseData->socketData, SSL, s); + } /* Destruct socket ext */ httpResponseData->~HttpResponseData(); diff --git a/packages/bun-uws/src/HttpContextData.h b/packages/bun-uws/src/HttpContextData.h index 502941de8746a..53f1b91065b8e 100644 --- a/packages/bun-uws/src/HttpContextData.h +++ b/packages/bun-uws/src/HttpContextData.h @@ -27,6 +27,7 @@ namespace uWS { template struct HttpResponse; struct HttpRequest; + template struct alignas(16) HttpContextData { template friend struct HttpContext; @@ -34,6 +35,7 @@ struct alignas(16) HttpContextData { template friend struct TemplatedApp; private: std::vector *, int)>> filterHandlers; + using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket); MoveOnlyFunction missingServerNameHandler; @@ -51,6 +53,9 @@ struct alignas(16) HttpContextData { bool isParsingHttp = false; bool rejectUnauthorized = false; + /* Used to simulate Node.js socket events. */ + OnSocketClosedCallback onSocketClosed = nullptr; + // TODO: SNI void clearRoutes() { this->router = HttpRouter{}; diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index a58a86bd6ade5..951d2b6e3335a 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -81,8 +81,12 @@ struct HttpResponse : public AsyncSocket { /* Called only once per request */ void writeMark() { + if (getHttpResponseData()->state & HttpResponseData::HTTP_WROTE_DATE_HEADER) { + return; + } /* Date is always written */ writeHeader("Date", std::string_view(((LoopData *) us_loop_ext(us_socket_context_loop(SSL, (us_socket_context(SSL, (us_socket_t *) this)))))->date, 29)); + getHttpResponseData()->state |= HttpResponseData::HTTP_WROTE_DATE_HEADER; } /* Returns true on success, indicating that it might be feasible to write more data. @@ -113,7 +117,8 @@ struct HttpResponse : public AsyncSocket { httpResponseData->state |= HttpResponseData::HTTP_CONNECTION_CLOSE; } - if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { + /* if write was called and there was previously no Content-Length header set */ + if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED && !(httpResponseData->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { /* We do not have tryWrite-like functionalities, so ignore optional in this path */ @@ -145,6 +150,8 @@ struct HttpResponse : public AsyncSocket { } } } + } else { + this->uncork(); } /* tryEnd can never fail when in chunked mode, since we do not have tryWrite (yet), only write */ @@ -152,7 +159,7 @@ struct HttpResponse : public AsyncSocket { return true; } else { /* Write content-length on first call */ - if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_END_CALLED))) { /* Write mark, this propagates to WebSockets too */ writeMark(); @@ -162,7 +169,8 @@ struct HttpResponse : public AsyncSocket { Super::write("Content-Length: ", 16); writeUnsigned64(totalSize); Super::write("\r\n\r\n", 4); - } else { + httpResponseData->state |= HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER; + } else if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED))) { Super::write("\r\n", 2); } @@ -207,6 +215,8 @@ struct HttpResponse : public AsyncSocket { } } } + } else { + this->uncork(); } } @@ -427,7 +437,7 @@ struct HttpResponse : public AsyncSocket { /* End the response with an optional data chunk. Always starts a timeout. */ void end(std::string_view data = {}, bool closeConnection = false) { - internalEnd(data, data.length(), false, true, closeConnection); + internalEnd(data, data.length(), false, !(this->getHttpResponseData()->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER), closeConnection); } /* Try and end the response. Returns [true, true] on success. @@ -440,7 +450,7 @@ struct HttpResponse : public AsyncSocket { bool sendTerminatingChunk(bool closeConnection = false) { writeStatus(HTTP_200_OK); HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + if (!(httpResponseData->state & (HttpResponseData::HTTP_WRITE_CALLED | HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER))) { /* Write mark on first call to write */ writeMark(); @@ -455,33 +465,46 @@ struct HttpResponse : public AsyncSocket { } /* Write parts of the response in chunking fashion. Starts timeout if failed. */ - bool write(std::string_view data) { + bool write(std::string_view data, size_t *writtenPtr = nullptr) { writeStatus(HTTP_200_OK); /* Do not allow sending 0 chunks, they mark end of response */ if (!data.length()) { + if (writtenPtr) { + *writtenPtr = 0; + } /* If you called us, then according to you it was fine to call us so it's fine to still call us */ return true; } HttpResponseData *httpResponseData = getHttpResponseData(); - if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { - /* Write mark on first call to write */ - writeMark(); + if (!(httpResponseData->state & HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { + if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + /* Write mark on first call to write */ + writeMark(); - writeHeader("Transfer-Encoding", "chunked"); + writeHeader("Transfer-Encoding", "chunked"); + httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; + } + + Super::write("\r\n", 2); + writeUnsignedHex((unsigned int) data.length()); + Super::write("\r\n", 2); + } else if (!(httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED)) { + writeMark(); + Super::write("\r\n", 2); httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; } - Super::write("\r\n", 2); - writeUnsignedHex((unsigned int) data.length()); - Super::write("\r\n", 2); - auto [written, failed] = Super::write(data.data(), (int) data.length()); /* Reset timeout on each sended chunk */ this->resetTimeout(); + if (writtenPtr) { + *writtenPtr = written; + } + /* If we did not fail the write, accept more */ return !failed; } @@ -628,6 +651,17 @@ struct HttpResponse : public AsyncSocket { data->received_bytes_per_timeout = 0; } + void* getSocketData() { + HttpResponseData *httpResponseData = getHttpResponseData(); + + return httpResponseData->socketData; + } + + void setSocketData(void* socketData) { + HttpResponseData *httpResponseData = getHttpResponseData(); + + httpResponseData->socketData = socketData; + } void setWriteOffset(uint64_t offset) { HttpResponseData *httpResponseData = getHttpResponseData(); diff --git a/packages/bun-uws/src/HttpResponseData.h b/packages/bun-uws/src/HttpResponseData.h index 9613e84fe495a..316430e562c9e 100644 --- a/packages/bun-uws/src/HttpResponseData.h +++ b/packages/bun-uws/src/HttpResponseData.h @@ -78,11 +78,14 @@ struct HttpResponseData : AsyncSocketData, HttpParser { HTTP_WRITE_CALLED = 2, // used HTTP_END_CALLED = 4, // used HTTP_RESPONSE_PENDING = 8, // used - HTTP_CONNECTION_CLOSE = 16 // used + HTTP_CONNECTION_CLOSE = 16, // used + HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, // used + HTTP_WROTE_DATE_HEADER = 64, // used }; /* Shared context pointer */ void* userData = nullptr; + void* socketData = nullptr; /* Per socket event handlers */ OnWritableCallback onWritable = nullptr; diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h index f8ea7f6e3aacb..4f1a31cb1e670 100644 --- a/packages/bun-uws/src/Loop.h +++ b/packages/bun-uws/src/Loop.h @@ -24,6 +24,7 @@ #include "LoopData.h" #include #include +#include "AsyncSocket.h" extern "C" int bun_is_exiting(); @@ -52,6 +53,15 @@ struct Loop { for (auto &p : loopData->preHandlers) { p.second((Loop *) loop); } + + void *corkedSocket = loopData->getCorkedSocket(); + if (corkedSocket) { + if (loopData->isCorkedSSL()) { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } else { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } + } } static void postCb(us_loop_t *loop) { @@ -148,6 +158,10 @@ struct Loop { getLazyLoop().loop = nullptr; } + static LoopData* data(struct us_loop_t *loop) { + return (LoopData *) us_loop_ext(loop); + } + void addPostHandler(void *key, MoveOnlyFunction &&handler) { LoopData *loopData = (LoopData *) us_loop_ext((us_loop_t *) this); diff --git a/packages/bun-uws/src/LoopData.h b/packages/bun-uws/src/LoopData.h index e68ca51b0eb0d..96e69eec25e1d 100644 --- a/packages/bun-uws/src/LoopData.h +++ b/packages/bun-uws/src/LoopData.h @@ -63,6 +63,7 @@ struct alignas(16) LoopData { } delete [] corkBuffer; } + void* getCorkedSocket() { return this->corkedSocket; } diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 535db8e26fa93..14a9a7d094f2f 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -734,6 +734,8 @@ pub const H2FrameParser = struct { autouncork_registered: bool = false, has_nonnative_backpressure: bool = false, ref_count: u8 = 1, + has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), + reported_estimated_size: usize = 0, threadlocal var shared_request_buffer: [16384]u8 = undefined; /// The streams hashmap may mutate when growing we use this when we need to make sure its safe to iterate over it @@ -919,6 +921,7 @@ pub const H2FrameParser = struct { .max => return @min(maxLen - frameLen, 255), } } + pub fn flushQueue(this: *Stream, client: *H2FrameParser, written: *usize) FlushState { if (this.canSendData()) { // flush one frame @@ -943,6 +946,7 @@ pub const H2FrameParser = struct { } else { // flush with some payload client.queuedDataSize -= frame.len; + const padding = this.getPadding(frame.len, MAX_PAYLOAD_SIZE_WITHOUT_FRAME - 1); const payload_size = frame.len + (if (padding != 0) padding + 1 else 0); var flags: u8 = if (frame.end_stream and !this.waitForTrailers) @intFromEnum(DataFrameFlags.END_STREAM) else 0; @@ -1051,12 +1055,12 @@ pub const H2FrameParser = struct { }; if (bytes.len > 0) { @memcpy(frame.buffer[0..bytes.len], bytes); - client.globalThis.vm().reportExtraMemory(bytes.len); } log("dataFrame enqueued {}", .{frame.len}); this.dataFrameQueue.enqueue(frame, client.allocator); client.outboundQueueSize += 1; client.queuedDataSize += frame.len; + client.calculateEstimatedByteSize(); } pub fn init(streamIdentifier: u32, initialWindowSize: u32) Stream { @@ -1132,6 +1136,7 @@ pub const H2FrameParser = struct { frame.deinit(client.allocator); client.outboundQueueSize -= 1; } + client.calculateEstimatedByteSize(); } /// this can be called multiple times pub fn freeResources(this: *Stream, client: *H2FrameParser, comptime finalizing: bool) void { @@ -1473,6 +1478,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } log("_genericFlush {}", .{buffer.len}); } else { @@ -1483,6 +1489,7 @@ pub const H2FrameParser = struct { pub fn _genericWrite(this: *H2FrameParser, comptime T: type, socket: T, bytes: []const u8) bool { log("_genericWrite {}", .{bytes.len}); + defer this.calculateEstimatedByteSize(); const buffer = this.writeBuffer.slice()[this.writeBufferOffset..]; if (buffer.len > 0) { @@ -1494,7 +1501,6 @@ pub const H2FrameParser = struct { // we still have more to buffer and even more now _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); log("_genericWrite flushed {} and buffered more {}", .{ written, bytes.len }); return false; @@ -1510,7 +1516,6 @@ pub const H2FrameParser = struct { const pending = bytes[written..]; // ops not all data was sent, lets buffer again _ = this.writeBuffer.write(this.allocator, pending) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(pending.len); log("_genericWrite buffered more {}", .{pending.len}); return false; @@ -1521,6 +1526,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } return true; } @@ -1530,7 +1536,6 @@ pub const H2FrameParser = struct { const pending = bytes[written..]; // ops not all data was sent, lets buffer again _ = this.writeBuffer.write(this.allocator, pending) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(pending.len); return false; } @@ -1540,6 +1545,8 @@ pub const H2FrameParser = struct { fn flushStreamQueue(this: *H2FrameParser) usize { log("flushStreamQueue {}", .{this.outboundQueueSize}); var written: usize = 0; + defer this.calculateEstimatedByteSize(); + // try to send as much as we can until we reach backpressure while (this.outboundQueueSize > 0) { var it = StreamResumableIterator.init(this); @@ -1575,6 +1582,7 @@ pub const H2FrameParser = struct { this.writeBuffer.len = MAX_BUFFER_SIZE; this.writeBuffer.shrinkAndFree(this.allocator, MAX_BUFFER_SIZE); this.writeBuffer.clearRetainingCapacity(); + this.calculateEstimatedByteSize(); } } const output_value = this.handlers.binary_type.toJS(bytes, this.handlers.globalObject); @@ -1602,10 +1610,11 @@ pub const H2FrameParser = struct { .tls_writeonly, .tls => |socket| this._genericWrite(*TLSSocket, socket, bytes), .tcp_writeonly, .tcp => |socket| this._genericWrite(*TCPSocket, socket, bytes), else => { + defer this.calculateEstimatedByteSize(); + if (this.has_nonnative_backpressure) { // we should not invoke JS when we have backpressure is cheaper to keep it queued here _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); return false; } @@ -1617,7 +1626,6 @@ pub const H2FrameParser = struct { -1 => { // dropped _ = this.writeBuffer.write(this.allocator, bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); this.has_nonnative_backpressure = true; }, 0 => { @@ -1648,6 +1656,7 @@ pub const H2FrameParser = struct { if (bytes.len > 0) { _ = this._write(bytes); } + CORKED_H2 = null; } } } @@ -1704,7 +1713,7 @@ pub const H2FrameParser = struct { if (this.remainingLength > 0) { // buffer more data _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(payload.len); + this.calculateEstimatedByteSize(); return null; } else if (this.remainingLength < 0) { @@ -1717,7 +1726,7 @@ pub const H2FrameParser = struct { if (this.readBuffer.list.items.len > 0) { // return buffered data _ = this.readBuffer.appendSlice(payload) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(payload.len); + this.calculateEstimatedByteSize(); return .{ .data = this.readBuffer.list.items, @@ -2198,6 +2207,7 @@ pub const H2FrameParser = struct { const settings = this.remoteSettings orelse this.localSettings; const entry = this.streams.getOrPut(streamIdentifier) catch bun.outOfMemory(); entry.value_ptr.* = Stream.init(streamIdentifier, settings.initialWindowSize); + this.calculateEstimatedByteSize(); const ctx_value = this.strong_ctx.get() orelse return entry.value_ptr; const callback = this.handlers.onStreamStart; if (callback != .zero) { @@ -2256,7 +2266,7 @@ pub const H2FrameParser = struct { if (total < FrameHeader.byteSize) { // buffer more data _ = this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); + this.calculateEstimatedByteSize(); return bytes.len; } @@ -2296,7 +2306,7 @@ pub const H2FrameParser = struct { if (bytes.len < FrameHeader.byteSize) { // buffer more dheaderata this.readBuffer.appendSlice(bytes) catch bun.outOfMemory(); - this.globalThis.vm().reportExtraMemory(bytes.len); + this.calculateEstimatedByteSize(); return bytes.len; } @@ -3111,6 +3121,27 @@ pub const H2FrameParser = struct { this.dispatchWithExtra(.onStreamEnd, identifier, JSC.JSValue.jsNumber(@intFromEnum(stream.state))); return .undefined; } + + fn getNextStreamID(this: *H2FrameParser) u32 { + var stream_id: u32 = this.lastStreamID; + if (this.isServer) { + if (stream_id % 2 == 0) { + stream_id += 2; + } else { + stream_id += 1; + } + } else { + if (stream_id % 2 == 0) { + stream_id += 1; + } else if (stream_id == 0) { + stream_id = 1; + } else { + stream_id += 2; + } + } + return stream_id; + } + pub fn writeStream(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue { JSC.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -3168,26 +3199,6 @@ pub const H2FrameParser = struct { return JSC.JSValue.jsBoolean(true); } - fn getNextStreamID(this: *H2FrameParser) u32 { - var stream_id: u32 = this.lastStreamID; - if (this.isServer) { - if (stream_id % 2 == 0) { - stream_id += 2; - } else { - stream_id += 1; - } - } else { - if (stream_id % 2 == 0) { - stream_id += 1; - } else if (stream_id == 0) { - stream_id = 1; - } else { - stream_id += 2; - } - } - return stream_id; - } - pub fn hasNativeRead(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue { return JSC.JSValue.jsBoolean(this.native_socket == .tcp or this.native_socket == .tls); } @@ -3817,6 +3828,7 @@ pub const H2FrameParser = struct { }); } }; + this.calculateEstimatedByteSize(); // check if socket is provided, and if it is a valid native socket if (options.get(globalObject, "native")) |socket_js| { if (JSTLSSocket.fromJS(socket_js)) |socket| { @@ -3894,7 +3906,7 @@ pub const H2FrameParser = struct { } return this; } - pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue { + pub fn detachFromJS(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, _: JSC.JSValue) JSValue { JSC.markBinding(@src()); this.detach(false); return .undefined; @@ -3902,6 +3914,7 @@ pub const H2FrameParser = struct { /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser, comptime finalizing: bool) void { + log("detach {s}", .{if (this.isServer) "server" else "client"}); this.flushCorked(); this.detachNativeSocket(); this.strong_ctx.deinit(); @@ -3922,8 +3935,11 @@ pub const H2FrameParser = struct { stream.freeResources(this, finalizing); } var streams = this.streams; - defer streams.deinit(); this.streams = bun.U32HashMap(Stream).init(bun.default_allocator); + streams.deinit(); + + this.has_pending_activity.store(false, .release); + this.calculateEstimatedByteSize(); } pub fn deinit(this: *H2FrameParser) void { @@ -3939,10 +3955,28 @@ pub const H2FrameParser = struct { this.detach(true); } + pub fn estimatedSize(this: ?*H2FrameParser) callconv(.C) usize { + if (this == null) { + return 0; + } + return this.?.reported_estimated_size; + } + + pub fn calculateEstimatedByteSize(this: *H2FrameParser) void { + this.reported_estimated_size = @sizeOf(H2FrameParser) + this.writeBuffer.len + this.queuedDataSize + (this.streams.capacity() * @sizeOf(Stream)); + } + pub fn hasPendingActivity(this: ?*H2FrameParser) callconv(.C) bool { + @fence(.acquire); + if (this == null) { + return false; + } + return this.?.has_pending_activity.load(.acquire); + } + pub fn finalize( this: *H2FrameParser, ) void { - log("finalize", .{}); + log("finalize {s}", .{if (this.isServer) "server" else "client"}); this.deref(); } }; diff --git a/src/bun.js/api/h2.classes.ts b/src/bun.js/api/h2.classes.ts index bcad57f64a45e..c5f05e3491b8a 100644 --- a/src/bun.js/api/h2.classes.ts +++ b/src/bun.js/api/h2.classes.ts @@ -4,6 +4,11 @@ export default [ define({ name: "H2FrameParser", JSType: "0b11101110", + finalize: true, + construct: true, + hasPendingActivity: true, + estimatedSize: true, + klass: {}, proto: { request: { fn: "request", @@ -40,6 +45,7 @@ export default [ detach: { fn: "detachFromJS", length: 0, + passThis: true, }, rstStream: { fn: "rstStream", @@ -106,8 +112,5 @@ export default [ length: 0, }, }, - finalize: true, - construct: true, - klass: {}, }), ]; diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index 9182fa809e4f8..b68dd4012dedc 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -90,6 +90,81 @@ export default [ generate(`HTTPSServer`), generate(`DebugHTTPSServer`), + define({ + name: "NodeHTTPResponse", + JSType: "0b11101110", + proto: { + writeHead: { + fn: "writeHead", + length: 3, + }, + write: { + fn: "write", + length: 2, + }, + end: { + fn: "end", + length: 2, + }, + cork: { + fn: "cork", + length: 1, + }, + ref: { + fn: "jsRef", + }, + unref: { + fn: "jsUnref", + }, + abort: { + fn: "abort", + length: 0, + }, + pause: { + fn: "doPause", + length: 0, + }, + resume: { + fn: "doResume", + length: 0, + }, + bufferedAmount: { + getter: "getBufferedAmount", + }, + aborted: { + getter: "getAborted", + }, + finished: { + getter: "getFinished", + }, + hasBody: { + getter: "getHasBody", + }, + ended: { + getter: "getEnded", + }, + ondata: { + getter: "getOnData", + setter: "setOnData", + }, + onabort: { + getter: "getOnAbort", + setter: "setOnAbort", + }, + // ontimeout: { + // getter: "getOnTimeout", + // setter: "setOnTimeout", + // }, + onwritable: { + getter: "getOnWritable", + setter: "setOnWritable", + }, + }, + klass: {}, + finalize: true, + noConstructor: true, + }), + define({ name: "ServerWebSocket", JSType: "0b11101110", diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 59c16b6fb5771..d5132139654f6 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -455,6 +455,7 @@ pub const ServerConfig = struct { onError: JSC.JSValue = JSC.JSValue.zero, onRequest: JSC.JSValue = JSC.JSValue.zero, + onNodeHTTPRequest: JSC.JSValue = JSC.JSValue.zero, websocket: ?WebSocketServer = null, @@ -463,6 +464,8 @@ pub const ServerConfig = struct { id: []const u8 = "", allow_hot: bool = true, + is_node_http: bool = false, + static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), pub const StaticRouteEntry = struct { @@ -1433,6 +1436,16 @@ pub const ServerConfig = struct { } if (global.hasException()) return args; + if (arg.getTruthy(global, "onNodeHTTPRequest")) |onRequest_| { + if (!onRequest_.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected onNodeHTTPRequest to be a function", .{}, global, exception); + return args; + } + const onRequest = onRequest_.withAsyncContextIfNeeded(global); + JSC.C.JSValueProtect(global, onRequest.asObjectRef()); + args.onNodeHTTPRequest = onRequest; + } + if (arg.getTruthy(global, "fetch")) |onRequest_| { if (!onRequest_.isCallable(global.vm())) { JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global, exception); @@ -3547,7 +3560,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // we can't do buffering ourselves here or it won't work // uSockets will append and manage the buffer // so any write will buffer if the write fails - if (resp.write(chunk)) { + if (resp.write(chunk) == .want_more) { if (stream.isDone()) { this.endStream(this.shouldCloseConnection()); } @@ -5783,6 +5796,779 @@ pub const ServerWebSocket = struct { } }; +pub const NodeHTTPResponse = struct { + response: uws.AnyResponse, + onDataCallback: JSC.Strong = .{}, + onWritableCallback: JSC.Strong = .{}, + onAbortedCallback: JSC.Strong = .{}, + + ref_count: u32 = 1, + js_ref: JSC.Ref = .{}, + aborted: bool = false, + finished: bool = false, + ended: bool = false, + is_request_pending: bool = true, + body_read_state: BodyReadState = .none, + body_read_ref: JSC.Ref = .{}, + promise: JSC.Strong = .{}, + server: AnyServer, + + const log = bun.Output.scoped(.NodeHTTPResponse, false); + pub usingnamespace JSC.Codegen.JSNodeHTTPResponse; + pub usingnamespace bun.NewRefCounted(@This(), deinit); + + pub const BodyReadState = enum { + none, + pending, + done, + }; + + extern "C" fn Bun__getNodeHTTPResponseThisValue(c_int, *anyopaque) JSC.JSValue; + fn getThisValue(this: *NodeHTTPResponse) JSC.JSValue { + return Bun__getNodeHTTPResponseThisValue(@intFromBool(this.response == .SSL), this.response.socket()); + } + + pub fn maybeStopReadingBody(this: *NodeHTTPResponse, vm: *JSC.VirtualMachine) void { + if ((this.aborted or this.ended) and (this.body_read_ref.has or this.body_read_state == .pending) and !this.onDataCallback.has()) { + const had_ref = this.body_read_ref.has; + this.response.clearOnData(); + this.body_read_ref.unref(vm); + this.body_read_state = .done; + + if (had_ref) { + this.markRequestAsDoneIfNecessary(); + } + + this.deref(); + } + } + + pub fn shouldRequestBePending(this: *const NodeHTTPResponse) bool { + if (this.aborted) { + return false; + } + + if (this.ended) { + return this.body_read_state == .pending; + } + + return true; + } + + fn markRequestAsDone(this: *NodeHTTPResponse) void { + log("markRequestAsDone()", .{}); + this.is_request_pending = false; + + this.clearJSValues(); + this.clearOnDataCallback(); + const server = this.server; + this.js_ref.unref(JSC.VirtualMachine.get()); + this.deref(); + server.onRequestComplete(); + } + + fn markRequestAsDoneIfNecessary(this: *NodeHTTPResponse) void { + if (this.is_request_pending and !this.shouldRequestBePending()) { + this.markRequestAsDone(); + } + } + + pub fn create( + any_server_tag: u64, + globalObject: *JSC.JSGlobalObject, + has_body: *i32, + request: *uws.Request, + is_ssl: i32, + response_ptr: *anyopaque, + node_response_ptr: *?*NodeHTTPResponse, + ) callconv(.C) JSC.JSValue { + const vm = globalObject.bunVM(); + if ((HTTP.Method.which(request.method()) orelse HTTP.Method.OPTIONS).hasRequestBody()) { + const req_len: usize = brk: { + if (request.header("content-length")) |content_length| { + break :brk std.fmt.parseInt(usize, content_length, 10) catch 0; + } + + break :brk 0; + }; + + has_body.* = @intFromBool(req_len > 0 or request.header("transfer-encoding") != null); + } + + const response = NodeHTTPResponse.new(.{ + .server = AnyServer{ .ptr = AnyServer.Ptr.from(@ptrFromInt(any_server_tag)) }, + .response = switch (is_ssl != 0) { + true => uws.AnyResponse{ .SSL = @ptrCast(response_ptr) }, + false => uws.AnyResponse{ .TCP = @ptrCast(response_ptr) }, + }, + .body_read_state = if (has_body.* != 0) .pending else .none, + // 1 - the HTTP response + // 1 - the JS object + // 1 - the Server handler. + // 1 - the onData callback (request bod) + .ref_count = if (has_body.* != 0) 4 else 3, + }); + if (has_body.* != 0) { + response.body_read_ref.ref(vm); + } + response.js_ref.ref(vm); + const js_this = response.toJS(globalObject); + node_response_ptr.* = response; + return js_this; + } + + pub fn setOnAbortedHandler(this: *NodeHTTPResponse) void { + this.response.onAborted(*NodeHTTPResponse, onAbort, this); + this.response.onTimeout(*NodeHTTPResponse, onTimeout, this); + } + + fn isDone(this: *const NodeHTTPResponse) bool { + return this.finished or this.ended or this.aborted; + } + + pub fn getEnded(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.ended); + } + + pub fn getFinished(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.finished); + } + + pub fn getAborted(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.aborted); + } + + pub fn getHasBody(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.body_read_state != .none); + } + + pub fn getBufferedAmount(this: *const NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + if (this.finished) { + return JSC.JSValue.jsNull(); + } + + return JSC.JSValue.jsNumber(this.response.getBufferedAmount()); + } + + pub fn jsRef(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue { + if (!this.isDone()) { + this.js_ref.ref(globalObject.bunVM()); + } + return .undefined; + } + + pub fn jsUnref(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue { + if (!this.isDone()) { + this.js_ref.unref(globalObject.bunVM()); + } + return .undefined; + } + + fn handleEndedIfNecessary(state: uws.State, globalObject: *JSC.JSGlobalObject) bool { + if (!state.isResponsePending()) { + globalObject.ERR_HTTP_HEADERS_SENT("Stream is already ended", .{}).throw(); + return true; + } + return false; + } + + extern "C" fn NodeHTTPServer__writeHead_http( + globalObject: *JSC.JSGlobalObject, + statusMessage: [*]const u8, + statusMessageLength: usize, + headersObjectValue: JSC.JSValue, + response: *anyopaque, + ) void; + + extern "C" fn NodeHTTPServer__writeHead_https( + globalObject: *JSC.JSGlobalObject, + statusMessage: [*]const u8, + statusMessageLength: usize, + headersObjectValue: JSC.JSValue, + response: *anyopaque, + ) void; + + pub fn writeHead(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.argumentsUndef(3).slice(); + + if (this.isDone()) { + globalObject.ERR_STREAM_ALREADY_FINISHED("Stream is already ended", .{}).throw(); + return .undefined; + } + + const state = this.response.state(); + if (handleEndedIfNecessary(state, globalObject)) { + return .zero; + } + + const status_code_value = if (arguments.len > 0) arguments[0] else .undefined; + const status_message_value = if (arguments.len > 1 and arguments[1] != .null) arguments[1] else .undefined; + const headers_object_value = if (arguments.len > 2 and arguments[2] != .null) arguments[2] else .undefined; + + const status_code: i32 = brk: { + if (status_code_value != .undefined) { + break :brk globalObject.validateIntegerRange(status_code_value, i32, 200, .{ + .min = 100, + .max = 599, + }) orelse return .zero; + } + + break :brk 200; + }; + + var stack_fallback = std.heap.stackFallback(256, bun.default_allocator); + const allocator = stack_fallback.get(); + const status_message_slice = if (status_message_value != .undefined) + status_message_value.toSlice(globalObject, allocator) + else + ZigString.Slice.empty; + defer status_message_slice.deinit(); + + if (globalObject.hasException()) { + return .zero; + } + + if (state.isHttpStatusCalled()) { + globalObject.ERR_HTTP_HEADERS_SENT("Stream already started", .{}).throw(); + return .zero; + } + + do_it: { + if (status_message_slice.len == 0) { + if (HTTPStatusText.get(@intCast(status_code))) |status_message| { + writeHeadInternal(this.response, globalObject, status_message, headers_object_value); + break :do_it; + } + } + + const message = if (status_message_slice.len > 0) status_message_slice.slice() else "HM"; + const status_message = std.fmt.allocPrint(allocator, "{d} {s}", .{ status_code, message }) catch bun.outOfMemory(); + defer allocator.free(status_message); + writeHeadInternal(this.response, globalObject, status_message, headers_object_value); + break :do_it; + } + + return .undefined; + } + + fn writeHeadInternal(response: uws.AnyResponse, globalObject: *JSC.JSGlobalObject, status_message: []const u8, headers: JSC.JSValue) void { + log("writeHeadInternal({s})", .{status_message}); + switch (response) { + .TCP => NodeHTTPServer__writeHead_http(globalObject, status_message.ptr, status_message.len, headers, @ptrCast(response.TCP)), + .SSL => NodeHTTPServer__writeHead_https(globalObject, status_message.ptr, status_message.len, headers, @ptrCast(response.SSL)), + } + } + + pub fn writeContinue(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + _ = arguments; // autofix + if (this.isDone()) { + return .undefined; + } + + const state = this.response.state(); + if (handleEndedIfNecessary(state, globalObject)) { + return .zero; + } + + this.response.writeContinue(); + return .undefined; + } + + pub const AbortEvent = enum(u8) { + none = 0, + abort = 1, + timeout = 2, + }; + + fn handleAbortOrTimeout(this: *NodeHTTPResponse, comptime event: AbortEvent) void { + if (this.finished) { + return; + } + + if (event == .abort) { + this.aborted = true; + } + + this.ref(); + defer if (event == .abort) this.markRequestAsDoneIfNecessary(); + defer this.deref(); + + const js_this: JSValue = this.getThisValue(); + if (this.onAbortedCallback.get()) |on_aborted| { + defer { + if (event == .abort) { + this.onAbortedCallback.deinit(); + } + } + const globalThis = this.onAbortedCallback.globalThis orelse JSC.VirtualMachine.get().global; + const vm = globalThis.bunVM(); + const event_loop = vm.eventLoop(); + + event_loop.runCallback(on_aborted, globalThis, js_this, &.{ + JSC.JSValue.jsNumber(@intFromEnum(event)), + }); + } + + if (event == .abort) { + this.onDataOrAborted("", true, .abort); + } + } + + pub fn onAbort(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + _ = response; // autofix + log("onAbort", .{}); + this.handleAbortOrTimeout(.abort); + } + + pub fn onTimeout(this: *NodeHTTPResponse, response: uws.AnyResponse) void { + _ = response; // autofix + log("onTimeout", .{}); + this.handleAbortOrTimeout(.timeout); + } + + pub fn doPause(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.finished or this.aborted) { + return .undefined; + } + + this.response.pause(); + return .undefined; + } + + pub fn doResume(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.finished or this.aborted) { + return .undefined; + } + + this.response.@"resume"(); + return .undefined; + } + + fn onRequestComplete(this: *NodeHTTPResponse) void { + if (this.finished) { + return; + } + log("onRequestComplete", .{}); + this.finished = true; + this.js_ref.unref(JSC.VirtualMachine.get()); + + this.clearJSValues(); + this.markRequestAsDoneIfNecessary(); + } + + pub export fn Bun__NodeHTTPRequest__onResolve(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + log("onResolve", .{}); + const arguments = callframe.arguments(2).slice(); + const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; + this.promise.deinit(); + defer this.deref(); + this.maybeStopReadingBody(globalObject.bunVM()); + + if (!this.finished and !this.aborted) { + this.clearJSValues(); + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + if (this.response.state().isResponsePending()) { + this.response.endWithoutBody(this.response.state().isHttpConnectionClose()); + } + this.onRequestComplete(); + } + + return .undefined; + } + + pub export fn Bun__NodeHTTPRequest__onReject(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(2).slice(); + const err = arguments[0]; + const this: *NodeHTTPResponse = arguments[1].as(NodeHTTPResponse).?; + this.promise.deinit(); + this.maybeStopReadingBody(globalObject.bunVM()); + + defer this.deref(); + + if (!this.finished and !this.aborted) { + this.clearJSValues(); + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + if (!this.response.state().isHttpStatusCalled()) { + this.response.writeStatus("500 Internal Server Error"); + } + this.response.endStream(this.response.state().isHttpConnectionClose()); + this.onRequestComplete(); + } + + _ = globalObject.bunVM().uncaughtException(globalObject, err, true); + return .undefined; + } + + fn clearJSValues(this: *NodeHTTPResponse) void { + // Promise is handled separately. + this.onWritableCallback.deinit(); + this.onAbortedCallback.deinit(); + } + + pub fn abort(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + _ = globalObject; // autofix + _ = callframe; // autofix + if (this.isDone()) { + return .undefined; + } + + this.aborted = true; + const state = this.response.state(); + if (state.isHttpEndCalled()) { + return .undefined; + } + + this.response.clearAborted(); + this.response.clearOnData(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + this.response.endWithoutBody(true); + this.onRequestComplete(); + return .undefined; + } + + fn onDataOrAborted(this: *NodeHTTPResponse, chunk: []const u8, last: bool, event: AbortEvent) void { + if (last) { + this.ref(); + this.body_read_state = .done; + } + + const was_finished = this.finished; + _ = was_finished; // autofix + + defer { + if (last) { + if (this.body_read_ref.has) { + this.body_read_ref.unref(JSC.VirtualMachine.get()); + this.markRequestAsDoneIfNecessary(); + this.deref(); + } + + this.deref(); + } + } + + if (this.onDataCallback.get()) |callback| { + const globalThis = this.onDataCallback.globalThis orelse JSC.VirtualMachine.get().global; + const event_loop = globalThis.bunVM().eventLoop(); + event_loop.runCallback(callback, globalThis, .undefined, &.{ + if (chunk.len > 0) JSC.ArrayBuffer.createBuffer(globalThis, chunk) else .undefined, + JSC.JSValue.jsBoolean(last), + JSC.JSValue.jsNumber(@intFromEnum(event)), + }); + } + } + pub const BUN_DEBUG_REFCOUNT_NAME = "NodeHTTPServerResponse"; + pub fn onData(this: *NodeHTTPResponse, chunk: []const u8, last: bool) void { + log("onData({d} bytes, is_last = {d})", .{ chunk.len, @intFromBool(last) }); + + onDataOrAborted(this, chunk, last, .none); + } + + fn onDrain(this: *NodeHTTPResponse, offset: u64, response: uws.AnyResponse) bool { + log("onDrain({d})", .{offset}); + this.ref(); + defer this.deref(); + response.clearOnWritable(); + if (this.aborted or this.finished) { + return false; + } + const on_writable = this.onWritableCallback.trySwap() orelse return false; + const globalThis = this.onWritableCallback.globalThis orelse JSC.VirtualMachine.get().global; + const vm = globalThis.bunVM(); + + response.corked(JSC.EventLoop.runCallback, .{ vm.eventLoop(), on_writable, globalThis, .undefined, &.{JSC.JSValue.jsNumberFromUint64(offset)} }); + if (this.aborted or this.finished) { + return false; + } + + return true; + } + + fn writeOrEnd( + this: *NodeHTTPResponse, + globalObject: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + comptime is_end: bool, + ) JSC.JSValue { + if (this.isDone()) { + globalObject.ERR_STREAM_WRITE_AFTER_END("Stream already ended", .{}).throw(); + return .zero; + } + + const state = this.response.state(); + if (!state.isResponsePending()) { + globalObject.ERR_STREAM_WRITE_AFTER_END("Stream already ended", .{}).throw(); + return .zero; + } + + const input_value = if (arguments.len > 0) arguments[0] else .undefined; + var encoding_value = if (arguments.len > 1) arguments[1] else .undefined; + const callback_value = brk: { + if ((encoding_value != .null and encoding_value != .undefined) and encoding_value.isCallable(globalObject.vm())) { + encoding_value = .undefined; + break :brk arguments[1]; + } + + if (arguments.len > 2 and arguments[2] != .undefined) { + if (!arguments[2].isCallable(globalObject.vm())) { + return globalObject.throwInvalidArgumentTypeValue("callback", "function", arguments[2]); + } + + break :brk arguments[2]; + } + + break :brk .undefined; + }; + + const string_or_buffer: JSC.Node.StringOrBuffer = brk: { + if (input_value == .null or input_value == .undefined) { + break :brk JSC.Node.StringOrBuffer.empty; + } + + var encoding: JSC.Node.Encoding = .utf8; + if (encoding_value != .undefined and encoding_value != .null) { + if (!encoding_value.isString()) { + return globalObject.throwInvalidArgumentTypeValue("encoding", "string", encoding_value); + } + + encoding = JSC.Node.Encoding.fromJS(encoding_value, globalObject) orelse { + globalObject.throwInvalidArguments("Invalid encoding", .{}); + return .zero; + }; + } + + break :brk JSC.Node.StringOrBuffer.fromJSWithEncoding(globalObject, bun.default_allocator, input_value, encoding) orelse { + if (!globalObject.hasException()) { + return globalObject.throwInvalidArgumentTypeValue("input", "string or buffer", input_value); + } + return .zero; + }; + }; + defer string_or_buffer.deinit(); + + if (globalObject.hasException()) { + return .zero; + } + + const bytes = string_or_buffer.slice(); + + if (comptime is_end) { + log("end('{s}', {d})", .{ bytes[0..@min(bytes.len, 128)], bytes.len }); + } else { + log("write('{s}', {d})", .{ bytes[0..@min(bytes.len, 128)], bytes.len }); + } + + if (is_end) { + this.response.clearAborted(); + this.response.clearOnWritable(); + this.response.clearTimeout(); + this.ended = true; + if (!state.isHttpWriteCalled() or bytes.len > 0) { + this.response.end(bytes, state.isHttpConnectionClose()); + } else { + this.response.endStream(state.isHttpConnectionClose()); + } + this.onRequestComplete(); + + return JSC.JSValue.jsNumberFromUint64(bytes.len); + } else { + switch (this.response.write(bytes)) { + .want_more => |written| { + this.response.clearOnWritable(); + this.onWritableCallback.clear(); + return JSC.JSValue.jsNumberFromUint64(written); + }, + .backpressure => |written| { + if (callback_value != .undefined) { + this.onWritableCallback.set(globalObject, callback_value.withAsyncContextIfNeeded(globalObject)); + this.response.onWritable(*NodeHTTPResponse, onDrain, this); + } + return JSC.JSValue.jsNumberFromInt64(-@as(i64, @intCast(written))); + }, + } + } + } + + pub fn setOnWritable(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (this.isDone() or value == .undefined) { + this.onWritableCallback.clear(); + return true; + } + + this.onWritableCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + return true; + } + + pub fn getOnWritable(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onWritableCallback.get() orelse .undefined; + } + + pub fn getOnAbort(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onAbortedCallback.get() orelse .undefined; + } + + pub fn setOnAbort(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (this.isDone() or value == .undefined) { + this.onAbortedCallback.clear(); + return true; + } + + this.onAbortedCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + return true; + } + + pub fn getOnData(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.onDataCallback.get() orelse .undefined; + } + + fn clearOnDataCallback(this: *NodeHTTPResponse) void { + if (this.body_read_state != .none) { + this.onDataCallback.deinit(); + if (!this.aborted) + this.response.clearOnData(); + if (this.body_read_state != .done) { + this.body_read_state = .done; + if (this.body_read_ref.has) { + this.deref(); + } + } + } + } + + pub fn setOnData(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, value: JSValue) bool { + if (value == .undefined or this.ended or this.aborted or this.body_read_state == .none) { + this.onDataCallback.deinit(); + defer { + if (this.body_read_ref.has) { + this.body_read_ref.unref(globalObject.bunVM()); + this.deref(); + } + } + switch (this.body_read_state) { + .pending, .done => { + if (!this.ended and !this.aborted) { + this.response.clearOnData(); + } + this.body_read_state = .done; + }, + .none => {}, + } + return true; + } + + this.onDataCallback.set(globalObject, value.withAsyncContextIfNeeded(globalObject)); + this.response.onData(*NodeHTTPResponse, onData, this); + + if (!this.body_read_ref.has) { + this.ref(); + this.body_read_ref.ref(globalObject.bunVM()); + } + + return true; + } + + pub fn write(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(3).slice(); + + return writeOrEnd(this, globalObject, arguments, false); + } + + pub fn end(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(3).slice(); + return writeOrEnd(this, globalObject, arguments, true); + } + + fn handleCorked(globalObject: *JSC.JSGlobalObject, function: JSC.JSValue, result: *JSValue, is_exception: *bool) void { + result.* = function.call(globalObject, .undefined, &.{}) catch |err| { + result.* = globalObject.takeException(err); + is_exception.* = true; + return; + }; + } + + export fn NodeHTTPResponse__setTimeout(this: *NodeHTTPResponse, seconds: JSC.JSValue, globalThis: *JSC.JSGlobalObject) void { + if (!seconds.isNumber()) { + _ = globalThis.throwInvalidArgumentTypeValue("timeout", "number", seconds); + return; + } + + if (this.finished or this.aborted) { + return; + } + + this.response.timeout(@intCast(@min(seconds.to(c_uint), 255))); + } + + pub fn cork(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len == 0 or !arguments[0].isCallable(globalObject.vm())) { + return globalObject.throwInvalidArgumentTypeValue("cork", "function", arguments[0]); + } + + if (this.finished or this.aborted) { + globalObject.ERR_STREAM_ALREADY_FINISHED("Stream is already ended", .{}).throw(); + return .zero; + } + + var result: JSC.JSValue = .zero; + var is_exception: bool = false; + this.ref(); + defer this.deref(); + + this.response.corked(handleCorked, .{ globalObject, arguments[0], &result, &is_exception }); + + if (is_exception) { + if (result != .zero) { + globalObject.throwValue(result); + } else { + globalObject.throw("unknown error", .{}); + } + return .zero; + } + + if (result == .zero) { + return .undefined; + } + + return result; + } + pub fn finalize(this: *NodeHTTPResponse) void { + this.clearJSValues(); + this.deref(); + } + + pub fn deinit(this: *NodeHTTPResponse) void { + bun.debugAssert(!this.body_read_ref.has); + bun.debugAssert(!this.js_ref.has); + bun.debugAssert(!this.is_request_pending); + bun.debugAssert(this.aborted or this.finished); + + this.js_ref.unref(JSC.VirtualMachine.get()); + this.body_read_ref.unref(JSC.VirtualMachine.get()); + this.onAbortedCallback.deinit(); + this.onDataCallback.deinit(); + this.onWritableCallback.deinit(); + this.promise.deinit(); + this.destroy(); + } + + comptime { + @export(create, .{ .name = "NodeHTTPResponse__createForJS" }); + } +}; + pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { return struct { pub const ssl_enabled = ssl_enabled_; @@ -5824,8 +6610,8 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp pub const doPublish = JSC.wrapInstanceMethod(ThisServer, "publish", false); pub const doReload = onReload; pub const doFetch = onFetch; - pub const doRequestIP = JSC.wrapInstanceMethod(ThisServer, "requestIP", false); - pub const doTimeout = JSC.wrapInstanceMethod(ThisServer, "timeout", false); + pub const doRequestIP = requestIP; + pub const doTimeout = timeout; pub fn doSubscriberCount(this: *ThisServer, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { const arguments = callframe.arguments(1); @@ -5862,28 +6648,70 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp extern fn JSSocketAddress__create(global: *JSC.JSGlobalObject, ip: JSValue, port: i32, is_ipv6: bool) JSValue; - pub fn requestIP(this: *ThisServer, request: *JSC.WebCore.Request) JSC.JSValue { + pub fn requestIP(this: *ThisServer, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len < 1 or arguments[0].isEmptyOrUndefinedOrNull()) { + globalObject.throwNotEnoughArguments("requestIP", 1, 0); + return .zero; + } + if (this.config.address == .unix) { return JSValue.jsNull(); } - return if (request.request_context.getRemoteSocketInfo()) |info| - JSSocketAddress__create( - this.globalThis, - bun.String.init(info.ip).toJS(this.globalThis), - info.port, - info.is_ipv6, - ) - else - JSValue.jsNull(); + + const info = brk: { + if (arguments[0].as(Request)) |request| { + if (request.request_context.getRemoteSocketInfo()) |info| + break :brk info; + } else if (arguments[0].as(NodeHTTPResponse)) |response| { + if (!response.finished) { + if (response.response.getRemoteSocketInfo()) |info| { + break :brk info; + } + } + } + + return JSC.JSValue.jsNull(); + }; + + return JSSocketAddress__create( + globalObject, + bun.String.init(info.ip).toJS(this.globalThis), + info.port, + info.is_ipv6, + ); } - pub fn timeout(this: *ThisServer, request: *JSC.WebCore.Request, seconds: JSValue) JSC.JSValue { + pub fn timeout(this: *ThisServer, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue { + const arguments = callframe.arguments(2).slice(); + if (arguments.len < 2 or arguments[0].isEmptyOrUndefinedOrNull()) { + globalObject.throwNotEnoughArguments("timeout", 2, arguments.len); + return .zero; + } + + const seconds = arguments[1]; + + if (this.config.address == .unix) { + return JSValue.jsNull(); + } + if (!seconds.isNumber()) { this.globalThis.throw("timeout() requires a number", .{}); return .zero; } const value = seconds.to(c_uint); - _ = request.request_context.setTimeout(value); + + if (arguments[0].as(Request)) |request| { + _ = request.request_context.setTimeout(value); + } else if (arguments[0].as(NodeHTTPResponse)) |response| { + if (!response.finished) { + _ = response.response.timeout(@intCast(@min(value, 255))); + } + } else { + this.globalThis.throwInvalidArguments("timeout() requires a Request object", .{}); + return .zero; + } + return JSValue.jsUndefined(); } @@ -6126,6 +6954,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.config.onRequest.unprotect(); this.config.onRequest = new_config.onRequest; } + if (this.config.onNodeHTTPRequest != new_config.onNodeHTTPRequest) { + this.config.onNodeHTTPRequest.unprotect(); + this.config.onNodeHTTPRequest = new_config.onNodeHTTPRequest; + } if (this.config.onError != new_config.onError) { this.config.onError.unprotect(); this.config.onError = new_config.onError; @@ -6587,8 +7419,13 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp httplog("scheduleDeinit", .{}); if (!this.flags.terminated) { + // App.close can cause finalizers to run. + // scheduleDeinit can be called inside a finalizer. + // Therefore, we split it into two tasks. this.flags.terminated = true; - this.app.close(); + const task = bun.default_allocator.create(JSC.AnyTask) catch unreachable; + task.* = JSC.AnyTask.New(App, App.close).init(this.app); + this.vm.enqueueTask(JSC.Task.init(task)); } const task = bun.default_allocator.create(JSC.AnyTask) catch unreachable; @@ -6849,6 +7686,143 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.pending_requests += 1; } + pub fn onNodeHTTPRequest( + this: *ThisServer, + req: *uws.Request, + resp: *App.Response, + ) void { + JSC.markBinding(@src()); + this.onPendingRequest(); + if (comptime Environment.isDebug) { + this.vm.eventLoop().debug.enter(); + } + defer { + if (comptime Environment.isDebug) { + this.vm.eventLoop().debug.exit(); + } + } + req.setYield(false); + resp.timeout(this.config.idleTimeout); + + const globalThis = this.globalThis; + const thisObject = this.thisObject; + const vm = this.vm; + + var node_http_response: ?*NodeHTTPResponse = null; + var is_async = false; + defer { + if (!is_async) { + if (node_http_response) |node_response| { + node_response.deref(); + } + } + } + + const result: JSValue = onNodeHTTPRequestFn( + @bitCast(AnyServer.from(this)), + globalThis, + thisObject, + this.config.onNodeHTTPRequest, + req, + resp, + &node_http_response, + ); + + const HTTPResult = union(enum) { + rejection: JSC.JSValue, + exception: JSC.JSValue, + success: void, + pending: JSC.JSValue, + }; + var strong_promise: JSC.Strong = .{}; + var needs_to_drain = true; + + defer { + if (needs_to_drain) { + vm.drainMicrotasks(); + } + } + defer strong_promise.deinit(); + const http_result: HTTPResult = brk: { + if (result.toError()) |err| { + break :brk .{ .exception = err }; + } + + if (result.asAnyPromise()) |promise| { + if (promise.status(globalThis.vm()) == .pending) { + strong_promise.set(globalThis, result); + needs_to_drain = false; + vm.drainMicrotasks(); + } + + switch (promise.status(globalThis.vm())) { + .fulfilled => { + globalThis.handleRejectedPromises(); + break :brk .{ .success = {} }; + }, + .rejected => { + promise.setHandled(globalThis.vm()); + break :brk .{ .rejection = promise.result(globalThis.vm()) }; + }, + .pending => { + globalThis.handleRejectedPromises(); + if (node_http_response) |node_response| { + if (node_response.finished or node_response.aborted) { + strong_promise.deinit(); + break :brk .{ .success = {} }; + } + + const strong_self = node_response.getThisValue(); + node_response.promise = strong_promise; + strong_promise = .{}; + result._then(globalThis, strong_self, NodeHTTPResponse.Bun__NodeHTTPRequest__onResolve, NodeHTTPResponse.Bun__NodeHTTPRequest__onReject); + is_async = true; + } + + break :brk .{ .pending = result }; + }, + } + } + + break :brk .{ .success = {} }; + }; + + switch (http_result) { + .exception, .rejection => |err| { + _ = vm.uncaughtException(globalThis, err, http_result == .rejection); + + if (node_http_response) |node_response| { + if (!node_response.finished and node_response.response.state().isResponsePending()) { + if (node_response.response.state().isHttpStatusCalled()) { + node_response.response.writeStatus("500 Internal Server Error"); + node_response.response.endWithoutBody(true); + } else { + node_response.response.endStream(true); + } + } + node_response.onRequestComplete(); + } + }, + .success => {}, + .pending => {}, + } + + if (node_http_response) |node_response| { + if (!node_response.finished and node_response.response.state().isResponsePending()) { + node_response.setOnAbortedHandler(); + } + // If we ended the response without attaching an ondata handler, we discard the body read stream + else if (http_result != .pending) { + node_response.maybeStopReadingBody(vm); + } + } + } + + const onNodeHTTPRequestFn = if (ssl_enabled) + NodeHTTPServer__onRequest_https + else + NodeHTTPServer__onRequest_http; + var did_send_idletimeout_warning_once = false; fn onTimeoutForIdleWarn(_: *anyopaque, _: *App.Response) void { if (debug_mode and !did_send_idletimeout_warning_once) { @@ -7095,7 +8069,12 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp ); } - this.app.any("/*", *ThisServer, this, onRequest); + if (this.config.onNodeHTTPRequest != .zero) { + this.app.any("/*", *ThisServer, this, onNodeHTTPRequest); + NodeHTTP_assignOnCloseFunction(@intFromBool(ssl_enabled), this.app); + } else if (this.config.onRequest != .zero) { + this.app.any("/*", *ThisServer, this, onRequest); + } if (comptime debug_mode) { this.app.get("/bun:info", *ThisServer, this, onBunInfoRequest); @@ -7218,43 +8197,57 @@ pub const HTTPServer = NewServer(JSC.Codegen.JSHTTPServer, false, false); pub const HTTPSServer = NewServer(JSC.Codegen.JSHTTPSServer, true, false); pub const DebugHTTPServer = NewServer(JSC.Codegen.JSDebugHTTPServer, false, true); pub const DebugHTTPSServer = NewServer(JSC.Codegen.JSDebugHTTPSServer, true, true); -const AnyServer = union(enum) { - HTTPServer: *HTTPServer, - HTTPSServer: *HTTPSServer, - DebugHTTPServer: *DebugHTTPServer, - DebugHTTPSServer: *DebugHTTPSServer, +const AnyServer = packed struct { + ptr: Ptr, + + const Ptr = bun.TaggedPointerUnion(.{ + HTTPServer, + HTTPSServer, + DebugHTTPServer, + DebugHTTPSServer, + }); pub fn config(this: AnyServer) *const ServerConfig { - return switch (this) { - inline else => |server| &server.config, + return switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => &this.ptr.as(HTTPServer).config, + Ptr.case(HTTPSServer) => &this.ptr.as(HTTPSServer).config, + Ptr.case(DebugHTTPServer) => &this.ptr.as(DebugHTTPServer).config, + Ptr.case(DebugHTTPSServer) => &this.ptr.as(DebugHTTPSServer).config, + else => bun.unreachablePanic("Invalid pointer tag", .{}), }; } pub fn from(server: anytype) AnyServer { - return switch (@TypeOf(server)) { - *HTTPServer => AnyServer{ .HTTPServer = server }, - *HTTPSServer => AnyServer{ .HTTPSServer = server }, - *DebugHTTPServer => AnyServer{ .DebugHTTPServer = server }, - *DebugHTTPSServer => AnyServer{ .DebugHTTPSServer = server }, - else => @compileError("Invalid server type"), - }; + return .{ .ptr = Ptr.init(server) }; } pub fn onPendingRequest(this: AnyServer) void { - switch (this) { - inline else => |server| server.onPendingRequest(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onPendingRequest(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onPendingRequest(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onPendingRequest(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onPendingRequest(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } pub fn onRequestComplete(this: AnyServer) void { - switch (this) { - inline else => |server| server.onRequestComplete(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestComplete(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestComplete(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestComplete(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestComplete(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } pub fn onStaticRequestComplete(this: AnyServer) void { - switch (this) { - inline else => |server| server.onStaticRequestComplete(), + switch (this.ptr.tag()) { + Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onStaticRequestComplete(), + Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onStaticRequestComplete(), + Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onStaticRequestComplete(), + Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onStaticRequestComplete(), + else => bun.unreachablePanic("Invalid pointer tag", .{}), } } }; @@ -7295,5 +8288,28 @@ pub export fn Server__setIdleTimeout( comptime { if (!JSC.is_bindgen) { _ = Server__setIdleTimeout; + _ = NodeHTTPResponse.create; } } + +extern fn NodeHTTPServer__onRequest_http( + any_server: u64, + globalThis: *JSC.JSGlobalObject, + this: JSC.JSValue, + callback: JSC.JSValue, + request: *uws.Request, + response: *uws.NewApp(false).Response, + node_response_ptr: *?*NodeHTTPResponse, +) JSC.JSValue; + +extern fn NodeHTTPServer__onRequest_https( + any_server: u64, + globalThis: *JSC.JSGlobalObject, + this: JSC.JSValue, + callback: JSC.JSValue, + request: *uws.Request, + response: *uws.NewApp(true).Response, + node_response_ptr: *?*NodeHTTPResponse, +) JSC.JSValue; + +extern fn NodeHTTP_assignOnCloseFunction(c_int, *anyopaque) void; diff --git a/src/bun.js/bindings/BunJSCEventLoop.cpp b/src/bun.js/bindings/BunJSCEventLoop.cpp new file mode 100644 index 0000000000000..7a3cf95b2a01e --- /dev/null +++ b/src/bun.js/bindings/BunJSCEventLoop.cpp @@ -0,0 +1,18 @@ +#include "root.h" + +#include +#include + +extern "C" int Bun__JSC_onBeforeWait(JSC::VM* vm) +{ + if (vm->heap.hasAccess()) { + vm->heap.releaseAccess(); + return 1; + } + return 0; +} + +extern "C" void Bun__JSC_onAfterWait(JSC::VM* vm) +{ + vm->heap.acquireAccess(); +} diff --git a/src/bun.js/bindings/CachedScript.h b/src/bun.js/bindings/CachedScript.h index 43a7e4ae6bc41..18d892c4030cf 100644 --- a/src/bun.js/bindings/CachedScript.h +++ b/src/bun.js/bindings/CachedScript.h @@ -1,7 +1,5 @@ #pragma once -#include "root.h" - namespace WebCore { class CachedScript { diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index a2b2bd96ecedd..99dd9492c8d4a 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -27,10 +27,10 @@ export default [ ["ERR_SERVER_NOT_RUNNING", Error, "Error"], ["ERR_SOCKET_BAD_TYPE", TypeError, "TypeError"], ["ERR_STREAM_ALREADY_FINISHED", TypeError, "TypeError"], - ["ERR_STREAM_CANNOT_PIPE", TypeError, "TypeError"], - ["ERR_STREAM_DESTROYED", TypeError, "TypeError"], + ["ERR_STREAM_CANNOT_PIPE", Error, "Error"], + ["ERR_STREAM_DESTROYED", Error, "Error"], ["ERR_STREAM_NULL_VALUES", TypeError, "TypeError"], - ["ERR_STREAM_WRITE_AFTER_END", TypeError, "TypeError"], + ["ERR_STREAM_WRITE_AFTER_END", Error, "Error"], ["ERR_ZLIB_INITIALIZATION_FAILED", Error, "Error"], ["ERR_STRING_TOO_LONG", Error, "Error"], ["ERR_CRYPTO_SCRYPT_INVALID_PARAMETER", Error, "Error"], @@ -45,7 +45,12 @@ export default [ ["ERR_BUFFER_OUT_OF_BOUNDS", RangeError, "RangeError"], ["ERR_UNKNOWN_SIGNAL", TypeError, "TypeError"], ["ERR_SOCKET_BAD_PORT", RangeError, "RangeError"], - + ["ERR_HTTP_HEADERS_SENT", Error, "Error"], + ["ERR_HTTP_BODY_NOT_ALLOWED", Error, "Error"], + ["ERR_HTTP_INVALID_STATUS_CODE", RangeError, "RangeError"], + ["ERR_HTTP_INVALID_HEADER_VALUE", TypeError, "TypeError"], + ["ERR_INVALID_CHAR", TypeError, "TypeError"], + ["ERR_METHOD_NOT_IMPLEMENTED", Error, "Error"], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError, "TypeError"], ["ERR_BODY_ALREADY_USED", Error, "Error"], diff --git a/src/bun.js/bindings/JSSocketAddress.cpp b/src/bun.js/bindings/JSSocketAddress.cpp index 1815073397954..8f478d4314374 100644 --- a/src/bun.js/bindings/JSSocketAddress.cpp +++ b/src/bun.js/bindings/JSSocketAddress.cpp @@ -1,5 +1,8 @@ -#include "JSSocketAddress.h" +#include "root.h" + #include "ZigGlobalObject.h" +#include "JSSocketAddress.h" + #include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/ObjectConstructor.h" #include "JavaScriptCore/JSCast.h" @@ -9,6 +12,21 @@ using namespace JSC; namespace Bun { namespace JSSocketAddress { +JSObject* create(Zig::GlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) +{ + static const NeverDestroyed IPv4 = MAKE_STATIC_STRING_IMPL("IPv4"); + static const NeverDestroyed IPv6 = MAKE_STATIC_STRING_IMPL("IPv6"); + + VM& vm = globalObject->vm(); + + JSObject* thisObject = constructEmptyObject(vm, globalObject->JSSocketAddressStructure()); + thisObject->putDirectOffset(vm, 0, value); + thisObject->putDirectOffset(vm, 1, isIPv6 ? jsString(vm, IPv6) : jsString(vm, IPv4)); + thisObject->putDirectOffset(vm, 2, jsNumber(port)); + + return thisObject; +} + // Using a structure with inlined offsets should be more lightweight than a class. Structure* createStructure(VM& vm, JSGlobalObject* globalObject) { @@ -45,19 +63,7 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) +extern "C" JSObject* JSSocketAddress__create(Zig::GlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) { - static const NeverDestroyed IPv4 = MAKE_STATIC_STRING_IMPL("IPv4"); - static const NeverDestroyed IPv6 = MAKE_STATIC_STRING_IMPL("IPv6"); - - VM& vm = globalObject->vm(); - - auto* global = jsCast(globalObject); - - JSObject* thisObject = constructEmptyObject(vm, global->JSSocketAddressStructure()); - thisObject->putDirectOffset(vm, 0, value); - thisObject->putDirectOffset(vm, 1, isIPv6 ? jsString(vm, IPv6) : jsString(vm, IPv4)); - thisObject->putDirectOffset(vm, 2, jsNumber(port)); - - return thisObject; + return Bun::JSSocketAddress::create(globalObject, value, port, isIPv6); } diff --git a/src/bun.js/bindings/JSSocketAddress.h b/src/bun.js/bindings/JSSocketAddress.h index 77bdca5d4fc94..8e6eed2eeea71 100644 --- a/src/bun.js/bindings/JSSocketAddress.h +++ b/src/bun.js/bindings/JSSocketAddress.h @@ -9,8 +9,9 @@ namespace Bun { namespace JSSocketAddress { Structure* createStructure(VM& vm, JSGlobalObject* globalObject); +JSObject* create(Zig::GlobalObject* globalObject, JSString* value, int port, bool isIPv6); } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int port, bool isIPv6); +extern "C" JSObject* JSSocketAddress__create(Zig::GlobalObject* globalObject, JSString* value, int port, bool isIPv6); diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index 2ff078e67d62e..d206005b57325 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -5,25 +5,420 @@ #include "helpers.h" #include "BunClientData.h" -#include "JavaScriptCore/AggregateError.h" -#include "JavaScriptCore/InternalFieldTuple.h" -#include "JavaScriptCore/ObjectConstructor.h" -#include "JavaScriptCore/ObjectConstructor.h" -#include "JavaScriptCore/JSFunction.h" +#include +#include +#include +#include +#include #include "wtf/URL.h" #include "JSFetchHeaders.h" #include "JSDOMExceptionHandling.h" #include #include "ZigGeneratedClasses.h" +#include "ScriptExecutionContext.h" +#include "AsyncContextFrame.h" +#include "ZigGeneratedClasses.h" +#include +#include +#include "JSSocketAddress.h" + +extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6); namespace Bun { using namespace JSC; using namespace WebCore; +JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, PropertyName propertyName)) +{ + return false; +} + +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose); +JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress); + +BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS); +JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex); +JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex); + +// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function +static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = { + { "onclose"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } }, + { "closed"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterClosed, noOpSetter } }, + { "response"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterResponse, noOpSetter } }, + { "duplex"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterDuplex, jsNodeHttpServerSocketSetterDuplex } }, + { "remoteAddress"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } }, + { "close"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } }, +}; + +class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSNodeHTTPServerSocketPrototype* create(VM& vm, Structure* structure) + { + JSNodeHTTPServerSocketPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodeHTTPServerSocketPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; + } + + DECLARE_INFO; + + static constexpr bool needsDestruction = false; + static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSNodeHTTPServerSocketPrototype, Base); + return &vm.plainObjectSpace(); + } + +private: + JSNodeHTTPServerSocketPrototype(VM& vm, Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(VM& vm) + { + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), JSNodeHTTPServerSocketPrototypeTableValues, *this); + this->structure()->setMayBePrototype(true); + } +}; + +class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static JSNodeHTTPServerSocket* create(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) + { + auto* object = new (JSC::allocateCell(vm)) JSNodeHTTPServerSocket(vm, structure, socket, is_ssl, response); + object->finishCreation(vm); + return object; + } + + static JSNodeHTTPServerSocket* create(JSC::VM& vm, Zig::GlobalObject* globalObject, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) + { + auto* structure = globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject); + return create(vm, structure, socket, is_ssl, response); + } + + static void destroy(JSC::JSCell* cell) + { + static_cast(cell)->JSNodeHTTPServerSocket::~JSNodeHTTPServerSocket(); + } + + template + static void clearSocketData(us_socket_t* socket) + { + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + if (httpResponseData->socketData) { + httpResponseData->socketData = nullptr; + } + } + + void close() + { + auto* socket = this->socket; + if (socket) { + us_socket_close(is_ssl, socket, 0, nullptr); + } + } + + bool isClosed() const + { + return !socket || us_socket_is_closed(is_ssl, socket); + } + + ~JSNodeHTTPServerSocket() + { + if (socket) { + if (is_ssl) { + clearSocketData(socket); + } else { + clearSocketData(socket); + } + } + } + + JSNodeHTTPServerSocket(JSC::VM& vm, JSC::Structure* structure, us_socket_t* socket, bool is_ssl, WebCore::JSNodeHTTPResponse* response) + : JSC::JSDestructibleObject(vm, structure) + , socket(socket) + , is_ssl(is_ssl) + { + currentResponseObject.setEarlyValue(vm, this, response); + } + + mutable WriteBarrier functionToCallOnClose; + mutable WriteBarrier currentResponseObject; + mutable WriteBarrier m_remoteAddress; + mutable WriteBarrier m_duplex; + + unsigned is_ssl : 1; + us_socket_t* socket; + JSC::Strong strongThis = {}; + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodeHTTPServerSocket = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNodeHTTPServerSocket.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodeHTTPServerSocket = std::forward(space); }); + } + + void onClose() + { + this->socket = nullptr; + this->m_duplex.clear(); + this->currentResponseObject.clear(); + + // This function can be called during GC! + Zig::GlobalObject* globalObject = static_cast(this->globalObject()); + if (!functionToCallOnClose) { + this->strongThis.clear(); + + return; + } + + WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext(); + + if (scriptExecutionContext) { + JSC::gcProtect(this); + scriptExecutionContext->postTask([self = this](ScriptExecutionContext& context) { + WTF::NakedPtr exception; + auto* globalObject = defaultGlobalObject(context.globalObject()); + auto* thisObject = self; + auto* callbackObject = thisObject->functionToCallOnClose.get(); + if (!callbackObject) { + JSC::gcUnprotect(self); + return; + } + auto callData = JSC::getCallData(callbackObject); + MarkedArgumentBuffer args; + EnsureStillAliveScope ensureStillAlive(self); + JSC::gcUnprotect(self); + + if (globalObject->scriptExecutionStatus(globalObject, thisObject) == ScriptExecutionStatus::Running) { + profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, thisObject, args, exception); + + if (auto* ptr = exception.get()) { + exception.clear(); + globalObject->reportUncaughtExceptionAtEventLoop(globalObject, ptr); + } + } + }); + } + + this->strongThis.clear(); + } + + static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + auto* structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), JSNodeHTTPServerSocketPrototype::info()); + auto* prototype = JSNodeHTTPServerSocketPrototype::create(vm, structure); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + void finishCreation(JSC::VM& vm) + { + Base::finishCreation(vm); + } +}; + +JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + return JSValue::encode(JSC::jsUndefined()); + } + if (thisObject->isClosed()) { + return JSValue::encode(JSC::jsUndefined()); + } + thisObject->close(); + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_duplex) { + return JSValue::encode(thisObject->m_duplex.get()); + } + return JSValue::encode(JSC::jsNull()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + if (auto* object = value.getObject()) { + thisObject->m_duplex.set(vm, thisObject, object); + + } else { + thisObject->m_duplex.clear(); + } + + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (thisObject->m_remoteAddress) { + return JSValue::encode(thisObject->m_remoteAddress.get()); + } + + us_socket_t* socket = thisObject->socket; + if (!socket) { + return JSValue::encode(JSC::jsNull()); + } + + const char* address = nullptr; + int port = 0; + bool is_ipv6 = false; + + uws_res_get_remote_address_info(socket, &address, &port, &is_ipv6); + + if (address == nullptr) { + return JSValue::encode(JSC::jsNull()); + } + + auto addressString = WTF::String::fromUTF8(address); + if (addressString.isEmpty()) { + return JSValue::encode(JSC::jsNull()); + } + + auto* object = JSSocketAddress::create(defaultGlobalObject(globalObject), jsString(vm, addressString), port, is_ipv6); + thisObject->m_remoteAddress.set(vm, thisObject, object); + return JSValue::encode(object); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + + if (thisObject->functionToCallOnClose) { + return JSValue::encode(thisObject->functionToCallOnClose.get()); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterOnClose, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName propertyName)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + JSValue value = JSC::JSValue::decode(encodedValue); + + if (value.isUndefined() || value.isNull()) { + thisObject->functionToCallOnClose.clear(); + return true; + } + + if (!value.isCallable()) { + return false; + } + + thisObject->functionToCallOnClose.set(vm, thisObject, value.getObject()); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterClosed, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsBoolean(thisObject->isClosed())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + if (!thisObject->currentResponseObject) { + return JSValue::encode(JSC::jsNull()); + } + + return JSValue::encode(thisObject->currentResponseObject.get()); +} + +template +void JSNodeHTTPServerSocket::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSNodeHTTPServerSocket* fn = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(fn, info()); + Base::visitChildren(fn, visitor); + + visitor.append(fn->currentResponseObject); + visitor.append(fn->functionToCallOnClose); + visitor.append(fn->m_remoteAddress); + visitor.append(fn->m_duplex); +} + +DEFINE_VISIT_CHILDREN(JSNodeHTTPServerSocket); + +template +static JSNodeHTTPServerSocket* getNodeHTTPServerSocket(us_socket_t* socket) +{ + auto* httpResponseData = (uWS::HttpResponseData*)us_socket_ext(SSL, socket); + return reinterpret_cast(httpResponseData->socketData); +} + +template +static WebCore::JSNodeHTTPResponse* getNodeHTTPResponse(us_socket_t* socket) +{ + auto* serverSocket = getNodeHTTPServerSocket(socket); + if (!serverSocket) { + return nullptr; + } + return serverSocket->currentResponseObject.get(); +} + +const JSC::ClassInfo JSNodeHTTPServerSocket::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocket) }; + +const JSC::ClassInfo JSNodeHTTPServerSocketPrototype::s_info = { "NodeHTTPServerSocket"_s, &Base::s_info, nullptr, nullptr, + CREATE_METHOD_TABLE(JSNodeHTTPServerSocketPrototype) }; + +template +static void* getNodeHTTPResponsePtr(us_socket_t* socket) +{ + WebCore::JSNodeHTTPResponse* responseObject = getNodeHTTPResponse(socket); + if (!responseObject) { + return nullptr; + } + return responseObject->wrapped(); +} + +extern "C" EncodedJSValue Bun__getNodeHTTPResponseThisValue(int is_ssl, us_socket_t* socket) +{ + if (is_ssl) { + return JSValue::encode(getNodeHTTPResponse(socket)); + } + return JSValue::encode(getNodeHTTPResponse(socket)); +} + +BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue); +BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer); extern "C" uWS::HttpRequest* Request__getUWSRequest(void*); extern "C" void Request__setInternalEventCallback(void*, EncodedJSValue, JSC::JSGlobalObject*); extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*); +extern "C" void NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*); extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*); static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm) { @@ -94,6 +489,164 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject return JSValue::encode(tuple); } +static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, MarkedArgumentBuffer& args, JSC::JSGlobalObject* globalObject, JSC::VM& vm) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + std::string_view fullURLStdStr = request->getFullUrl(); + String fullURL = String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast(fullURLStdStr.data()), fullURLStdStr.length() }); + + // Get the URL. + { + args.append(jsString(vm, fullURL)); + } + + // Get the method. + { + std::string_view methodView = request->getMethod(); + WTF::String methodString; + switch (methodView.length()) { + case 3: { + if (methodView == std::string_view("get", 3)) { + methodString = "GET"_s; + break; + } + if (methodView == std::string_view("put", 3)) { + methodString = "PUT"_s; + break; + } + + break; + } + case 4: { + if (methodView == std::string_view("post", 4)) { + methodString = "POST"_s; + break; + } + if (methodView == std::string_view("head", 4)) { + methodString = "HEAD"_s; + break; + } + + if (methodView == std::string_view("copy", 4)) { + methodString = "COPY"_s; + break; + } + } + + case 5: { + if (methodView == std::string_view("patch", 5)) { + methodString = "PATCH"_s; + break; + } + if (methodView == std::string_view("merge", 5)) { + methodString = "MERGE"_s; + break; + } + if (methodView == std::string_view("trace", 5)) { + methodString = "TRACE"_s; + break; + } + if (methodView == std::string_view("fetch", 5)) { + methodString = "FETCH"_s; + break; + } + if (methodView == std::string_view("purge", 5)) { + methodString = "PURGE"_s; + break; + } + + break; + } + + case 6: { + if (methodView == std::string_view("delete", 6)) { + methodString = "DELETE"_s; + break; + } + + break; + } + + case 7: { + if (methodView == std::string_view("connect", 7)) { + methodString = "CONNECT"_s; + break; + } + if (methodView == std::string_view("options", 7)) { + methodString = "OPTIONS"_s; + break; + } + + break; + } + } + + if (methodString.isNull()) { + methodString = String::fromUTF8ReplacingInvalidSequences({ reinterpret_cast(methodView.data()), methodView.length() }); + } + + args.append(jsString(vm, methodString)); + } + + size_t size = 0; + for (auto it = request->begin(); it != request->end(); ++it) { + size++; + } + + JSC::JSObject* headersObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(size, static_cast(JSFinalObject::maxInlineCapacity))); + RETURN_IF_EXCEPTION(scope, void()); + JSC::JSArray* array = constructEmptyArray(globalObject, nullptr, size * 2); + JSC::JSArray* setCookiesHeaderArray = nullptr; + JSC::JSString* setCookiesHeaderString = nullptr; + + args.append(headersObject); + args.append(array); + + unsigned i = 0; + + for (auto it = request->begin(); it != request->end(); ++it) { + auto pair = *it; + StringView nameView = StringView(std::span { reinterpret_cast(pair.first.data()), pair.first.length() }); + LChar* data = nullptr; + auto value = String::createUninitialized(pair.second.length(), data); + if (pair.second.length() > 0) + memcpy(data, pair.second.data(), pair.second.length()); + + HTTPHeaderName name; + WTF::String nameString; + WTF::String lowercasedNameString; + + if (WebCore::findHTTPHeaderName(nameView, name)) { + nameString = WTF::httpHeaderNameStringImpl(name); + lowercasedNameString = nameString; + } else { + nameString = nameView.toString(); + lowercasedNameString = nameString.convertToASCIILowercase(); + } + + JSString* jsValue = jsString(vm, value); + + if (name == WebCore::HTTPHeaderName::SetCookie) { + if (!setCookiesHeaderArray) { + setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr); + setCookiesHeaderString = jsString(vm, nameString); + headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), setCookiesHeaderArray, 0); + RETURN_IF_EXCEPTION(scope, void()); + } + array->putDirectIndex(globalObject, i++, setCookiesHeaderString); + array->putDirectIndex(globalObject, i++, jsValue); + setCookiesHeaderArray->push(globalObject, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + + } else { + headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), jsValue, 0); + array->putDirectIndex(globalObject, i++, jsString(vm, nameString)); + array->putDirectIndex(globalObject, i++, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + } + } +} + // This is an 8% speedup. static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm) { @@ -257,6 +810,297 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS return JSValue::encode(tuple); } +template +static void assignOnCloseFunction(uWS::TemplatedApp* app) +{ + app->setOnClose([](void* socketData, int is_ssl, struct us_socket_t* rawSocket) -> void { + auto* socket = reinterpret_cast(socketData); + ASSERT(rawSocket == socket->socket || socket->socket == nullptr); + socket->onClose(); + }); +} + +extern "C" void NodeHTTP_assignOnCloseFunction(int is_ssl, void* uws_app) +{ + if (is_ssl) { + assignOnCloseFunction(reinterpret_cast*>(uws_app)); + } else { + assignOnCloseFunction(reinterpret_cast*>(uws_app)); + } +} +extern "C" EncodedJSValue NodeHTTPResponse__createForJS(size_t any_server, JSC::JSGlobalObject* globalObject, int* hasBody, uWS::HttpRequest* request, int isSSL, void* response_ptr, void** nodeHttpResponsePtr); + +template +static EncodedJSValue NodeHTTPServer__onRequest( + size_t any_server, + Zig::GlobalObject* globalObject, + JSValue thisValue, + JSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSObject* callbackObject = jsCast(callback); + MarkedArgumentBuffer args; + args.append(thisValue); + + assignHeadersFromUWebSocketsForCall(request, args, globalObject, vm); + if (scope.exception()) { + auto* exception = scope.exception(); + response->endWithoutBody(); + scope.clearException(); + return JSValue::encode(exception); + } + + int hasBody = 0; + WebCore::JSNodeHTTPResponse* nodeHTTPResponseObject = jsCast(JSValue::decode(NodeHTTPResponse__createForJS(any_server, globalObject, &hasBody, request, isSSL, response, nodeHttpResponsePtr))); + + JSC::CallData callData = getCallData(callbackObject); + args.append(nodeHTTPResponseObject); + args.append(jsBoolean(hasBody)); + + auto* currentSocketDataPtr = reinterpret_cast(response->getHttpResponseData()->socketData); + + if (currentSocketDataPtr) { + auto* thisSocket = jsCast(currentSocketDataPtr); + thisSocket->currentResponseObject.set(vm, thisSocket, nodeHTTPResponseObject); + args.append(thisSocket); + args.append(jsBoolean(true)); + if (thisSocket->m_duplex) { + args.append(thisSocket->m_duplex.get()); + } else { + args.append(jsUndefined()); + } + } else { + JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create( + vm, + globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject), + (us_socket_t*)response, + isSSL, nodeHTTPResponseObject); + + socket->strongThis.set(vm, socket); + response->getHttpResponseData()->socketData = socket; + + args.append(socket); + args.append(jsBoolean(false)); + args.append(jsUndefined()); + } + + WTF::NakedPtr exception; + JSValue returnValue = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, callbackObject, callData, jsUndefined(), args, exception); + if (exception) { + auto* ptr = exception.get(); + exception.clear(); + return JSValue::encode(ptr); + } + + return JSValue::encode(returnValue); +} + +template +static void writeResponseHeader(uWS::HttpResponse* res, const WTF::StringView& name, const WTF::StringView& value) +{ + WTF::CString nameStr; + WTF::CString valueStr; + + std::string_view nameView; + std::string_view valueView; + + if (name.is8Bit()) { + const auto nameSpan = name.span8(); + ASSERT(name.containsOnlyASCII()); + nameView = std::string_view(reinterpret_cast(nameSpan.data()), nameSpan.size()); + } else { + nameStr = name.utf8(); + nameView = std::string_view(nameStr.data(), nameStr.length()); + } + + if (value.is8Bit()) { + const auto valueSpan = value.span8(); + valueView = std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size()); + } else { + valueStr = value.utf8(); + valueView = std::string_view(valueStr.data(), valueStr.length()); + } + + res->writeHeader(nameView, valueView); +} + +template +static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::HttpResponse* res) +{ + auto& internalHeaders = headers.internalHeaders(); + + for (auto& value : internalHeaders.getSetCookieHeaders()) { + + if (value.is8Bit()) { + const auto valueSpan = value.span8(); + res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size())); + } else { + WTF::CString valueStr = value.utf8(); + res->writeHeader(std::string_view("set-cookie", 10), std::string_view(valueStr.data(), valueStr.length())); + } + } + + auto* data = res->getHttpResponseData(); + + for (const auto& header : internalHeaders.commonHeaders()) { + + const auto& name = WebCore::httpHeaderNameString(header.key); + const auto& value = header.value; + + // We have to tell uWS not to automatically insert a TransferEncoding or Date header. + // Otherwise, you get this when using Fastify; + // + // ❯ curl http://localhost:3000 --verbose + // * Trying [::1]:3000... + // * Connected to localhost (::1) port 3000 + // > GET / HTTP/1.1 + // > Host: localhost:3000 + // > User-Agent: curl/8.4.0 + // > Accept: */* + // > + // < HTTP/1.1 200 OK + // < Content-Type: application/json; charset=utf-8 + // < Content-Length: 17 + // < Date: Sun, 06 Oct 2024 13:37:01 GMT + // < Transfer-Encoding: chunked + // < + // + if (header.key == WebCore::HTTPHeaderName::ContentLength) { + if (!(data->state & uWS::HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER)) { + data->state |= uWS::HttpResponseData::HTTP_WROTE_CONTENT_LENGTH_HEADER; + res->writeMark(); + } + } + writeResponseHeader(res, name, value); + } + + for (auto& header : internalHeaders.uncommonHeaders()) { + const auto& name = header.key; + const auto& value = header.value; + + writeResponseHeader(res, name, value); + } +} + +template +static void NodeHTTPServer__writeHead( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSObject* headersObject = headersObjectValue.getObject(); + if (response->getLoopData()->canCork() && response->getBufferedAmount() == 0) { + response->getLoopData()->setCorkedSocket(response, isSSL); + } + response->writeStatus(std::string_view(statusMessage, statusMessageLength)); + + if (headersObject) { + if (auto* fetchHeaders = jsDynamicCast(headersObject)) { + writeFetchHeadersToUWSResponse(fetchHeaders->wrapped(), response); + return; + } + + if (UNLIKELY(headersObject->hasNonReifiedStaticProperties())) { + headersObject->reifyAllStaticProperties(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + } + + auto* structure = headersObject->structure(); + + if (structure->canPerformFastPropertyEnumeration()) { + structure->forEachProperty(vm, [&](const auto& entry) { + JSValue headerValue = headersObject->getDirect(entry.offset()); + if (!headerValue.isString()) { + + return true; + } + + String key = entry.key(); + String value = headerValue.toWTFString(globalObject); + if (scope.exception()) { + return false; + } + + writeResponseHeader(response, key, value); + + return true; + }); + } else { + PropertyNameArray propertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + headersObject->getOwnPropertyNames(headersObject, globalObject, propertyNames, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(scope, void()); + + for (unsigned i = 0; i < propertyNames.size(); ++i) { + JSValue headerValue = headersObject->getIfPropertyExists(globalObject, propertyNames[i]); + if (!headerValue.isString()) { + continue; + } + + String key = propertyNames[i].string(); + String value = headerValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + writeResponseHeader(response, key, value); + } + } + } + + RELEASE_AND_RETURN(scope, void()); +} + +extern "C" void NodeHTTPServer__writeHead_http( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + return NodeHTTPServer__writeHead(globalObject, statusMessage, statusMessageLength, headersObjectValue, response); +} + +extern "C" void NodeHTTPServer__writeHead_https( + JSC::JSGlobalObject* globalObject, + const char* statusMessage, + size_t statusMessageLength, + JSValue headersObjectValue, + uWS::HttpResponse* response) +{ + return NodeHTTPServer__writeHead(globalObject, statusMessage, statusMessageLength, headersObjectValue, response); +} + +extern "C" EncodedJSValue NodeHTTPServer__onRequest_http( + size_t any_server, + Zig::GlobalObject* globalObject, + EncodedJSValue thisValue, + EncodedJSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + return NodeHTTPServer__onRequest(any_server, globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); +} + +extern "C" EncodedJSValue NodeHTTPServer__onRequest_https( + size_t any_server, + Zig::GlobalObject* globalObject, + EncodedJSValue thisValue, + EncodedJSValue callback, + uWS::HttpRequest* request, + uWS::HttpResponse* response, + void** nodeHttpResponsePtr) +{ + return NodeHTTPServer__onRequest(any_server, globalObject, JSValue::decode(thisValue), JSValue::decode(callback), request, response, nodeHttpResponsePtr); +} + JSC_DEFINE_HOST_FUNCTION(jsHTTPAssignHeaders, (JSGlobalObject * globalObject, CallFrame* callFrame)) { auto& vm = globalObject->vm(); @@ -357,6 +1201,10 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetTimeout, (JSGlobalObject * globalObject, CallF Request__setTimeout(jsRequest->wrapped(), JSValue::encode(seconds), globalObject); } + if (auto* nodeHttpResponse = jsDynamicCast(requestValue)) { + NodeHTTPResponse__setTimeout(nodeHttpResponse->wrapped(), JSValue::encode(seconds), globalObject); + } + return JSValue::encode(jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -415,14 +1263,15 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr auto scope = DECLARE_THROW_SCOPE(vm); JSValue headersValue = callFrame->argument(0); + JSValue nameValue = callFrame->argument(1); + JSValue valueValue = callFrame->argument(2); if (auto* headers = jsDynamicCast(headersValue)) { - JSValue nameValue = callFrame->argument(1); + if (nameValue.isString()) { String name = nameValue.toWTFString(globalObject); FetchHeaders* impl = &headers->wrapped(); - JSValue valueValue = callFrame->argument(2); if (valueValue.isUndefined()) return JSValue::encode(jsUndefined()); @@ -433,23 +1282,28 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr JSValue item = array->getIndex(globalObject, 0); if (UNLIKELY(scope.exception())) return JSValue::encode(jsUndefined()); - impl->set(name, item.getString(globalObject)); + + auto value = item.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->set(name, value); RETURN_IF_EXCEPTION(scope, {}); } for (unsigned i = 1; i < length; ++i) { JSValue value = array->getIndex(globalObject, i); if (UNLIKELY(scope.exception())) return JSValue::encode(jsUndefined()); - if (!value.isString()) - continue; - impl->append(name, value.getString(globalObject)); + auto string = value.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->append(name, string); RETURN_IF_EXCEPTION(scope, {}); } RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); return JSValue::encode(jsUndefined()); } - impl->set(name, valueValue.getString(globalObject)); + auto value = valueValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + impl->set(name, value); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(jsUndefined()); } @@ -497,7 +1351,31 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "headersTuple"_s)), JSC::InternalFieldTuple::create(vm, globalObject->m_internalFieldTupleStructure.get()), 0); + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "webRequestOrResponseHasBodyValue"_s)), + 1, jsFunctionRequestOrResponseHasBodyValue, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); + + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "getCompleteWebRequestOrResponseBodyValueAsArrayBuffer"_s)), + 1, jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); + obj->putDirectNativeFunction( + vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "drainMicrotasks"_s)), + 0, Bun__drainMicrotasksFromJS, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); return obj; } +extern "C" void WebCore__FetchHeaders__toUWSResponse(WebCore::FetchHeaders* arg0, bool is_ssl, void* arg2) +{ + if (is_ssl) { + writeFetchHeadersToUWSResponse(*arg0, reinterpret_cast*>(arg2)); + } else { + writeFetchHeadersToUWSResponse(*arg0, reinterpret_cast*>(arg2)); + } +} + +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + return JSNodeHTTPServerSocket::createStructure(vm, globalObject); +} + } // namespace Bun diff --git a/src/bun.js/bindings/NodeHTTP.h b/src/bun.js/bindings/NodeHTTP.h index d953e3b0f58be..8035cc216c846 100644 --- a/src/bun.js/bindings/NodeHTTP.h +++ b/src/bun.js/bindings/NodeHTTP.h @@ -6,6 +6,7 @@ JSC_DECLARE_HOST_FUNCTION(jsHTTPAssignHeaders); JSC_DECLARE_HOST_FUNCTION(jsHTTPGetHeader); JSC_DECLARE_HOST_FUNCTION(jsHTTPSetHeader); +JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); JSC::JSValue createNodeHTTPInternalBinding(Zig::GlobalObject*); } diff --git a/src/bun.js/bindings/Strong.cpp b/src/bun.js/bindings/Strong.cpp index 20728656e673e..c5fc34f24077f 100644 --- a/src/bun.js/bindings/Strong.cpp +++ b/src/bun.js/bindings/Strong.cpp @@ -17,6 +17,7 @@ WTF_MAKE_ISO_ALLOCATED_IMPL(StrongRef); extern "C" void Bun__StrongRef__delete(Bun::StrongRef* strongRef) { + strongRef->m_cell.clear(); delete strongRef; } diff --git a/src/bun.js/bindings/Strong.h b/src/bun.js/bindings/Strong.h index f8b2bd7b01179..00f3f58f53fb6 100644 --- a/src/bun.js/bindings/Strong.h +++ b/src/bun.js/bindings/Strong.h @@ -28,6 +28,11 @@ class StrongRef { { } + ~StrongRef() + { + m_cell.clear(); + } + JSC::Strong m_cell; }; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index a4598fb061e8a..7adc0345f72ff 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2741,6 +2741,10 @@ void GlobalObject::finishCreation(VM& vm) m_http2_commongStrings.initialize(); Bun::addNodeModuleConstructorProperties(vm, this); + m_JSNodeHTTPServerSocketStructure.initLater( + [](const Initializer& init) { + init.set(Bun::createNodeHTTPServerSocketStructure(init.vm, init.owner)); + }); m_lazyStackCustomGetterSetter.initLater( [](const Initializer& init) { @@ -3677,6 +3681,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSBufferClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); thisObject->m_JSBufferSubclassStructure.visit(visitor); + thisObject->m_JSNodeHTTPServerSocketStructure.visit(visitor); thisObject->m_JSCryptoKey.visit(visitor); thisObject->m_lazyStackCustomGetterSetter.visit(visitor); thisObject->m_JSDOMFileConstructor.visit(visitor); @@ -4172,6 +4177,7 @@ JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGloba GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction handler) { + if (handler == Bun__HTTPRequestContext__onReject) { return GlobalObject::PromiseFunctions::Bun__HTTPRequestContext__onReject; } else if (handler == Bun__HTTPRequestContext__onRejectStream) { @@ -4224,6 +4230,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::Bun__onResolveEntryPointResult; } else if (handler == Bun__onRejectEntryPointResult) { return GlobalObject::PromiseFunctions::Bun__onRejectEntryPointResult; + } else if (handler == Bun__NodeHTTPRequest__onResolve) { + return GlobalObject::PromiseFunctions::Bun__NodeHTTPRequest__onResolve; + } else if (handler == Bun__NodeHTTPRequest__onReject) { + return GlobalObject::PromiseFunctions::Bun__NodeHTTPRequest__onReject; } else { RELEASE_ASSERT_NOT_REACHED(); } diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 87ed6d6330133..4fe758aba9e59 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -354,8 +354,10 @@ class GlobalObject : public Bun::GlobalScope { Bun__BodyValueBufferer__onResolveStream, Bun__onResolveEntryPointResult, Bun__onRejectEntryPointResult, + Bun__NodeHTTPRequest__onResolve, + Bun__NodeHTTPRequest__onReject, }; - static constexpr size_t promiseFunctionsSize = 24; + static constexpr size_t promiseFunctionsSize = 25; static PromiseFunctions promiseHandlerID(SYSV_ABI EncodedJSValue (*handler)(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1)); @@ -589,6 +591,8 @@ class GlobalObject : public Bun::GlobalScope { LazyProperty m_processObject; LazyProperty m_lazyStackCustomGetterSetter; + LazyProperty m_JSNodeHTTPServerSocketStructure; + bool hasOverridenModuleResolveFilenameFunction = false; private: diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 0c1edd2273fff..9d3c248a0a77f 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -137,65 +137,6 @@ static WTF::StringView StringView_slice(WTF::StringView sv, unsigned start, unsi return sv.substring(start, end - start); } -template -static void writeResponseHeader(UWSResponse* res, const WTF::StringView& name, const WTF::StringView& value) -{ - WTF::CString nameStr; - WTF::CString valueStr; - - std::string_view nameView; - std::string_view valueView; - - if (name.is8Bit()) { - const auto nameSpan = name.span8(); - nameView = std::string_view(reinterpret_cast(nameSpan.data()), nameSpan.size()); - } else { - nameStr = name.utf8(); - nameView = std::string_view(nameStr.data(), nameStr.length()); - } - - if (value.is8Bit()) { - const auto valueSpan = value.span8(); - valueView = std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size()); - } else { - valueStr = value.utf8(); - valueView = std::string_view(valueStr.data(), valueStr.length()); - } - - res->writeHeader(nameView, valueView); -} - -template -static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res) -{ - auto& internalHeaders = headers->internalHeaders(); - - for (auto& value : internalHeaders.getSetCookieHeaders()) { - - if (value.is8Bit()) { - const auto valueSpan = value.span8(); - res->writeHeader(std::string_view("set-cookie", 10), std::string_view(reinterpret_cast(valueSpan.data()), valueSpan.size())); - } else { - WTF::CString valueStr = value.utf8(); - res->writeHeader(std::string_view("set-cookie", 10), std::string_view(valueStr.data(), valueStr.length())); - } - } - - for (const auto& header : internalHeaders.commonHeaders()) { - const auto& name = WebCore::httpHeaderNameString(header.key); - const auto& value = header.value; - - writeResponseHeader(res, name, value); - } - - for (auto& header : internalHeaders.uncommonHeaders()) { - const auto& name = header.key; - const auto& value = header.value; - - writeResponseHeader(res, name, value); - } -} - using namespace JSC; using namespace WebCore; @@ -1365,15 +1306,6 @@ bool WebCore__FetchHeaders__isEmpty(WebCore__FetchHeaders* arg0) return arg0->size() == 0; } -void WebCore__FetchHeaders__toUWSResponse(WebCore__FetchHeaders* arg0, bool is_ssl, void* arg2) -{ - if (is_ssl) { - copyToUWS>(arg0, reinterpret_cast*>(arg2)); - } else { - copyToUWS>(arg0, reinterpret_cast*>(arg2)); - } -} - WebCore__FetchHeaders* WebCore__FetchHeaders__createEmpty() { auto* headers = new WebCore::FetchHeaders({ WebCore::FetchHeaders::Guard::None, {} }); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 4e709b0394ad4..36646b248df3c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6159,6 +6159,22 @@ pub const VM = extern struct { LargeHeap = 1, }; + extern fn Bun__JSC_onBeforeWait(vm: *VM) i32; + extern fn Bun__JSC_onAfterWait(vm: *VM) void; + pub const ReleaseHeapAccess = struct { + vm: *VM, + needs_to_release: bool, + pub fn acquire(this: *const ReleaseHeapAccess) void { + if (this.needs_to_release) { + Bun__JSC_onAfterWait(this.vm); + } + } + }; + + pub fn releaseHeapAccess(vm: *VM) ReleaseHeapAccess { + return .{ .vm = vm, .needs_to_release = Bun__JSC_onBeforeWait(vm) != 0 }; + } + pub fn create(heap_type: HeapType) *VM { return cppFn("create", .{@intFromEnum(heap_type)}); } diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index f32a6a9b19b00..192317c0f60d3 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -76,4 +76,5 @@ pub const Classes = struct { pub const TextEncoderStreamEncoder = JSC.WebCore.TextEncoderStreamEncoder; pub const NativeZlib = JSC.API.NativeZlib; pub const NativeBrotli = JSC.API.NativeBrotli; + pub const NodeHTTPResponse = JSC.API.NodeHTTPResponse; }; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 6d5efdb701743..4348468805304 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -790,6 +790,9 @@ BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onRejectStream); BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onResolve); BUN_DECLARE_HOST_FUNCTION(Bun__HTTPRequestContext__onResolveStream); +BUN_DECLARE_HOST_FUNCTION(Bun__NodeHTTPRequest__onResolve); +BUN_DECLARE_HOST_FUNCTION(Bun__NodeHTTPRequest__onReject); + #endif #ifdef __cplusplus diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 3ba6b4c6e2efc..d4f177c1ccade 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -56,6 +56,7 @@ class DOMClientIsoSubspaces { std::unique_ptr m_clientSubspaceForHandleScopeBuffer; std::unique_ptr m_clientSubspaceForFunctionTemplate; std::unique_ptr m_clientSubspaceForV8Function; + std::unique_ptr m_clientSubspaceForJSNodeHTTPServerSocket; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index e86f18bfd52e4..cee6e4a3fcf50 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -56,6 +56,7 @@ class DOMIsoSubspaces { std::unique_ptr m_subspaceForHandleScopeBuffer; std::unique_ptr m_subspaceForFunctionTemplate; std::unique_ptr m_subspaceForV8Function; + std::unique_ptr m_subspaceForJSNodeHTTPServerSocket; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ @@ -898,7 +899,7 @@ class DOMIsoSubspaces { // std::unique_ptr m_subspaceForXPathNSResolver; // std::unique_ptr m_subspaceForXPathResult; // std::unique_ptr m_subspaceForXSLTProcessor; - + std::unique_ptr m_subspaceForBakeGlobalScope; std::unique_ptr m_subspaceForAbortController; diff --git a/src/bun.js/bindings/webcore/JSTextEncoder.cpp b/src/bun.js/bindings/webcore/JSTextEncoder.cpp index d886ce16e342e..ec28c91670fd2 100644 --- a/src/bun.js/bindings/webcore/JSTextEncoder.cpp +++ b/src/bun.js/bindings/webcore/JSTextEncoder.cpp @@ -375,10 +375,9 @@ static inline JSC::EncodedJSValue jsTextEncoderPrototypeFunction_encodeBody(JSC: { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); EnsureStillAliveScope argument0 = callFrame->argument(0); - JSC::JSString* input = argument0.value().toStringOrNull(lexicalGlobalObject); + JSC::JSString* input = argument0.value().toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); JSC::EncodedJSValue res; String str; if (input->is8Bit()) { @@ -397,11 +396,6 @@ static inline JSC::EncodedJSValue jsTextEncoderPrototypeFunction_encodeBody(JSC: res = TextEncoder__encode16(lexicalGlobalObject, str.span16().data(), str.length()); } - if (UNLIKELY(JSC::JSValue::decode(res).isObject() && JSC::JSValue::decode(res).getObject()->isErrorInstance())) { - throwScope.throwException(lexicalGlobalObject, JSC::JSValue::decode(res)); - return {}; - } - RELEASE_AND_RETURN(throwScope, res); } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index bca57c7f3e9e9..27ee38eb41d4f 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -406,84 +406,84 @@ const ServerAllConnectionsClosedTask = @import("./api/server.zig").ServerAllConn // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ - FetchTasklet, + Access, + AnyTask, + AppendFile, AsyncGlobWalkTask, AsyncTransformTask, - ReadFileTask, + bun.bake.DevServer.HotReloadTask, + bun.shell.Interpreter.Builtin.Yes.YesTask, + Chmod, + Chown, + Close, + CopyFile, CopyFilePromiseTask, - WriteFileTask, - AnyTask, - ManagedTask, - ShellIOReaderAsyncDeinit, - ShellIOWriterAsyncDeinit, - napi_async_work, - ThreadSafeFunction, CppTask, - HotReloadTask, - PollPendingModulesTask, - GetAddrInfoRequestTask, + Exists, + Fchmod, + FChown, + Fdatasync, + FetchTasklet, + Fstat, FSWatchTask, + Fsync, + FTruncate, + Futimes, + GetAddrInfoRequestTask, + HotReloadTask, JSCDeferredWorkTask, - Stat, + Lchmod, + Lchown, + Link, Lstat, - Fstat, + Lutimes, + ManagedTask, + Mkdir, + Mkdtemp, + napi_async_work, + NativeBrotli, + NativeZlib, Open, - ReadFile, - WriteFile, - CopyFile, + PollPendingModulesTask, + ProcessWaiterThreadTask, Read, - Write, - Truncate, - FTruncate, Readdir, ReaddirRecursive, - Close, - Rm, - Rmdir, - Chown, - FChown, - Utimes, - Lutimes, - Chmod, - Fchmod, - Link, - Symlink, + ReadFile, + ReadFileTask, Readlink, - Realpath, - Mkdir, - Fsync, - Fdatasync, - Writev, Readv, + Realpath, Rename, - Access, - AppendFile, - Mkdtemp, - Exists, - Futimes, - Lchmod, - Lchown, - Unlink, - NativeZlib, - NativeBrotli, + Rm, + Rmdir, + RuntimeTranspilerStore, + ServerAllConnectionsClosedTask, + ShellAsync, + ShellAsyncSubprocessDone, + ShellCondExprStatTask, + ShellCpTask, ShellGlobTask, - ShellRmTask, - ShellRmDirTask, - ShellMvCheckTargetTask, - ShellMvBatchedTask, + ShellIOReaderAsyncDeinit, + ShellIOWriterAsyncDeinit, ShellLsTask, ShellMkdirTask, + ShellMvBatchedTask, + ShellMvCheckTargetTask, + ShellRmDirTask, + ShellRmTask, ShellTouchTask, - ShellCpTask, - ShellCondExprStatTask, - ShellAsync, - ShellAsyncSubprocessDone, + Stat, + Symlink, + ThreadSafeFunction, TimerObject, - bun.shell.Interpreter.Builtin.Yes.YesTask, - ProcessWaiterThreadTask, - RuntimeTranspilerStore, - ServerAllConnectionsClosedTask, - bun.bake.DevServer.HotReloadTask, + Truncate, + Unlink, + Utimes, + Write, + WriteFile, + WriteFileTask, + Writev, }); const UnboundedQueue = @import("./unbounded_queue.zig").UnboundedQueue; pub const ConcurrentTask = struct { @@ -545,6 +545,7 @@ pub const GarbageCollectionController = struct { const actual = uws.Loop.get(); this.gc_timer = uws.Timer.createFallthrough(actual, this); this.gc_repeating_timer = uws.Timer.createFallthrough(actual, this); + actual.internal_loop_data.jsc_vm = vm.jsc; if (comptime Environment.isDebug) { if (bun.getenvZ("BUN_TRACK_LAST_FN_NAME") != null) { @@ -821,7 +822,10 @@ pub const EventLoop = struct { defer this.debug.exit(); if (count == 1) { - this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); + const vm = this.virtual_machine; + const global = this.global; + const jsc = vm.jsc; + this.drainTasks(vm, global, jsc); } this.entered_event_loop_count -= 1; @@ -926,6 +930,7 @@ pub const EventLoop = struct { } while (@field(this, queue_name).readItem()) |task| { + log("run {s}", .{@tagName(task.tag())}); defer counter += 1; switch (task.tag()) { @field(Task.Tag, typeBaseName(@typeName(ShellAsync))) => { @@ -1479,6 +1484,19 @@ pub const EventLoop = struct { this.virtual_machine.gc_controller.processGCTimer(); } + pub fn drainTasks(this: *EventLoop, ctx: *JSC.VirtualMachine, global: *JSC.JSGlobalObject, js_vm: *JSC.VM) void { + while (true) { + while (this.tickWithCount(ctx) > 0) : (global.handleRejectedPromises()) { + this.tickConcurrent(); + } else { + this.drainMicrotasksWithGlobal(global, js_vm); + this.tickConcurrent(); + if (this.tasks.count > 0) continue; + } + break; + } + } + pub fn tick(this: *EventLoop) void { JSC.markBinding(@src()); { @@ -1496,16 +1514,7 @@ pub const EventLoop = struct { const global = ctx.global; const global_vm = ctx.jsc; - while (true) { - while (this.tickWithCount(ctx) > 0) : (this.global.handleRejectedPromises()) { - this.tickConcurrent(); - } else { - this.drainMicrotasksWithGlobal(global, global_vm); - this.tickConcurrent(); - if (this.tasks.count > 0) continue; - } - break; - } + this.drainTasks(ctx, global, global_vm); while (this.tickWithCount(ctx) > 0) { this.tickConcurrent(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 4e546ef578727..33380d83426ac 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2561,8 +2561,14 @@ pub const VirtualMachine = struct { pub const main_file_name: string = "bun:main"; + pub export fn Bun__drainMicrotasksFromJS(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = callframe; // autofix + globalObject.bunVM().drainMicrotasks(); + return .undefined; + } + pub fn drainMicrotasks(this: *VirtualMachine) void { - this.eventLoop().drainMicrotasks(); + this.eventLoop().drainTasks(this, this.global, this.jsc); } pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, referrer: bun.String, log: *logger.Log, ret: *ErrorableResolvedSource, err: anyerror) void { diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index edab5fba41ea3..1bcaa39d770f8 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1646,6 +1646,8 @@ pub const ModuleLoader = struct { var parse_result: ParseResult = switch (disable_transpilying or (loader == .json and !path.isJSONCFile())) { inline else => |return_file_only| brk: { + const heap_access = if (!disable_transpilying) jsc_vm.jsc.releaseHeapAccess() else JSC.VM.ReleaseHeapAccess{ .vm = jsc_vm.jsc, .needs_to_release = false }; + defer heap_access.acquire(); break :brk jsc_vm.bundler.parseMaybeReturnFileOnly( parse_options, null, diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index 5337a6427146b..02aca9890f107 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -114,35 +114,25 @@ pub fn CompressionStream(comptime T: type) type { // const vm = globalThis.bunVM(); - var task = AsyncJob.new(.{ - .binding = this, - }); + this.task = .{ .callback = &AsyncJob.runTask }; this.poll_ref.ref(vm); - JSC.WorkPool.schedule(&task.task); + JSC.WorkPool.schedule(&this.task); return .undefined; } const AsyncJob = struct { - task: JSC.WorkPoolTask = .{ .callback = &runTask }, - binding: *T, - - pub usingnamespace bun.New(@This()); - - pub fn runTask(this: *JSC.WorkPoolTask) void { - var job: *AsyncJob = @fieldParentPtr("task", this); - job.run(); - job.destroy(); + pub fn runTask(task: *JSC.WorkPoolTask) void { + const this: *T = @fieldParentPtr("task", task); + AsyncJob.run(this); } - pub fn run(job: *AsyncJob) void { - const this = job.binding; + pub fn run(this: *T) void { const globalThis: *JSC.JSGlobalObject = this.globalThis; const vm = globalThis.bunVMConcurrently(); this.stream.doWork(); - this.poll_ref.refConcurrently(vm); vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this))); } }; @@ -294,6 +284,29 @@ pub fn CompressionStream(comptime T: type) type { pub const NativeZlib = JSC.Codegen.JSNativeZlib.getConstructor; +const CountedKeepAlive = struct { + keep_alive: bun.Async.KeepAlive = .{}, + ref_count: u32 = 0, + + pub fn ref(this: *@This(), vm: *JSC.VirtualMachine) void { + if (this.ref_count == 0) { + this.keep_alive.ref(vm); + } + this.ref_count += 1; + } + + pub fn unref(this: *@This(), vm: *JSC.VirtualMachine) void { + this.ref_count -= 1; + if (this.ref_count == 0) { + this.keep_alive.unref(vm); + } + } + + pub fn deinit(this: *@This()) void { + this.keep_alive.disable(); + } +}; + pub const SNativeZlib = struct { pub usingnamespace bun.NewRefCounted(@This(), deinit); pub usingnamespace JSC.Codegen.JSNativeZlib; @@ -306,11 +319,12 @@ pub const SNativeZlib = struct { write_result: ?[*]u32 = null, write_callback: JSC.Strong = .{}, onerror_value: JSC.Strong = .{}, - poll_ref: bun.Async.KeepAlive = .{}, + poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, pending_close: bool = false, closed: bool = false, + task: JSC.WorkPoolTask = .{ .callback = undefined }, pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() { const arguments = callframe.argumentsUndef(4).ptr; @@ -392,6 +406,7 @@ pub const SNativeZlib = struct { pub fn deinit(this: *@This()) void { this.write_callback.deinit(); this.onerror_value.deinit(); + this.poll_ref.deinit(); this.destroy(); } }; @@ -662,11 +677,14 @@ pub const SNativeBrotli = struct { write_result: ?[*]u32 = null, write_callback: JSC.Strong = .{}, onerror_value: JSC.Strong = .{}, - poll_ref: bun.Async.KeepAlive = .{}, + poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, pending_close: bool = false, closed: bool = false, + task: JSC.WorkPoolTask = .{ + .callback = undefined, + }, pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() { const arguments = callframe.argumentsUndef(1).ptr; @@ -752,6 +770,7 @@ pub const SNativeBrotli = struct { pub fn deinit(this: *@This()) void { this.write_callback.deinit(); this.onerror_value.deinit(); + this.poll_ref.deinit(); this.destroy(); } }; diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 3e95bf26974c7..9f2b4ee1504e2 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -299,6 +299,20 @@ pub const Body = struct { Error: ValueError, Null, + // We may not have all the data yet + // So we can't know for sure if it's empty or not + // We CAN know that it is definitely empty. + pub fn isDefinitelyEmpty(this: *const Value) bool { + return switch (this.*) { + .Null => true, + .Used, .Empty => true, + .InternalBlob => this.InternalBlob.slice().len == 0, + .Blob => this.Blob.size == 0, + .WTFStringImpl => this.WTFStringImpl.length() == 0, + .Error, .Locked => false, + }; + } + pub const heap_breakdown_label = "BodyValue"; pub const ValueError = union(enum) { AbortReason: JSC.CommonAbortReason, diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index cd8389eefd4d1..34538d12acd71 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -59,13 +59,14 @@ pub const TextEncoder = struct { const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written); bun.assert(result.written <= buf.len); bun.assert(result.read == slice.len); - const array_buffer = uint8array.asArrayBuffer(globalThis).?; + const array_buffer = uint8array.asArrayBuffer(globalThis) orelse return .zero; bun.assert(result.written == array_buffer.len); @memcpy(array_buffer.byteSlice()[0..result.written], buf[0..result.written]); return uint8array; } else { const bytes = strings.allocateLatin1IntoUTF8(globalThis.bunVM().allocator, []const u8, slice) catch { - return JSC.toInvalidArguments("Out of memory", .{}, globalThis); + globalThis.throwOutOfMemory(); + return .zero; }; bun.assert(bytes.len >= slice.len); return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null); @@ -112,7 +113,8 @@ pub const TextEncoder = struct { @TypeOf(slice), slice, ) catch { - return JSC.toInvalidArguments("Out of memory", .{}, globalThis); + globalThis.throwOutOfMemory(); + return .zero; }; return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null); } diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 61c9c735e2f44..907f94bdb5721 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -119,10 +119,8 @@ pub const Request = struct { pub const InternalJSEventCallback = struct { function: JSC.Strong = .{}, - pub const EventType = enum(u8) { - timeout = 0, - abort = 1, - }; + pub const EventType = JSC.API.NodeHTTPResponse.AbortEvent; + pub fn init(function: JSC.JSValue, globalThis: *JSC.JSGlobalObject) InternalJSEventCallback { return InternalJSEventCallback{ .function = JSC.Strong.create(function, globalThis), diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 184c1f9cbbc06..1bb8019782469 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -112,6 +112,58 @@ pub const Response = struct { return &this.body.value; } + pub export fn jsFunctionRequestOrResponseHasBodyValue(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = globalObject; // autofix + const arguments = callframe.arguments(1); + const this_value = arguments.ptr[0]; + if (this_value.isEmptyOrUndefinedOrNull()) { + return .false; + } + + if (this_value.as(Response)) |response| { + return JSC.JSValue.jsBoolean(!response.body.value.isDefinitelyEmpty()); + } else if (this_value.as(Request)) |request| { + return JSC.JSValue.jsBoolean(!request.body.value.isDefinitelyEmpty()); + } + + return .false; + } + + pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(1); + const this_value = arguments.ptr[0]; + if (this_value.isEmptyOrUndefinedOrNull()) { + return .undefined; + } + + const body: *Body.Value = brk: { + if (this_value.as(Response)) |response| { + break :brk &response.body.value; + } else if (this_value.as(Request)) |request| { + break :brk &request.body.value; + } + + return .undefined; + }; + + // Get the body if it's available synchronously. + switch (body.*) { + .Used, .Empty, .Null => return .undefined, + .Blob => |*blob| { + if (blob.isBunFile()) { + return .undefined; + } + defer body.* = .{ .Used = {} }; + return blob.toArrayBuffer(globalObject, .transfer); + }, + .WTFStringImpl, .InternalBlob => { + var any_blob = body.useAsAnyBlob(); + return any_blob.toArrayBufferTransfer(globalObject); + }, + .Error, .Locked => return .undefined, + } + } + pub fn getFetchHeaders( this: *Response, ) ?*FetchHeaders { diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 9c2f589f3771f..8a4c0494dea39 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -2131,7 +2131,7 @@ pub fn HTTPServerWritable(comptime ssl: bool) type { this.res.end(buf, false); this.has_backpressure = false; } else { - this.has_backpressure = !this.res.write(buf); + this.has_backpressure = this.res.write(buf) == .backpressure; } this.handleWrote(buf.len); return true; diff --git a/src/bun.zig b/src/bun.zig index 6d77b9d8b7210..3e0956315d5f7 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3132,8 +3132,8 @@ pub fn NewRefCounted(comptime T: type, comptime deinit_fn: ?fn (self: *T) void) const ptr = bun.new(T, t); if (Environment.enable_logs) { - if (ptr.ref_count != 1) { - Output.panic("Expected ref_count to be 1, got {d}", .{ptr.ref_count}); + if (ptr.ref_count == 0) { + Output.panic("Expected ref_count to be > 0, got {d}", .{ptr.ref_count}); } } diff --git a/src/codegen/client-js.ts b/src/codegen/client-js.ts index 6ae7a3863a960..3f6918b74de76 100644 --- a/src/codegen/client-js.ts +++ b/src/codegen/client-js.ts @@ -9,7 +9,7 @@ let $debug_log_enabled = ((env) => ( // The rationale for checking all these variables is just so you don't have to exactly remember which one you set. (env.BUN_DEBUG_ALL && env.BUN_DEBUG_ALL !== '0') || (env.BUN_DEBUG_JS && env.BUN_DEBUG_JS !== '0') - || (env.BUN_DEBUG_${pathToUpperSnakeCase(filepath)}) + || (env.BUN_DEBUG_${pathToUpperSnakeCase(publicName)}) || (env.DEBUG_${pathToUpperSnakeCase(filepath)}) ))(Bun.env); let $debug_pid_prefix = Bun.env.SHOW_PID === '1'; diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index bc9ff248f8c23..1fb7216f3dd2c 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -119,6 +119,18 @@ extern "C" } } + extern "C" void uws_res_clear_corked_socket(us_loop_t *loop) { + uWS::LoopData *loopData = uWS::Loop::data(loop); + void *corkedSocket = loopData->getCorkedSocket(); + if (corkedSocket) { + if (loopData->isCorkedSSL()) { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } else { + ((uWS::AsyncSocket *) corkedSocket)->uncork(); + } + } +} + void uws_app_delete(int ssl, uws_app_t *app, const char *pattern, uws_method_handler handler, void *user_data) { if (ssl) @@ -275,6 +287,19 @@ extern "C" } } + size_t uws_res_get_buffered_amount(int ssl, uws_res_t *res) nonnull_fn_decl; + + size_t uws_res_get_buffered_amount(int ssl, uws_res_t *res) + { + if (ssl) { + uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + return uwsRes->getBufferedAmount(); + } else { + uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; + return uwsRes->getBufferedAmount(); + } + } + void uws_app_any(int ssl, uws_app_t *app, const char *pattern_ptr, size_t pattern_len, uws_method_handler handler, void *user_data) { std::string pattern = std::string(pattern_ptr, pattern_len); @@ -1241,17 +1266,27 @@ extern "C" } } - bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t length) nonnull_fn_decl; + bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) nonnull_fn_decl; - bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t length) + bool uws_res_write(int ssl, uws_res_r res, const char *data, size_t *length) { if (ssl) { uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; - return uwsRes->write(std::string_view(data, length)); + if (*length < 16 * 1024 && *length > 0) { + if (uwsRes->canCork()) { + uwsRes->uWS::AsyncSocket::cork(); + } + } + return uwsRes->write(std::string_view(data, *length), length); } uWS::HttpResponse *uwsRes = (uWS::HttpResponse *)res; - return uwsRes->write(std::string_view(data, length)); + if (*length < 16 * 1024 && *length > 0) { + if (uwsRes->canCork()) { + uwsRes->uWS::AsyncSocket::cork(); + } + } + return uwsRes->write(std::string_view(data, *length), length); } uint64_t uws_res_get_write_offset(int ssl, uws_res_r res) nonnull_fn_decl; uint64_t uws_res_get_write_offset(int ssl, uws_res_r res) diff --git a/src/deps/uws.zig b/src/deps/uws.zig index defe1e3d1220a..46064329b78dd 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -17,6 +17,10 @@ const SSLWrapper = @import("../bun.js/api/bun/ssl_wrapper.zig").SSLWrapper; const TextEncoder = @import("../bun.js/webcore/encoding.zig").Encoder; const JSC = bun.JSC; const EventLoopTimer = @import("../bun.js//api//Timer.zig").EventLoopTimer; +const WriteResult = union(enum) { + want_more: usize, + backpressure: usize, +}; pub const CloseCode = enum(i32) { normal = 0, @@ -55,6 +59,7 @@ pub const InternalLoopData = extern struct { parent_ptr: ?*anyopaque, parent_tag: c_char, iteration_nr: usize, + jsc_vm: ?*JSC.VM, pub fn recvSlice(this: *InternalLoopData) []u8 { return this.recv_buf[0..LIBUS_RECV_BUFFER_LENGTH]; @@ -2372,6 +2377,10 @@ pub const PosixLoop = extern struct { const log = bun.Output.scoped(.Loop, false); + pub fn uncork(this: *PosixLoop) void { + uws_res_clear_corked_socket(this); + } + pub fn iterationNumber(this: *const PosixLoop) u64 { return this.internal_loop_data.iteration_nr; } @@ -3043,6 +3052,47 @@ pub const AnyResponse = union(enum) { SSL: *NewApp(true).Response, TCP: *NewApp(false).Response, + pub fn socket(this: AnyResponse) *uws_res { + return switch (this) { + .SSL => |resp| resp.downcast(), + .TCP => |resp| resp.downcast(), + }; + } + pub fn getRemoteSocketInfo(this: AnyResponse) ?SocketAddress { + return switch (this) { + .SSL => |resp| resp.getRemoteSocketInfo(), + .TCP => |resp| resp.getRemoteSocketInfo(), + }; + } + + pub fn getWriteOffset(this: AnyResponse) u64 { + return switch (this) { + .SSL => |resp| resp.getWriteOffset(), + .TCP => |resp| resp.getWriteOffset(), + }; + } + + pub fn getBufferedAmount(this: AnyResponse) u64 { + return switch (this) { + .SSL => |resp| resp.getBufferedAmount(), + .TCP => |resp| resp.getBufferedAmount(), + }; + } + + pub fn writeContinue(this: AnyResponse) void { + return switch (this) { + .SSL => |resp| resp.writeContinue(), + .TCP => |resp| resp.writeContinue(), + }; + } + + pub fn state(this: AnyResponse) State { + return switch (this) { + .SSL => |resp| resp.state(), + .TCP => |resp| resp.state(), + }; + } + pub fn timeout(this: AnyResponse, seconds: u8) void { switch (this) { .SSL => |resp| resp.timeout(seconds), @@ -3050,6 +3100,16 @@ pub const AnyResponse = union(enum) { } } + pub fn onData(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, []const u8, bool) void, opcional_data: UserDataType) void { + return switch (this) { + inline .SSL, .TCP => |resp, ssl| resp.onData(UserDataType, struct { + pub fn onDataCallback(user_data: UserDataType, _: *uws.NewApp(ssl == .SSL).Response, data: []const u8, last: bool) void { + @call(.always_inline, handler, .{ user_data, data, last }); + } + }.onDataCallback, opcional_data), + }; + } + pub fn writeStatus(this: AnyResponse, status: []const u8) void { return switch (this) { .SSL => |resp| resp.writeStatus(status), @@ -3064,7 +3124,7 @@ pub const AnyResponse = union(enum) { }; } - pub fn write(this: AnyResponse, data: []const u8) void { + pub fn write(this: AnyResponse, data: []const u8) WriteResult { return switch (this) { .SSL => |resp| resp.write(data), .TCP => |resp| resp.write(data), @@ -3136,6 +3196,22 @@ pub const AnyResponse = union(enum) { }; } + pub fn onTimeout(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, AnyResponse) void, opcional_data: UserDataType) void { + const wrapper = struct { + pub fn ssl_handler(user_data: UserDataType, resp: *NewApp(true).Response) void { + handler(user_data, .{ .SSL = resp }); + } + pub fn tcp_handler(user_data: UserDataType, resp: *NewApp(false).Response) void { + handler(user_data, .{ .TCP = resp }); + } + }; + + return switch (this) { + .SSL => |resp| resp.onTimeout(UserDataType, wrapper.ssl_handler, opcional_data), + .TCP => |resp| resp.onTimeout(UserDataType, wrapper.tcp_handler, opcional_data), + }; + } + pub fn onAborted(this: AnyResponse, comptime UserDataType: type, comptime handler: fn (UserDataType, AnyResponse) void, opcional_data: UserDataType) void { const wrapper = struct { pub fn ssl_handler(user_data: UserDataType, resp: *NewApp(true).Response) void { @@ -3533,8 +3609,15 @@ pub fn NewApp(comptime ssl: bool) type { pub fn resetTimeout(res: *Response) void { uws_res_reset_timeout(ssl_flag, res.downcast()); } - pub fn write(res: *Response, data: []const u8) bool { - return uws_res_write(ssl_flag, res.downcast(), data.ptr, data.len); + pub fn getBufferedAmount(res: *Response) u64 { + return uws_res_get_buffered_amount(ssl_flag, res.downcast()); + } + pub fn write(res: *Response, data: []const u8) WriteResult { + var len: usize = data.len; + return switch (uws_res_write(ssl_flag, res.downcast(), data.ptr, &len)) { + true => .{ .want_more = len }, + false => .{ .backpressure = len }, + }; } pub fn getWriteOffset(res: *Response) u64 { return uws_res_get_write_offset(ssl_flag, res.downcast()); @@ -3959,7 +4042,8 @@ extern fn uws_res_end_without_body(ssl: i32, res: *uws_res, close_connection: bo extern fn uws_res_end_sendfile(ssl: i32, res: *uws_res, write_offset: u64, close_connection: bool) void; extern fn uws_res_timeout(ssl: i32, res: *uws_res, timeout: u8) void; extern fn uws_res_reset_timeout(ssl: i32, res: *uws_res) void; -extern fn uws_res_write(ssl: i32, res: *uws_res, data: [*c]const u8, length: usize) bool; +extern fn uws_res_get_buffered_amount(ssl: i32, res: *uws_res) u64; +extern fn uws_res_write(ssl: i32, res: *uws_res, data: ?[*]const u8, length: *usize) bool; extern fn uws_res_get_write_offset(ssl: i32, res: *uws_res) u64; extern fn uws_res_override_write_offset(ssl: i32, res: *uws_res, u64) void; extern fn uws_res_has_responded(ssl: i32, res: *uws_res) bool; @@ -4056,6 +4140,7 @@ pub const State = enum(u8) { HTTP_END_CALLED = 4, HTTP_RESPONSE_PENDING = 8, HTTP_CONNECTION_CLOSE = 16, + HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, _, @@ -4063,6 +4148,10 @@ pub const State = enum(u8) { return @intFromEnum(this) & @intFromEnum(State.HTTP_RESPONSE_PENDING) != 0; } + pub inline fn hasWrittenContentLengthHeader(this: State) bool { + return @intFromEnum(this) & @intFromEnum(State.HTTP_WROTE_CONTENT_LENGTH_HEADER) != 0; + } + pub inline fn isHttpEndCalled(this: State) bool { return @intFromEnum(this) & @intFromEnum(State.HTTP_END_CALLED) != 0; } @@ -4103,6 +4192,10 @@ pub const WindowsLoop = extern struct { pre: *uv.uv_prepare_t, check: *uv.uv_check_t, + pub fn uncork(this: *PosixLoop) void { + uws_res_clear_corked_socket(this); + } + pub fn get() *WindowsLoop { return uws_get_loop_with_native(bun.windows.libuv.Loop.get()); } @@ -4418,3 +4511,4 @@ pub fn onThreadExit() void { } extern fn uws_app_clear_routes(ssl_flag: c_int, app: *uws_app_t) void; +extern fn uws_res_clear_corked_socket(loop: *Loop) void; diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index b62f3028b3332..8616d19629d3d 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -299,7 +299,7 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF setup = undefined; }; - function nextTick(cb, args) { + function nextTick(cb, ...args) { validateFunction(cb, "callback"); if (setup) { setup(); @@ -309,7 +309,9 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF queue.push({ callback: cb, - args: $argumentCount() > 1 ? Array.prototype.slice.$call(arguments, 1) : undefined, + // We want to avoid materializing the args if there are none because it's + // a waste of memory and Array.prototype.slice shows up in profiling. + args: $argumentCount() > 1 ? args : undefined, frame: $getInternalField($asyncContext, 0), }); $putInternalField(nextTickQueue, 0, 1); diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 7c3cc0a36bce6..8bc126bec401b 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1,7 +1,66 @@ -// Hardcoded module "node:http" -const EventEmitter = require("node:events"); +const enum ClientRequestEmitState { + socket = 1, + prefinish = 2, + finish = 4, + response = 8, +} + +const enum NodeHTTPResponseAbortEvent { + none = 0, + abort = 1, + timeout = 2, +} +const enum NodeHTTPIncomingRequestType { + FetchRequest, + FetchResponse, + NodeHTTPResponse, +} +const enum NodeHTTPHeaderState { + none, + assigned, + sent, +} + +const headerStateSymbol = Symbol("headerState"); +// used for pretending to emit events in the right order +const kEmitState = Symbol("emitState"); + +const abortedSymbol = Symbol("aborted"); +const bodyStreamSymbol = Symbol("bodyStream"); +const closedSymbol = Symbol("closed"); +const controllerSymbol = Symbol("controller"); +const deferredSymbol = Symbol("deferred"); +const eofInProgress = Symbol("eofInProgress"); +const fakeSocketSymbol = Symbol("fakeSocket"); +const finishedSymbol = "finished"; +const firstWriteSymbol = Symbol("firstWrite"); +const headersSymbol = Symbol("headers"); +const isTlsSymbol = Symbol("is_tls"); +const kClearTimeout = Symbol("kClearTimeout"); +const kDeprecatedReplySymbol = Symbol("deprecatedReply"); +const kfakeSocket = Symbol("kfakeSocket"); +const kHandle = Symbol("handle"); +const kRealListen = Symbol("kRealListen"); +const noBodySymbol = Symbol("noBody"); +const optionsSymbol = Symbol("options"); +const reqSymbol = Symbol("req"); +const timeoutTimerSymbol = Symbol("timeoutTimer"); +const tlsSymbol = Symbol("tls"); +const typeSymbol = Symbol("type"); +const webRequestOrResponse = Symbol("FetchAPI"); +const statusCodeSymbol = Symbol("statusCode"); +const kEndCalled = Symbol.for("kEndCalled"); +const kAbortController = Symbol.for("kAbortController"); +const statusMessageSymbol = Symbol("statusMessage"); +const kInternalSocketData = Symbol.for("::bunternal::"); +const serverSymbol = Symbol.for("::bunternal::"); + +const kRequest = Symbol("request"); + +const kEmptyObject = Object.freeze(Object.create(null)); +const EventEmitter: typeof import("node:events").EventEmitter = require("node:events"); const { isTypedArray } = require("node:util/types"); -const { Duplex, Readable, Writable } = require("node:stream"); +const { Duplex, Readable, Stream } = require("node:stream"); const { ERR_INVALID_ARG_TYPE, ERR_INVALID_PROTOCOL } = require("internal/errors"); const { isPrimary } = require("internal/cluster/isPrimary"); const { kAutoDestroyed } = require("internal/shared"); @@ -20,6 +79,9 @@ const { Headers, Blob, headersTuple, + webRequestOrResponseHasBodyValue, + getCompleteWebRequestOrResponseBodyValueAsArrayBuffer, + drainMicrotasks, } = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as { getHeader: (headers: Headers, name: string) => string | undefined; setHeader: (headers: Headers, name: string, value: string) => void; @@ -32,6 +94,8 @@ const { Headers: (typeof globalThis)["Headers"]; Blob: (typeof globalThis)["Blob"]; headersTuple: any; + webRequestOrResponseHasBodyValue: (arg: any) => boolean; + getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined; }; let cluster; @@ -66,11 +130,11 @@ const validateHeaderName = (name, label) => { const validateHeaderValue = (name, value) => { if (value === undefined) { // throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); - throw new Error("ERR_HTTP_INVALID_HEADER_VALUE"); + throw $ERR_HTTP_INVALID_HEADER_VALUE(`Invalid header value: ${value} for ${name}`); } if (checkInvalidHeaderChar(value)) { // throw new ERR_INVALID_CHAR("header content", name); - throw new Error("ERR_INVALID_CHAR"); + throw $ERR_INVALID_CHAR(`Invalid header value: ${value} for ${name}`); } }; @@ -87,12 +151,6 @@ const setTimeout = globalThis.setTimeout; const fetch = Bun.fetch; const nop = () => {}; -const kEmptyObject = Object.freeze(Object.create(null)); -const kEndCalled = Symbol.for("kEndCalled"); -const kAbortController = Symbol.for("kAbortController"); -const kClearTimeout = Symbol("kClearTimeout"); -const kRealListen = Symbol("kRealListen"); - // Primordials const StringPrototypeSlice = String.prototype.slice; const StringPrototypeStartsWith = String.prototype.startsWith; @@ -104,11 +162,6 @@ const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; const NODE_HTTP_WARNING = "WARN: Agent is mostly unused in Bun's implementation of http. If you see strange behavior, this is probably the cause."; -var kInternalRequest = Symbol("kInternalRequest"); -const kInternalSocketData = Symbol.for("::bunternal::"); -const serverSymbol = Symbol.for("::bunternal::"); -const kfakeSocket = Symbol("kfakeSocket"); - const kEmptyBuffer = Buffer.alloc(0); function isValidTLSArray(obj) { @@ -247,6 +300,166 @@ var FakeSocket = class Socket extends Duplex { _write(chunk, encoding, callback) {} }; +class ConnResetException extends Error { + constructor(msg) { + super(msg); + this.code = "ECONNRESET"; + this.name = "ConnResetException"; + } +} + +const NodeHTTPServerSocket = class Socket extends Duplex { + bytesRead = 0; + bytesWritten = 0; + connecting = false; + timeout = 0; + [kHandle]; + server: Server; + _httpMessage; + + constructor(server: Server, handle, encrypted) { + super(); + this.server = server; + this[kHandle] = handle; + handle.onclose = this.#onClose.bind(this); + handle.duplex = this; + this.encrypted = encrypted; + this.on("timeout", onNodeHTTPServerSocketTimeout); + } + + #onClose() { + const handle = this[kHandle]; + this[kHandle] = null; + const message = this._httpMessage; + const req = message?.req; + if (req && !req.complete) { + req.destroy(new ConnResetException("aborted")); + } + } + #onCloseForDestroy(closeCallback) { + this.#onClose(); + $isCallable(closeCallback) && closeCallback(); + } + + address() { + return this[kHandle]?.remoteAddress || null; + } + + get bufferSize() { + return this.writableLength; + } + + connect(port, host, connectListener) { + return this; + } + + _destroy(err, callback) { + const handle = this[kHandle]; + if (!handle) { + $isCallable(callback) && callback(err); + return; + } + if (handle.closed) { + const onclose = handle.onclose; + handle.onclose = null; + if ($isCallable(onclose)) { + onclose.$call(handle); + } + $isCallable(callback) && callback(err); + return; + } + this[kHandle] = undefined; + handle.onclose = this.#onCloseForDestroy.bind(this, callback); + handle.close(); + } + + _final(callback) { + const handle = this[kHandle]; + if (!handle) { + callback(); + return; + } + handle.onclose = this.#onCloseForDestroy.bind(this, callback); + handle.close(); + } + + get localAddress() { + return "127.0.0.1"; + } + + get localFamily() { + return "IPv4"; + } + + get localPort() { + return 80; + } + + get pending() { + return this.connecting; + } + + _read(size) {} + + get readyState() { + if (this.connecting) return "opening"; + if (this.readable) { + return this.writable ? "open" : "readOnly"; + } else { + return this.writable ? "writeOnly" : "closed"; + } + } + + ref() { + return this; + } + + get remoteAddress() { + return this.address()?.address; + } + + set remoteAddress(val) { + // initialize the object so that other properties wouldn't be lost + this.address().address = val; + } + + get remotePort() { + return this.address()?.port; + } + + set remotePort(val) { + // initialize the object so that other properties wouldn't be lost + this.address().port = val; + } + + get remoteFamily() { + return this.address()?.family; + } + + set remoteFamily(val) { + // initialize the object so that other properties wouldn't be lost + this.address().family = val; + } + + resetAndDestroy() {} + + setKeepAlive(enable = false, initialDelay = 0) {} + + setNoDelay(noDelay = true) { + return this; + } + + setTimeout(timeout, callback) { + return this; + } + + unref() { + return this; + } + + _write(chunk, encoding, callback) {} +} as unknown as typeof import("node:net").Socket; + function createServer(options, callback) { return new Server(options, callback); } @@ -342,11 +555,8 @@ function emitListeningNextTick(self, hostname, port) { } } -var tlsSymbol = Symbol("tls"); -var isTlsSymbol = Symbol("is_tls"); -var optionsSymbol = Symbol("options"); - -function Server(options, callback) { +type Server = InstanceType; +const Server = function Server(options, callback) { if (!(this instanceof Server)) return new Server(options, callback); EventEmitter.$call(this); @@ -423,25 +633,42 @@ function Server(options, callback) { if (callback) this.on("request", callback); return this; -} +} as unknown as typeof import("node:http").Server; function onRequestEvent(event) { const [server, http_res, req] = this.socket[kInternalSocketData]; + if (!http_res[finishedSymbol]) { switch (event) { - case 0: // timeout + case NodeHTTPResponseAbortEvent.timeout: this.emit("timeout"); server.emit("timeout", req.socket); break; - case 1: // abort - this.complete = true; - this.emit("close"); + case NodeHTTPResponseAbortEvent.abort: http_res[finishedSymbol] = true; + this.destroy(); break; } } } +function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPResponseAbortEvent) { + const server: Server = this?.server; + const socket: NodeHTTPServerSocket = this; + switch (event) { + case NodeHTTPResponseAbortEvent.abort: { + if (!socket.destroyed) { + socket.destroy(); + } + break; + } + case NodeHTTPResponseAbortEvent.timeout: { + socket.emit("timeout"); + break; + } + } +} + Server.prototype = { ref() { this._unref = false; @@ -633,6 +860,88 @@ Server.prototype = { }, }, maxRequestBodySize: Number.MAX_SAFE_INTEGER, + + onNodeHTTPRequest( + bunServer, + url: string, + method: string, + headersObject: Record, + headersArray: string[], + handle, + hasBody: boolean, + socketHandle, + isSocketNew, + socket, + ) { + const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; + isNextIncomingMessageHTTPS = isHTTPS; + if (!socket) { + socket = new NodeHTTPServerSocket(server, socketHandle, !!tls); + } + + const http_req = new RequestClass(kHandle, url, method, headersObject, headersArray, handle, hasBody, socket); + const http_res = new ResponseClass(http_req, { + [kHandle]: handle, + }); + isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS; + + let capturedError; + let rejectFunction; + let errorCallback = err => { + if (capturedError) return; + capturedError = err; + if (rejectFunction) rejectFunction(err); + handle && (handle.onabort = undefined); + handle = undefined; + }; + + let resolveFunction; + let didFinish = false; + + handle.onabort = onServerRequestEvent.bind(socket); + + if (isSocketNew) { + server.emit("connection", socket); + } + + socket[kRequest] = http_req; + + http_res.assignSocket(socket); + function onClose() { + didFinish = true; + resolveFunction && resolveFunction(); + } + http_res.once("close", onClose); + + server.emit("request", http_req, http_res); + + socket.cork(drainMicrotasks); + + if (capturedError) { + handle = undefined; + http_res.removeListener("close", onClose); + if (socket._httpMessage === http_res) { + socket._httpMessage = null; + } + throw capturedError; + } + + if (handle.finished || didFinish) { + handle = undefined; + http_res.removeListener("close", onClose); + if (socket._httpMessage === http_res) { + socket._httpMessage = null; + } + return; + } + + const { reject, resolve, promise } = $newPromiseCapability(Promise); + resolveFunction = resolve; + rejectFunction = reject; + + return promise; + }, + // Be very careful not to access (web) Request object // properties: // - request.url @@ -657,13 +966,15 @@ Server.prototype = { const prevIsNextIncomingMessageHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = isHTTPS; - const http_req = new RequestClass(req); + const http_req = new RequestClass(req, { + [typeSymbol]: NodeHTTPIncomingRequestType.FetchRequest, + }); assignEventCallback(req, onRequestEvent.bind(http_req)); isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS; const upgrade = http_req.headers.upgrade; - const http_res = new ResponseClass(http_req, reply); + const http_res = new ResponseClass(http_req, { [kDeprecatedReplySymbol]: reply }); http_req.socket[kInternalSocketData] = [server, http_res, req]; server.emit("connection", http_req.socket); @@ -786,85 +1097,172 @@ function requestHasNoBody(method, req) { // This lets us skip some URL parsing var isNextIncomingMessageHTTPS = false; -var typeSymbol = Symbol("type"); -var reqSymbol = Symbol("req"); -var bodyStreamSymbol = Symbol("bodyStream"); -var noBodySymbol = Symbol("noBody"); -var abortedSymbol = Symbol("aborted"); +function emitEOFIncomingMessageOuter(self) { + self.push(null); + self.complete = true; +} +function emitEOFIncomingMessage(self) { + self[eofInProgress] = true; + process.nextTick(emitEOFIncomingMessageOuter, self); +} + function IncomingMessage(req, defaultIncomingOpts) { - this.method = null; + this[abortedSymbol] = false; + this[eofInProgress] = false; this._consuming = false; this._dumped = false; - this[noBodySymbol] = false; - this[abortedSymbol] = false; this.complete = false; - Readable.$call(this); - var { type = "request", [kInternalRequest]: nodeReq } = defaultIncomingOpts || {}; - - this[reqSymbol] = req; - this[typeSymbol] = type; + this._closed = false; + + // (url, method, headers, rawHeaders, handle, hasBody) + if (req === kHandle) { + this[typeSymbol] = NodeHTTPIncomingRequestType.NodeHTTPResponse; + this.url = arguments[1]; + this.method = arguments[2]; + this.headers = arguments[3]; + this.rawHeaders = arguments[4]; + this[kHandle] = arguments[5]; + this[noBodySymbol] = !arguments[6]; + this[fakeSocketSymbol] = arguments[7]; + Readable.$call(this); + } else { + if (isNextIncomingMessageHTTPS) { + // Creating a new Duplex is expensive. + // We can skip it if the request is not HTTPS. + const socket = new FakeSocket(); + this[fakeSocketSymbol] = socket; + socket.encrypted = true; + isNextIncomingMessageHTTPS = false; + } - this[bodyStreamSymbol] = undefined; + this[noBodySymbol] = false; + Readable.$call(this); + var { [typeSymbol]: type, [reqSymbol]: nodeReq } = defaultIncomingOpts || {}; - this.req = nodeReq; + this[webRequestOrResponse] = req; + this[typeSymbol] = type; + this[bodyStreamSymbol] = undefined; + this[statusMessageSymbol] = (req as Response)?.statusText || null; + this[statusCodeSymbol] = (req as Response)?.status || 200; - if (!assignHeaders(this, req)) { - this[fakeSocketSymbol] = req; - const reqUrl = String(req?.url || ""); - this.url = reqUrl; - } + if (type === NodeHTTPIncomingRequestType.FetchRequest || type === NodeHTTPIncomingRequestType.FetchResponse) { + if (!assignHeaders(this, req)) { + this[fakeSocketSymbol] = req; + } + } else { + // Node defaults url and method to null. + this.url = ""; + this.method = null; + } - if (isNextIncomingMessageHTTPS) { - // Creating a new Duplex is expensive. - // We can skip it if the request is not HTTPS. - const socket = new FakeSocket(); - this[fakeSocketSymbol] = socket; - socket.encrypted = true; - isNextIncomingMessageHTTPS = false; + this[noBodySymbol] = + type === NodeHTTPIncomingRequestType.FetchRequest // TODO: Add logic for checking for body on response + ? requestHasNoBody(this.method, this) + : false; } - this[noBodySymbol] = - type === "request" // TODO: Add logic for checking for body on response - ? requestHasNoBody(this.method, this) - : false; + this._readableState.readingMore = true; } -IncomingMessage.prototype = { +const IncomingMessagePrototype = { constructor: IncomingMessage, + __proto__: Readable.prototype, _construct(callback) { // TODO: streaming - if (this[typeSymbol] === "response" || this[noBodySymbol]) { - callback(); - return; - } + const type = this[typeSymbol]; - const contentLength = this.headers["content-length"]; - const length = contentLength ? parseInt(contentLength, 10) : 0; - if (length === 0) { - this[noBodySymbol] = true; - callback(); - return; + if (type === NodeHTTPIncomingRequestType.FetchResponse) { + if (!webRequestOrResponseHasBodyValue(this[webRequestOrResponse])) { + this.complete = true; + this.push(null); + } } callback(); }, + // Call this instead of resume() if we want to just + // dump all the data to /dev/null + _dump() { + if (!this._dumped) { + this._dumped = true; + // If there is buffered data, it may trigger 'data' events. + // Remove 'data' event listeners explicitly. + this.removeAllListeners("data"); + const handle = this[kHandle]; + if (handle) { + handle.ondata = undefined; + } + this.resume(); + } + }, _read(size) { + if (!this._consuming) { + this._readableState.readingMore = false; + this._consuming = true; + } + + if (this[eofInProgress]) { + // There is a nextTick pending that will emit EOF + return; + } + + let internalRequest; if (this[noBodySymbol]) { - this.complete = true; - this.push(null); + emitEOFIncomingMessage(this); + return; + } else if ((internalRequest = this[kHandle])) { + internalRequest.ondata = (chunk, isLast, aborted: NodeHTTPResponseAbortEvent) => { + if (aborted === NodeHTTPResponseAbortEvent.abort) { + this.destroy(); + return; + } + + if (chunk && !this._dumped) this.push(chunk); + + if (isLast) { + emitEOFIncomingMessage(this); + } + }; + + if (!internalRequest.hasBody) { + emitEOFIncomingMessage(this); + } + + return true; } else if (this[bodyStreamSymbol] == null) { - const reader = this[reqSymbol].body?.getReader() as ReadableStreamDefaultReader; + // If it's all available right now, we skip going through ReadableStream. + let completeBody = getCompleteWebRequestOrResponseBodyValueAsArrayBuffer(this[webRequestOrResponse]); + if (completeBody) { + $assert(completeBody instanceof ArrayBuffer, "completeBody is not an ArrayBuffer"); + $assert(completeBody.byteLength > 0, "completeBody should not be empty"); + + // They're ignoring the data. Let's not do anything with it. + if (!this._dumped) { + this.push(new Buffer(completeBody)); + } + emitEOFIncomingMessage(this); + return; + } + + const reader = this[webRequestOrResponse].body?.getReader?.() as ReadableStreamDefaultReader; if (!reader) { - this.complete = true; - this.push(null); + emitEOFIncomingMessage(this); return; } + this[bodyStreamSymbol] = reader; consumeStream(this, reader); } + + return; }, - _destroy(err, cb) { - if (!this.readableEnded || !this.complete) { + _finish() { + this.emit("prefinish"); + }, + _destroy: function IncomingMessage_destroy(err, cb) { + const shouldEmitAborted = !this.readableEnded || !this.complete; + + if (shouldEmitAborted) { this[abortedSymbol] = true; // IncomingMessage emits 'aborted'. // Client emits 'abort'. @@ -876,21 +1274,34 @@ IncomingMessage.prototype = { err = undefined; } - const stream = this[bodyStreamSymbol]; - this[bodyStreamSymbol] = undefined; - const streamState = stream?.$state; + var nodeHTTPResponse = this[kHandle]; + if (nodeHTTPResponse) { + this[kHandle] = undefined; + nodeHTTPResponse.onabort = nodeHTTPResponse.ondata = undefined; + if (!nodeHTTPResponse.finished) { + nodeHTTPResponse.abort(); + } + const socket = this.socket; + if (socket && !socket.destroyed && shouldEmitAborted) { + socket.destroy(err); + } + } else { + const stream = this[bodyStreamSymbol]; + this[bodyStreamSymbol] = undefined; + const streamState = stream?.$state; - if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) { - stream?.cancel?.().catch(nop); - } + if (streamState === $streamReadable || streamState === $streamWaiting || streamState === $streamWritable) { + stream?.cancel?.().catch(nop); + } - const socket = this[fakeSocketSymbol]; - if (socket) { - socket.destroy(err); + const socket = this[fakeSocketSymbol]; + if (socket && !socket.destroyed && shouldEmitAborted) { + socket.destroy(err); + } } - if (cb) { - emitErrorNextTick(this, err, cb); + if ($isCallable(cb)) { + emitErrorNextTickIfErrorListenerNT(this, err, cb); } }, get aborted() { @@ -903,17 +1314,17 @@ IncomingMessage.prototype = { return (this[fakeSocketSymbol] ??= new FakeSocket()); }, get statusCode() { - return this[reqSymbol].status; + return this[statusCodeSymbol]; }, set statusCode(value) { if (!(value in STATUS_CODES)) return; - this[reqSymbol].status = value; + this[statusCodeSymbol] = value; }, get statusMessage() { - return STATUS_CODES[this[reqSymbol].status]; + return this[statusMessageSymbol]; }, set statusMessage(value) { - // noop + this[statusMessageSymbol] = value; }, get httpVersion() { return "1.1"; @@ -946,7 +1357,9 @@ IncomingMessage.prototype = { // noop }, setTimeout(msecs, callback) { - const req = this[reqSymbol]; + this.take; + const req = this[kHandle] || this[webRequestOrResponse]; + if (req) { setRequestTimeout(req, Math.ceil(msecs / 1000)); typeof callback === "function" && this.once("timeout", callback); @@ -959,8 +1372,8 @@ IncomingMessage.prototype = { set socket(value) { this[fakeSocketSymbol] = value; }, -}; -$setPrototypeDirect.$call(IncomingMessage.prototype, Readable.prototype); +} satisfies typeof import("node:http").IncomingMessage.prototype; +IncomingMessage.prototype = IncomingMessagePrototype; $setPrototypeDirect.$call(IncomingMessage, Readable); async function consumeStream(self, reader: ReadableStreamDefaultReader) { @@ -979,8 +1392,10 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { if (self.destroyed || (aborted = self[abortedSymbol])) { break; } - for (var v of value) { - self.push(v); + if (!self._dumped) { + for (var v of value) { + self.push(v); + } } if (self.destroyed || (aborted = self[abortedSymbol]) || done) { @@ -995,173 +1410,231 @@ async function consumeStream(self, reader: ReadableStreamDefaultReader) { } if (!self.complete) { - self.complete = true; - self.push(null); + emitEOFIncomingMessage(self); } } -const headersSymbol = Symbol("headers"); -const finishedSymbol = Symbol("finished"); -const timeoutTimerSymbol = Symbol("timeoutTimer"); -const fakeSocketSymbol = Symbol("fakeSocket"); function OutgoingMessage(options) { - Writable.$call(this, options); - this.headersSent = false; + if (!new.target) { + return new OutgoingMessage(options); + } + + Stream.$call(this, options); + this.sendDate = true; this[finishedSymbol] = false; - this[kEndCalled] = false; + this[headerStateSymbol] = NodeHTTPHeaderState.none; this[kAbortController] = null; -} - -$setPrototypeDirect.$call((OutgoingMessage.prototype = {}), Writable.prototype); -OutgoingMessage.prototype.constructor = OutgoingMessage; // Re-add constructor which got lost when setting prototype -$setPrototypeDirect.$call(OutgoingMessage, Writable); -// Express "compress" package uses this -OutgoingMessage.prototype._implicitHeader = function () {}; + this.writable = true; + this.destroyed = false; + this._hasBody = true; + this._trailer = ""; + this._contentLength = null; + this._closed = false; + this._header = null; + this._headerSent = false; +} +const OutgoingMessagePrototype = { + constructor: OutgoingMessage, + __proto__: Stream.prototype, + + // These are fields which we do not use in our implementation, but are observable in Node.js. + _keepAliveTimeout: 0, + _defaultKeepAlive: true, + shouldKeepAlive: true, + _onPendingData: function nop() {}, + outputSize: 0, + outputData: [], + strictContentLength: false, + _removedTE: false, + _removedContLen: false, + _removedConnection: false, + usesChunkedEncodingByDefault: true, + + appendHeader(name, value) { + var headers = (this[headersSymbol] ??= new Headers()); + headers.append(name, value); + return this; + }, -OutgoingMessage.prototype.appendHeader = function (name, value) { - var headers = (this[headersSymbol] ??= new Headers()); - headers.append(name, value); -}; + _implicitHeader() { + throw $ERR_METHOD_NOT_IMPLEMENTED("The method _implicitHeader() is not implemented"); + }, + flushHeaders() {}, + getHeader(name) { + return getHeader(this[headersSymbol], name); + }, -OutgoingMessage.prototype.flushHeaders = function () {}; + getHeaderNames() { + var headers = this[headersSymbol]; + if (!headers) return []; + return Array.from(headers.keys()); + }, -OutgoingMessage.prototype.getHeader = function (name) { - return getHeader(this[headersSymbol], name); -}; + getHeaders() { + const headers = this[headersSymbol]; + if (!headers) return kEmptyObject; + return headers.toJSON(); + }, -OutgoingMessage.prototype.getHeaders = function () { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); -}; + removeHeader(name) { + if (this[headerStateSymbol] === NodeHTTPHeaderState.sent) { + throw $ERR_HTTP_HEADERS_SENT("Cannot remove header after headers have been sent."); + } + const headers = this[headersSymbol]; + if (!headers) return; + headers.delete(name); + }, -OutgoingMessage.prototype.getHeaderNames = function () { - var headers = this[headersSymbol]; - if (!headers) return []; - return Array.from(headers.keys()); -}; + setHeader(name, value) { + const headers = (this[headersSymbol] ??= new Headers()); + setHeader(headers, name, value); + return this; + }, -OutgoingMessage.prototype.removeHeader = function (name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); -}; + hasHeader(name) { + const headers = this[headersSymbol]; + if (!headers) return false; + return headers.has(name); + }, -OutgoingMessage.prototype.setHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - var headers = this[headersSymbol]; - headers.set(name, value); - return this; -}; + get headers() { + const headers = this[headersSymbol]; + if (!headers) return kEmptyObject; + return headers.toJSON(); + }, + set headers(value) { + this[headersSymbol] = new Headers(value); + }, -OutgoingMessage.prototype.hasHeader = function (name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); -}; + addTrailers(headers) { + throw new Error("not implemented"); + }, -OutgoingMessage.prototype.addTrailers = function (headers) { - throw new Error("not implemented"); -}; + setTimeout(msecs, callback) { + if (this.destroyed) return this; -function onTimeout() { - this[timeoutTimerSymbol] = undefined; - this[kAbortController]?.abort(); - this.emit("timeout"); -} + this.timeout = msecs = validateMsecs(msecs, "msecs"); -OutgoingMessage.prototype.setTimeout = function (msecs, callback) { - if (this.destroyed) return this; + // Attempt to clear an existing timer in both cases - + // even if it will be rescheduled we don't want to leak an existing timer. + clearTimeout(this[timeoutTimerSymbol]); - this.timeout = msecs = validateMsecs(msecs, "msecs"); + if (msecs === 0) { + if (callback != null) { + if (!$isCallable(callback)) validateFunction(callback, "callback"); + this.removeListener("timeout", callback); + } - // Attempt to clear an existing timer in both cases - - // even if it will be rescheduled we don't want to leak an existing timer. - clearTimeout(this[timeoutTimerSymbol]); + this[timeoutTimerSymbol] = undefined; + } else { + this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref(); - if (msecs === 0) { - if (callback !== undefined) { - validateFunction(callback, "callback"); - this.removeListener("timeout", callback); + if (callback != null) { + if (!$isCallable(callback)) validateFunction(callback, "callback"); + this.once("timeout", callback); + } } - this[timeoutTimerSymbol] = undefined; - } else { - this[timeoutTimerSymbol] = setTimeout(onTimeout.bind(this), msecs).unref(); + return this; + }, - if (callback !== undefined) { - validateFunction(callback, "callback"); - this.once("timeout", callback); - } - } + get connection() { + return this.socket; + }, - return this; -}; + get socket() { + this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket(); + return this[fakeSocketSymbol]; + }, -Object.defineProperty(OutgoingMessage.prototype, "headers", { - // For compat with IncomingRequest - get: function () { - if (!this[headersSymbol]) return kEmptyObject; - return this[headersSymbol].toJSON(); + set socket(value) { + this[fakeSocketSymbol] = value; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "chunkedEncoding", { - get: function () { + get chunkedEncoding() { return false; }, - set: function (value) { - // throw new Error('not implemented'); + set chunkedEncoding(value) { + // noop }, -}); -Object.defineProperty(OutgoingMessage.prototype, "shouldKeepAlive", { - get: function () { - return true; + get writableObjectMode() { + return false; }, - set: function (value) { - // throw new Error('not implemented'); + get writableLength() { + return 0; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "useChunkedEncodingByDefault", { - get: function () { - return true; + get writableHighWaterMark() { + return 16 * 1024; }, - set: function (value) { - // throw new Error('not implemented'); + get writableNeedDrain() { + return !this.destroyed && !this[finishedSymbol]; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "socket", { - get: function () { - this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket(); - return this[fakeSocketSymbol]; + get writableEnded() { + return this[finishedSymbol]; }, - set: function (val) { - this[fakeSocketSymbol] = val; + get writableFinished() { + return this[finishedSymbol]; }, -}); -Object.defineProperty(OutgoingMessage.prototype, "connection", { - get: function () { - return this.socket; + _send(data, encoding, callback, byteLength) { + if (this.destroyed) { + return false; + } + return this.write(data, encoding, callback); }, -}); - -Object.defineProperty(OutgoingMessage.prototype, "finished", { - get: function () { - return this[finishedSymbol]; + end(chunk, encoding, callback) { + return this; }, -}); + destroy(err?: Error) { + if (this.destroyed) return this; + const handle = this[kHandle]; + this.destroyed = true; + if (handle) { + handle.abort(); + } + return this; + }, +} satisfies typeof import("node:http").OutgoingMessage.prototype; +OutgoingMessage.prototype = OutgoingMessagePrototype; +$setPrototypeDirect.$call(OutgoingMessage, Stream); + +function onNodeHTTPServerSocketTimeout() { + const req = this[kRequest]; + const reqTimeout = req && !req.complete && req.emit("timeout", this); + const res = this._httpMessage; + const resTimeout = res && res.emit("timeout", this); + const serverTimeout = this.server.emit("timeout", this); + + if (!reqTimeout && !resTimeout && !serverTimeout) this.destroy(); +} + +function onTimeout() { + this[timeoutTimerSymbol] = undefined; + this[kAbortController]?.abort(); + const handle = this[kHandle]; + + this.emit("timeout"); + if (handle) { + handle.abort(); + } +} function emitContinueAndSocketNT(self) { if (self.destroyed) return; // Ref: https://github.com/nodejs/node/blob/f63e8b7fa7a4b5e041ddec67307609ec8837154f/lib/_http_client.js#L803-L839 - self.emit("socket", self.socket); + if (!(self[kEmitState] & ClientRequestEmitState.socket)) { + self[kEmitState] |= ClientRequestEmitState.socket; + self.emit("socket", self.socket); + } //Emit continue event for the client (internally we auto handle it) if (!self._closed && self.getHeader("expect") === "100-continue") { @@ -1169,10 +1642,21 @@ function emitContinueAndSocketNT(self) { } } function emitCloseNT(self) { + if (!self._closed) { + self.destroyed = true; + self._closed = true; + + self.emit("close"); + } +} + +function emitCloseNTAndComplete(self) { if (!self._closed) { self._closed = true; self.emit("close"); } + + self.complete = true; } function emitRequestCloseNT(self) { @@ -1198,74 +1682,350 @@ function onServerResponseClose() { // Ergo, we need to deal with stale 'close' events and handle the case // where the ServerResponse object has already been deconstructed. // Fortunately, that requires only a single if check. :-) - if (this._httpMessage) { - emitCloseNT(this._httpMessage); + const httpMessage = this._httpMessage; + if (httpMessage) { + emitCloseNT(httpMessage); } } let OriginalWriteHeadFn, OriginalImplicitHeadFn; -const controllerSymbol = Symbol("controller"); -const firstWriteSymbol = Symbol("firstWrite"); -const deferredSymbol = Symbol("deferred"); -function ServerResponse(req, reply) { - OutgoingMessage.$call(this, reply); + +function ServerResponse(req, options) { + if (!new.target) { + return new ServerResponse(req, options); + } + + if ((this[kDeprecatedReplySymbol] = options?.[kDeprecatedReplySymbol])) { + this[controllerSymbol] = undefined; + this[firstWriteSymbol] = undefined; + this[deferredSymbol] = undefined; + this.write = ServerResponse_writeDeprecated; + this.end = ServerResponse_finalDeprecated; + } + + OutgoingMessage.$call(this, options); + this.req = req; - this._reply = reply; this.sendDate = true; - this.statusCode = 200; - this.headersSent = false; - this.statusMessage = undefined; - this[controllerSymbol] = undefined; - this[firstWriteSymbol] = undefined; - this._writableState.decodeStrings = false; - this[deferredSymbol] = undefined; - this._sent100 = false; - this._defaultKeepAlive = false; - this._removedConnection = false; - this._removedContLen = false; - this._hasBody = true; - this[finishedSymbol] = false; + this[headerStateSymbol] = NodeHTTPHeaderState.none; // this is matching node's behaviour // https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/lib/_http_server.js#L192 if (req.method === "HEAD") this._hasBody = false; + + const handle = options?.[kHandle]; + + if (handle) { + this[kHandle] = handle; + } } -$setPrototypeDirect.$call((ServerResponse.prototype = {}), OutgoingMessage.prototype); -ServerResponse.prototype.constructor = ServerResponse; // Re-add constructor which got lost when setting prototype -$setPrototypeDirect.$call(ServerResponse, OutgoingMessage); - -// Express "compress" package uses this -ServerResponse.prototype._implicitHeader = function () { - // @ts-ignore - this.writeHead(this.statusCode); -}; -ServerResponse.prototype._write = function (chunk, encoding, callback) { - if (this[firstWriteSymbol] === undefined && !this.headersSent) { - this[firstWriteSymbol] = chunk; - callback(); - return; +function callWriteHeadIfObservable(self, headerState) { + if ( + headerState === NodeHTTPHeaderState.none && + !(self.writeHead === OriginalWriteHeadFn && self._implicitHeader === OriginalImplicitHeadFn) + ) { + self.writeHead(self.statusCode, self.statusMessage, self[headersSymbol]); } +} - ensureReadableStreamController.$call(this, controller => { - controller.write(chunk); - callback(); - }); -}; +const ServerResponsePrototype = { + constructor: ServerResponse, + __proto__: OutgoingMessage.prototype, -ServerResponse.prototype._writev = function (chunks, callback) { - if (chunks.length === 1 && !this.headersSent && this[firstWriteSymbol] === undefined) { - this[firstWriteSymbol] = chunks[0].chunk; + // Unused but observable fields: + _removedConnection: false, + _removedContLen: false, + + get headersSent() { + return this[headerStateSymbol] === NodeHTTPHeaderState.sent; + }, + set headersSent(value) { + this[headerStateSymbol] = value ? NodeHTTPHeaderState.sent : NodeHTTPHeaderState.none; + }, + + // This end method is actually on the OutgoingMessage prototype in Node.js + // But we don't want it for the fetch() response version. + end(chunk, encoding, callback) { + if (this.destroyed) { + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_DESTROYED("Stream is destroyed"), callback); + return this; + } + + const handle = this[kHandle]; + const isFinished = this.finished || handle?.finished; + + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; + } + + if (isFinished && chunk) { + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + return this; + } + + if (handle) { + const headerState = this[headerStateSymbol]; + callWriteHeadIfObservable(this, headerState); + + if (headerState !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + + // If handle.writeHead throws, we don't want headersSent to be set to true. + // So we set it here. + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + + // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/_http_outgoing.js#L987 + this._contentLength = handle.end(chunk, encoding); + }); + } else { + // If there's no data but you already called end, then you're done. + // We can ignore it in that case. + if (!(!chunk && handle.ended)) { + handle.end(chunk, encoding); + } + } + this._header = " "; + const req = this.req; + const socket = req.socket; + if (!req._consuming && !req?._readableState?.resumeScheduled) { + req._dump(); + } + this.detachSocket(socket); + this[finishedSymbol] = this.finished = true; + + this.emit("prefinish"); + + if (callback) { + process.nextTick( + function (callback, self) { + // In Node.js, the "finish" event triggers the "close" event. + // So it shouldn't become closed === true until after "finish" is emitted and the callback is called. + self.emit("finish"); + + try { + callback(); + } catch (err) { + self.emit("error", err); + } + + process.nextTick(emitCloseNT, self); + }, + callback, + this, + ); + } else { + this.emit("finish"); + + process.nextTick(emitCloseNT, this); + } + } + + return this; + }, + + write(chunk, encoding, callback) { + const handle = this[kHandle]; + + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; + } + + if (this.destroyed || !handle) { + if ($isCallable(callback)) { + callback($ERR_STREAM_DESTROYED("Stream is destroyed")); + } + return false; + } + + if (this.finished) { + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Stream is already finished"), callback); + return false; + } + + let result = 0; + + const headerState = this[headerStateSymbol]; + callWriteHeadIfObservable(this, headerState); + + if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + + // If handle.writeHead throws, we don't want headersSent to be set to true. + // So we set it here. + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + + result = handle.write(chunk, encoding); + }); + } else { + result = handle.write(chunk, encoding); + } + + if (result < 0) { + handle.onwritable = callback + ? ServerResponsePrototypeOnWritable.bind(this, callback) + : ServerResponsePrototypeOnWritable.bind(this); + return false; + } + + if (result > 0) { + if (callback) { + process.nextTick(callback); + } + this.emit("drain"); + } + + return true; + }, + + _finish() { + OutgoingMessage.prototype._finish.$call(this); + }, + + detachSocket(socket) { + if (socket._httpMessage === this) { + socket.removeListener("close", onServerResponseClose); + socket._httpMessage = null; + } + + this.socket = null; + }, + + _implicitHeader() { + // @ts-ignore + this.writeHead(this.statusCode); + }, + + get writableNeedDrain() { + return !this.destroyed && !this[finishedSymbol] && (this[kHandle]?.bufferedAmount ?? 1) !== 0; + }, + + get writableFinished() { + const isWritableFinished = this[finishedSymbol] && (!this[kHandle] || this[kHandle].finished); + return isWritableFinished; + }, + + get writableLength() { + return 16 * 1024; + }, + + get writableHighWaterMark() { + return 64 * 1024; + }, + + get closed() { + return this[closedSymbol]; + }, + + _send(data, encoding, callback, byteLength) { + const handle = this[kHandle]; + if (!handle) { + return OutgoingMessagePrototype._send.$apply(this, arguments); + } + + if (this[headerStateSymbol] !== NodeHTTPHeaderState.sent) { + handle.cork(() => { + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + handle.write(data, encoding, callback); + }); + } else { + handle.write(data, encoding, callback); + } + }, + + writeHead(statusCode, statusMessage, headers) { + if (this[headerStateSymbol] === NodeHTTPHeaderState.none) { + _writeHead(statusCode, statusMessage, headers, this); + this[headerStateSymbol] = NodeHTTPHeaderState.assigned; + } + + return this; + }, + + assignSocket(socket) { + if (socket._httpMessage) { + throw ERR_HTTP_SOCKET_ASSIGNED(); + } + socket._httpMessage = this; + socket.once("close", onServerResponseClose); + this.socket = socket; + this.emit("socket", socket); + }, + + statusMessage: undefined, + statusCode: 200, + + get shouldKeepAlive() { + return this[kHandle]?.shouldKeepAlive ?? true; + }, + set shouldKeepAlive(value) { + // throw new Error('not implemented'); + }, + + get chunkedEncoding() { + return false; + }, + set chunkedEncoding(value) { + // throw new Error('not implemented'); + }, + + get useChunkedEncodingByDefault() { + return true; + }, + set useChunkedEncodingByDefault(value) { + // throw new Error('not implemented'); + }, + + destroy(err?: Error) { + if (this.destroyed) return this; + const handle = this[kHandle]; + this.destroyed = true; + if (handle) { + handle.abort(); + } + return this; + }, + + flushHeaders() { + this._implicitHeader(); + + const handle = this[kHandle]; + if (handle && !this.headersSent) { + this[headerStateSymbol] = NodeHTTPHeaderState.sent; + handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]); + } + }, +} satisfies typeof import("node:http").ServerResponse.prototype; +ServerResponse.prototype = ServerResponsePrototype; +$setPrototypeDirect.$call(ServerResponse, Stream); + +const ServerResponse_writeDeprecated = function _write(chunk, encoding, callback) { + if (this[firstWriteSymbol] === undefined && !this.headersSent) { + this[firstWriteSymbol] = chunk; callback(); return; } ensureReadableStreamController.$call(this, controller => { - for (const chunk of chunks) { - controller.write(chunk.chunk); - } - + controller.write(chunk); callback(); }); }; @@ -1276,7 +2036,7 @@ function ensureReadableStreamController(run) { this.headersSent = true; let firstWrite = this[firstWriteSymbol]; this[controllerSymbol] = undefined; - this._reply( + this[kDeprecatedReplySymbol]( new Response( new ReadableStream({ type: "direct", @@ -1309,17 +2069,51 @@ function drainHeadersIfObservable() { this._implicitHeader(); } -ServerResponse.prototype._final = function (callback) { +function ServerResponsePrototypeOnWritable(this: import("node:http").ServerResponse, optionalCallback) { + if (optionalCallback) { + optionalCallback(); + } + + if (!this.finished && !this.destroyed) { + this.emit("drain"); + } +} + +function ServerResponse_finalDeprecated(chunk, encoding, callback) { + if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } + if (!$isCallable(callback)) { + callback = undefined; + } + if (this.destroyed || this.finished) { + if (callback) callback(); + return; + } + const req = this.req; const shouldEmitClose = req && req.emit && !this[finishedSymbol]; if (!this.headersSent) { - var data = this[firstWriteSymbol] || ""; + var data = this[firstWriteSymbol]; + if (chunk) { + if (data) { + if (encoding) { + data = Buffer.from(data, encoding); + } + + data = new Blob([data, chunk]); + } + } else if (!data) { + data = undefined; + } + this[firstWriteSymbol] = undefined; this[finishedSymbol] = true; this.headersSent = true; // https://github.com/oven-sh/bun/issues/3458 drainHeadersIfObservable.$call(this); - this._reply( + this[kDeprecatedReplySymbol]( new Response(data, { headers: this[headersSymbol], status: this.statusCode, @@ -1336,130 +2130,38 @@ ServerResponse.prototype._final = function (callback) { this[finishedSymbol] = true; ensureReadableStreamController.$call(this, controller => { - controller.end(); - if (shouldEmitClose) { - req.complete = true; - process.nextTick(emitRequestCloseNT, req); + if (chunk && encoding) { + chunk = Buffer.from(chunk, encoding); } - callback(); - const deferred = this[deferredSymbol]; - if (deferred) { - this[deferredSymbol] = undefined; - deferred(); - } - }); -}; - -ServerResponse.prototype.writeProcessing = function () { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.addTrailers = function (headers) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.assignSocket = function (socket) { - if (socket._httpMessage) { - throw ERR_HTTP_SOCKET_ASSIGNED(); - } - socket._httpMessage = this; - socket.on("close", () => onServerResponseClose.$call(socket)); - this.socket = socket; - this._writableState.autoDestroy = false; - this.emit("socket", socket); -}; - -ServerResponse.prototype.detachSocket = function (socket) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.writeContinue = function (callback) { - throw new Error("not implemented"); -}; - -ServerResponse.prototype.setTimeout = function (msecs, callback) { - // TODO: - return this; -}; - -ServerResponse.prototype.appendHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - const headers = this[headersSymbol]; - headers.append(name, value); -}; - -ServerResponse.prototype.flushHeaders = function () {}; - -ServerResponse.prototype.getHeader = function (name) { - return getHeader(this[headersSymbol], name); -}; - -ServerResponse.prototype.getHeaders = function () { - const headers = this[headersSymbol]; - if (!headers) return kEmptyObject; - return headers.toJSON(); -}; - -ServerResponse.prototype.getHeaderNames = function () { - const headers = this[headersSymbol]; - if (!headers) return []; - return Array.from(headers.keys()); -}; - -ServerResponse.prototype.removeHeader = function (name) { - if (!this[headersSymbol]) return; - this[headersSymbol].delete(name); -}; - -ServerResponse.prototype.setHeader = function (name, value) { - this[headersSymbol] = this[headersSymbol] ?? new Headers(); - const headers = this[headersSymbol]; - setHeader(headers, name, value); - return this; -}; - -ServerResponse.prototype.hasHeader = function (name) { - if (!this[headersSymbol]) return false; - return this[headersSymbol].has(name); -}; - -ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) { - _writeHead(statusCode, statusMessage, headers, this); - return this; -}; + let prom; + if (chunk) { + prom = controller.end(chunk); + } else { + prom = controller.end(); + } -Object.defineProperty(ServerResponse.prototype, "shouldKeepAlive", { - get() { - return true; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); + const handler = () => { + callback(); + const deferred = this[deferredSymbol]; + if (deferred) { + this[deferredSymbol] = undefined; + deferred(); + } + }; + if ($isPromise(prom)) prom.then(handler, handler); + else handler(); + }); +} -Object.defineProperty(ServerResponse.prototype, "chunkedEncoding", { - get() { - return false; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); +// ServerResponse.prototype._final = ServerResponse_finalDeprecated; -Object.defineProperty(ServerResponse.prototype, "useChunkedEncodingByDefault", { - get() { - return true; - }, - set(value) { - // throw new Error('not implemented'); - }, -}); +ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; OriginalWriteHeadFn = ServerResponse.prototype.writeHead; OriginalImplicitHeadFn = ServerResponse.prototype._implicitHeader; -class ClientRequest extends OutgoingMessage { +class ClientRequest extends (OutgoingMessage as unknown as typeof import("node:http").OutgoingMessage) { #timeout; #res: IncomingMessage | null = null; #upgradeOrConnect = false; @@ -1484,7 +2186,7 @@ class ClientRequest extends OutgoingMessage { [kAbortController]: AbortController | null = null; #timeoutTimer?: Timer = undefined; #options; - #finished; + [finishedSymbol] = false; _httpMessage; @@ -1512,32 +2214,99 @@ class ClientRequest extends OutgoingMessage { return this.#agent; } - _write(chunk, encoding, callback) { + write(chunk, encoding, callback) { + if (this.destroyed) return false; + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; + } + + return this.#write(chunk, encoding, callback); + } + + #write(chunk, encoding, callback) { + const MAX_FAKE_BACKPRESSURE_SIZE = 1024 * 1024; + const canSkipReEncodingData = + // UTF-8 string: + (typeof chunk === "string" && (encoding === "utf-8" || encoding === "utf8" || !encoding)) || + // Buffer + ($isTypedArrayView(chunk) && (!encoding || encoding === "buffer" || encoding === "utf-8")); + let bodySize = 0; + if (!canSkipReEncodingData) { + chunk = Buffer.from(chunk, encoding); + bodySize = chunk.length; + } else { + bodySize = chunk.length; + } + if (!this.#bodyChunks) { this.#bodyChunks = [chunk]; - callback(); - return; + + if (callback) callback(); + return true; + } + + // Signal fake backpressure if the body size is > 1024 * 1024 + // So that code which loops forever until backpressure is signaled + // will eventually exit. + + for (let chunk of this.#bodyChunks) { + bodySize += chunk.length; + if (bodySize > MAX_FAKE_BACKPRESSURE_SIZE) { + break; + } } this.#bodyChunks.push(chunk); - callback(); + + if (callback) callback(); + return bodySize < MAX_FAKE_BACKPRESSURE_SIZE; } - _writev(chunks, callback) { - if (!this.#bodyChunks) { - this.#bodyChunks = chunks; - callback(); - return; + end(chunk, encoding, callback) { + if ($isCallable(chunk)) { + callback = chunk; + chunk = undefined; + encoding = undefined; + } else if ($isCallable(encoding)) { + callback = encoding; + encoding = undefined; + } else if (!$isCallable(callback)) { + callback = undefined; } - this.#bodyChunks.push(...chunks); - callback(); + + if (chunk) { + if (this[finishedSymbol]) { + emitErrorNextTickIfErrorListenerNT(this, $ERR_STREAM_WRITE_AFTER_END("Cannot write after end"), callback); + return this; + } + + this.#write(chunk, encoding, callback); + } + + if (callback) { + this.once("finish", callback); + } + + this.#send(); + return this; } - _destroy(err, callback) { + destroy(err?: Error, callback) { + if (this.destroyed) return this; this.destroyed = true; + + this[finishedSymbol] = true; // If request is destroyed we abort the current response this[kAbortController]?.abort?.(); - this.socket.destroy(); - emitErrorNextTick(this, err, callback); + this.socket.destroy(err); + + return this; } _ensureTls() { @@ -1545,8 +2314,8 @@ class ClientRequest extends OutgoingMessage { return this.#tls; } - _final(callback) { - this.#finished = true; + #send() { + this[finishedSymbol] = true; this[kAbortController] = new AbortController(); this[kAbortController].signal.addEventListener( "abort", @@ -1563,7 +2332,10 @@ class ClientRequest extends OutgoingMessage { } var method = this.#method, - body = this.#bodyChunks?.length === 1 ? this.#bodyChunks[0] : Buffer.concat(this.#bodyChunks || []); + body = this.#bodyChunks && this.#bodyChunks.length > 1 ? new Blob(this.#bodyChunks) : this.#bodyChunks?.[0]; + if (body) { + this.#bodyChunks = []; + } let url: string; let proxy: string | undefined; @@ -1624,22 +2396,41 @@ class ClientRequest extends OutgoingMessage { fetchOptions.unix = socketPath; } - this._writableState.autoDestroy = false; //@ts-ignore this.#fetchRequest = fetch(url, fetchOptions) .then(response => { if (this.aborted) { + this.#maybeEmitClose(); return; } const prevIsHTTPS = isNextIncomingMessageHTTPS; isNextIncomingMessageHTTPS = response.url.startsWith("https:"); var res = (this.#res = new IncomingMessage(response, { - type: "response", - [kInternalRequest]: this, + [typeSymbol]: NodeHTTPIncomingRequestType.FetchResponse, + [reqSymbol]: this, })); isNextIncomingMessageHTTPS = prevIsHTTPS; - this.emit("response", res); + res.req = this; + + process.nextTick( + (self, res) => { + // If the user did not listen for the 'response' event, then they + // can't possibly read the data, so we ._dump() it into the void + // so that the socket doesn't hang there in a paused state. + if (!self.emit("response", res)) { + res._dump(); + } + }, + this, + res, + ); + this.#maybeEmitClose(); + if (res.statusCode === 304) { + res.complete = true; + this.#maybeEmitClose(); + return; + } }) .catch(err => { // Node treats AbortError separately. @@ -1655,18 +2446,51 @@ class ClientRequest extends OutgoingMessage { .finally(() => { this.#fetchRequest = null; this[kClearTimeout](); - emitCloseNT(this); }); } catch (err) { if (!!$debug) globalReportError(err); this.emit("error", err); } finally { - callback(); + this.#maybeEmitFinish(); + } + } + + // --- For faking the events in the right order --- + #maybeEmitSocket() { + if (!(this[kEmitState] & ClientRequestEmitState.socket)) { + this[kEmitState] |= ClientRequestEmitState.socket; + this.emit("socket", this.socket); + } + } + + #maybeEmitPrefinish() { + this.#maybeEmitSocket(); + + if (!(this[kEmitState] & ClientRequestEmitState.prefinish)) { + this[kEmitState] |= ClientRequestEmitState.prefinish; + this.emit("prefinish"); + } + } + + #maybeEmitFinish() { + this.#maybeEmitPrefinish(); + + if (!(this[kEmitState] & ClientRequestEmitState.finish)) { + this[kEmitState] |= ClientRequestEmitState.finish; + this.emit("finish"); + } + } + + #maybeEmitClose() { + this.#maybeEmitPrefinish(); + + if (!this._closed) { + process.nextTick(emitCloseNTAndComplete, this); } } get aborted() { - return this[abortedSymbol] || this.#signal?.aborted || !!this[kAbortController]?.signal.aborted; + return this[abortedSymbol] || this.#signal?.aborted || !!this[kAbortController]?.signal?.aborted; } set aborted(value) { @@ -1717,6 +2541,7 @@ class ClientRequest extends OutgoingMessage { throw ERR_INVALID_ARG_TYPE("options.agent", "Agent-like Object, undefined, or false", agent); } this.#agent = agent; + this.destroyed = false; const protocol = options.protocol || defaultAgent.protocol; let expectedProtocol = defaultAgent.protocol; @@ -1867,7 +2692,7 @@ class ClientRequest extends OutgoingMessage { // this.useChunkedEncodingByDefault = true; // } - this.#finished = false; + this[finishedSymbol] = false; this.#res = null; this.#upgradeOrConnect = false; this.#parser = null; @@ -1943,6 +2768,8 @@ class ClientRequest extends OutgoingMessage { process.nextTick(emitContinueAndSocketNT, this); } + [kEmitState]: number = 0; + setSocketKeepAlive(enable = true, initialDelay = 0) { $debug(`${NODE_HTTP_WARNING}\n`, "WARN: ClientRequest.setSocketKeepAlive is a no-op"); } @@ -1961,7 +2788,7 @@ class ClientRequest extends OutgoingMessage { #onTimeout() { this.#timeoutTimer = undefined; - this[kAbortController]?.abort(); + this[kAbortController]?.abort?.(); this.emit("timeout"); } @@ -2166,7 +2993,7 @@ function _normalizeArgs(args) { function _writeHead(statusCode, reason, obj, response) { statusCode |= 0; if (statusCode < 100 || statusCode > 999) { - throw new Error("status code must be between 100 and 999"); + throw $ERR_HTTP_INVALID_STATUS_CODE(`Invalid status code: ${statusCode}`); } if (typeof reason === "string") { @@ -2214,13 +3041,25 @@ function _writeHead(statusCode, reason, obj, response) { // consisting only of the Status-Line and optional headers, and is // terminated by an empty line. response._hasBody = false; - const req = response.req; - if (req) { - req.complete = true; - } } } +function ServerResponse_writevDeprecated(chunks, callback) { + if (chunks.length === 1 && !this.headersSent && this[firstWriteSymbol] === undefined) { + this[firstWriteSymbol] = chunks[0].chunk; + callback(); + return; + } + + ensureReadableStreamController.$call(this, controller => { + for (const chunk of chunks) { + controller.write(chunk.chunk); + } + + callback(); + }); +} + /** * Makes an HTTP request. * @param {string | URL} url @@ -2253,15 +3092,25 @@ function get(url, options, cb) { } function onError(self, error, cb) { - if (error) { + if ($isCallable(cb)) { cb(error); - } else { - cb(); } } -function emitErrorNextTick(self, err, cb) { - process.nextTick(onError, self, err, cb); +function emitErrorNextTickIfErrorListenerNT(self, err, cb) { + process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb); +} + +function emitErrorNextTickIfErrorListener(self, err, cb) { + if ($isCallable(cb)) { + // This is to keep backward compatible behavior. + // An error is emitted only if there are listeners attached to the event. + if (self.listenerCount("error") == 0) { + cb(); + } else { + cb(err); + } + } } function emitAbortNextTick(self) { @@ -2272,7 +3121,7 @@ const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTT const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0); var globalAgent = new Agent(); -export default { +const http_exports = { Agent, Server, METHODS, @@ -2297,3 +3146,5 @@ export default { ClientRequest, OutgoingMessage, }; + +export default http_exports; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 72936d97851a1..174f65a68d4fb 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -2388,9 +2388,11 @@ class ServerHttp2Session extends Http2Session { this.#parser = null; } this.close(); + this[bunHTTP2Socket] = null; } #onError(error: Error) { + this[bunHTTP2Socket] = null; this.destroy(error); } diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index ef8f56317ba08..aae4c5b26cf7e 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -401,21 +401,23 @@ function processChunk(self, chunk, flushFlag, cb) { const handle = self._handle; if (!handle) return process.nextTick(cb); + const outOffset = self._outOffset; + const byteLength = chunk.byteLength; handle.buffer = chunk; handle.cb = cb; - handle.availOutBefore = self._chunkSize - self._outOffset; - handle.availInBefore = chunk.byteLength; + const availOutBefore = self._chunkSize - outOffset; + handle.availInBefore = byteLength; + handle.availOutBefore = availOutBefore; handle.inOff = 0; handle.flushFlag = flushFlag; - handle.write( flushFlag, // flush chunk, // in 0, // in_off handle.availInBefore, // in_len self._outBuffer, // out - self._outOffset, // out_off - handle.availOutBefore, // out_len + outOffset, // out_off + availOutBefore, // out_len ); } diff --git a/src/jsc.zig b/src/jsc.zig index 8e4c8072649ec..11865ba754a6f 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -29,6 +29,7 @@ pub const Jest = @import("./bun.js/test/jest.zig"); pub const Expect = @import("./bun.js/test/expect.zig"); pub const Snapshot = @import("./bun.js/test/snapshot.zig"); pub const API = struct { + pub const NodeHTTPResponse = @import("./bun.js/api/server.zig").NodeHTTPResponse; pub const Glob = @import("./bun.js/api/glob.zig"); pub const Shell = @import("./shell/shell.zig"); pub const JSBundler = @import("./bun.js/api/JSBundler.zig").JSBundler; diff --git a/src/tagged_pointer.zig b/src/tagged_pointer.zig index ef9122582ff0f..9cbbeb7993c7e 100644 --- a/src/tagged_pointer.zig +++ b/src/tagged_pointer.zig @@ -100,7 +100,7 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { const TagType: type = result.tag_type; - return struct { + return packed struct { pub const Tag = TagType; pub const TagInt = TagSize; pub const type_map: TypeMap(Types) = result.ty_map; @@ -147,6 +147,10 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { return @as(TagType, @enumFromInt(this.repr.data)); } + pub fn case(comptime Type: type) Tag { + return @field(Tag, typeBaseName(@typeName(Type))); + } + /// unsafely cast a tagged pointer to a specific type, without checking that it's really that type pub inline fn as(this: This, comptime Type: type) *Type { comptime assert_type(Type); diff --git a/test/integration/next-pages/test/dev-server.test.ts b/test/integration/next-pages/test/dev-server.test.ts index ae7fadb38dcb6..6a3c553c6e15e 100644 --- a/test/integration/next-pages/test/dev-server.test.ts +++ b/test/integration/next-pages/test/dev-server.test.ts @@ -79,7 +79,7 @@ async function getDevServerURL() { readStream() .catch(e => reject(e)) .finally(() => { - dev_server.unref?.(); + dev_server?.unref?.(); }); await promise; return baseUrl; diff --git a/test/js/node/http/fixtures/log-events.mjs b/test/js/node/http/fixtures/log-events.mjs index bb5dcdd820afc..ef087634dcb38 100644 --- a/test/js/node/http/fixtures/log-events.mjs +++ b/test/js/node/http/fixtures/log-events.mjs @@ -10,7 +10,7 @@ const options = { const req = http.request(options, res => { patchEmitter(res, "res"); - console.log(`STATUS: ${res.statusCode}`); + console.log(`"STATUS: ${res.statusCode}"`); res.setEncoding("utf8"); }); patchEmitter(req, "req"); @@ -21,7 +21,10 @@ function patchEmitter(emitter, prefix) { var oldEmit = emitter.emit; emitter.emit = function () { - console.log([prefix, arguments[0]]); + if (typeof arguments[0] !== "symbol") { + console.log([prefix, arguments[0]]); + } + oldEmit.apply(emitter, arguments); }; } diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index a3f4e2f76775d..1d2674f504756 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -63,7 +63,7 @@ describe("node:http", () => { it("is not marked encrypted (#5867)", async () => { try { var server = createServer((req, res) => { - expect(req.connection.encrypted).toBe(undefined); + expect(req.connection.encrypted).toBe(false); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World"); }); @@ -77,10 +77,8 @@ describe("node:http", () => { } }); it("request & response body streaming (large)", async () => { + const input = Buffer.alloc("hello world, hello world".length * 9000, "hello world, hello world"); try { - const bodyBlob = new Blob(["hello world", "hello world".repeat(9000)]); - const input = await bodyBlob.text(); - var server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); req.on("data", chunk => { @@ -94,11 +92,11 @@ describe("node:http", () => { const url = await listen(server); const res = await fetch(url, { method: "POST", - body: bodyBlob, + body: input, }); const out = await res.text(); - expect(out).toBe(input); + expect(out).toBe(input.toString()); } finally { server.close(); } @@ -269,10 +267,20 @@ describe("node:http", () => { } if (reqUrl.pathname.includes("timeout")) { if (timer) clearTimeout(timer); + req.on("timeout", () => { + console.log("req timeout"); + }); + res.on("timeout", () => { + console.log("res timeout"); + }); timer = setTimeout(() => { + if (res.closed) { + return; + } + res.end("Hello World"); timer = null; - }, 3000); + }, 3000).unref(); return; } if (reqUrl.pathname === "/pathTest") { @@ -416,6 +424,7 @@ describe("node:http", () => { const req = request(`http://localhost:${port}`, res => { let data = ""; res.setEncoding("utf8"); + res.on("data", chunk => { data += chunk; }); @@ -772,62 +781,73 @@ describe("node:http", () => { }); }); - it("request via http proxy, issue#4295", done => { - const proxyServer = createServer(function (req, res) { - let option = url.parse(req.url); - option.host = req.headers.host; - option.headers = req.headers; + it("request via http proxy, issue#4295", async () => { + const http = require("http"); + const defer = Promise.withResolvers(); + let wentThroughProxy = false; - const proxyRequest = request(option, function (proxyResponse) { - res.writeHead(proxyResponse.statusCode, proxyResponse.headers); - proxyResponse.on("data", function (chunk) { - res.write(chunk, "binary"); - }); - proxyResponse.on("end", function () { - res.end(); + const proxyServer = http.createServer((req, res) => { + fetch("http://example.com/" + req.url, { + headers: { + "X-Bun": "true", + }, + }) + .then(response => { + response.body.pipeTo( + new WritableStream({ + write(chunk) { + res.write(chunk); + }, + close() { + res.end(); + wentThroughProxy = true; + }, + }), + ); + }) + .catch(error => { + defer.reject(error); }); - }); - req.on("data", function (chunk) { - proxyRequest.write(chunk, "binary"); - }); - req.on("end", function () { - proxyRequest.end(); - }); }); - proxyServer.listen({ port: 0 }, async (_err, hostname, port) => { + proxyServer.listen({ port: 0 }, () => { const options = { protocol: "http:", - hostname: hostname, - port: port, - path: "http://example.com", + hostname: "127.0.0.1", + port: proxyServer.address().port, + path: "http://example.com/something", headers: { Host: "example.com", - "accept-encoding": "identity", }, }; - const req = request(options, res => { + const req = http.request(options, res => { let data = ""; + res.on("data", chunk => { data += chunk; }); + + res.on("error", error => { + defer.reject(error); + }); + res.on("end", () => { - try { - expect(res.statusCode).toBe(200); - expect(data.length).toBeGreaterThan(0); - expect(data).toContain("This domain is for use in illustrative examples in documents"); - done(); - } catch (err) { - done(err); - } + defer.resolve(data); }); }); - req.on("error", err => { - done(err); + + req.on("error", error => { + defer.reject(error); }); + req.end(); }); + + const result = await defer.promise; + proxyServer.close(); + expect(result.toLowerCase()).toStartWith(`\n\n { @@ -1279,22 +1299,7 @@ describe("server.address should be valid IP", () => { done(err); } }); - test("ServerResponse reply", done => { - const createDone = createDoneDotAll(done); - const doneRequest = createDone(); - try { - const req = {}; - const sendedText = "Bun\n"; - const res = new ServerResponse(req, async (res: Response) => { - expect(await res.text()).toBe(sendedText); - doneRequest(); - }); - res.write(sendedText); - res.end(); - } catch (err) { - doneRequest(err); - } - }); + test("ServerResponse instanceof OutgoingMessage", () => { expect(new ServerResponse({}) instanceof OutgoingMessage).toBe(true); }); @@ -1362,417 +1367,6 @@ it("should not accept untrusted certificates", async () => { server.close(); }); -it("IncomingMessage with a RequestLike object", () => { - const rawHeadersMap = { - "x-test": "test", - "Real-Header": "test", - "content-type": "text/plain", - "User-Agent": "Bun", - }; - - // To excercise the case where inline capacity cannot be used - for (let i = 0; i < 64; i++) { - rawHeadersMap[`header-${i}`] = `value-${i}`; - } - - const headers = new Headers(rawHeadersMap); - headers.append("set-cookie", "foo=bar"); - headers.append("set-cookie", "bar=baz"); - - const request = new Request("https://example.com/hello/hi", { - headers, - }); - - const incomingMessageFromRequest = new IncomingMessage(request); - const incomingMessageFromRequestLike1 = new IncomingMessage({ - url: "/hello/hi", - headers: headers, - method: request.method, - }); - const incomingMessageFromRequestLike2 = new IncomingMessage({ - url: "/hello/hi", - headers: headers.toJSON(), - method: request.method, - }); - for (let incomingMessageFromRequestLike of [ - incomingMessageFromRequestLike1, - incomingMessageFromRequestLike2, - incomingMessageFromRequest, - ]) { - expect(incomingMessageFromRequestLike.headers).toEqual(incomingMessageFromRequest.headers); - expect(incomingMessageFromRequestLike.method).toEqual(incomingMessageFromRequest.method); - expect(incomingMessageFromRequestLike.url).toEqual(incomingMessageFromRequest.url); - expect(incomingMessageFromRequestLike.headers).toEqual({ - "x-test": "test", - "real-header": "test", - "content-type": "text/plain", - "user-agent": "Bun", - "set-cookie": ["foo=bar", "bar=baz"], - "header-0": "value-0", - "header-1": "value-1", - "header-10": "value-10", - "header-11": "value-11", - "header-12": "value-12", - "header-13": "value-13", - "header-14": "value-14", - "header-15": "value-15", - "header-16": "value-16", - "header-17": "value-17", - "header-18": "value-18", - "header-19": "value-19", - "header-2": "value-2", - "header-20": "value-20", - "header-21": "value-21", - "header-22": "value-22", - "header-23": "value-23", - "header-24": "value-24", - "header-25": "value-25", - "header-26": "value-26", - "header-27": "value-27", - "header-28": "value-28", - "header-29": "value-29", - "header-3": "value-3", - "header-30": "value-30", - "header-31": "value-31", - "header-32": "value-32", - "header-33": "value-33", - "header-34": "value-34", - "header-35": "value-35", - "header-36": "value-36", - "header-37": "value-37", - "header-38": "value-38", - "header-39": "value-39", - "header-4": "value-4", - "header-40": "value-40", - "header-41": "value-41", - "header-42": "value-42", - "header-43": "value-43", - "header-44": "value-44", - "header-45": "value-45", - "header-46": "value-46", - "header-47": "value-47", - "header-48": "value-48", - "header-49": "value-49", - "header-5": "value-5", - "header-50": "value-50", - "header-51": "value-51", - "header-52": "value-52", - "header-53": "value-53", - "header-54": "value-54", - "header-55": "value-55", - "header-56": "value-56", - "header-57": "value-57", - "header-58": "value-58", - "header-59": "value-59", - "header-6": "value-6", - "header-60": "value-60", - "header-61": "value-61", - "header-62": "value-62", - "header-63": "value-63", - "header-7": "value-7", - "header-8": "value-8", - "header-9": "value-9", - }); - } - - // this one preserves the original case - expect(incomingMessageFromRequestLike1.rawHeaders).toEqual([ - "content-type", - "text/plain", - "user-agent", - "Bun", - "set-cookie", - "foo=bar", - "set-cookie", - "bar=baz", - "x-test", - "test", - "Real-Header", - "test", - "header-0", - "value-0", - "header-1", - "value-1", - "header-2", - "value-2", - "header-3", - "value-3", - "header-4", - "value-4", - "header-5", - "value-5", - "header-6", - "value-6", - "header-7", - "value-7", - "header-8", - "value-8", - "header-9", - "value-9", - "header-10", - "value-10", - "header-11", - "value-11", - "header-12", - "value-12", - "header-13", - "value-13", - "header-14", - "value-14", - "header-15", - "value-15", - "header-16", - "value-16", - "header-17", - "value-17", - "header-18", - "value-18", - "header-19", - "value-19", - "header-20", - "value-20", - "header-21", - "value-21", - "header-22", - "value-22", - "header-23", - "value-23", - "header-24", - "value-24", - "header-25", - "value-25", - "header-26", - "value-26", - "header-27", - "value-27", - "header-28", - "value-28", - "header-29", - "value-29", - "header-30", - "value-30", - "header-31", - "value-31", - "header-32", - "value-32", - "header-33", - "value-33", - "header-34", - "value-34", - "header-35", - "value-35", - "header-36", - "value-36", - "header-37", - "value-37", - "header-38", - "value-38", - "header-39", - "value-39", - "header-40", - "value-40", - "header-41", - "value-41", - "header-42", - "value-42", - "header-43", - "value-43", - "header-44", - "value-44", - "header-45", - "value-45", - "header-46", - "value-46", - "header-47", - "value-47", - "header-48", - "value-48", - "header-49", - "value-49", - "header-50", - "value-50", - "header-51", - "value-51", - "header-52", - "value-52", - "header-53", - "value-53", - "header-54", - "value-54", - "header-55", - "value-55", - "header-56", - "value-56", - "header-57", - "value-57", - "header-58", - "value-58", - "header-59", - "value-59", - "header-60", - "value-60", - "header-61", - "value-61", - "header-62", - "value-62", - "header-63", - "value-63", - ]); - - // this one does not preserve the original case - expect(incomingMessageFromRequestLike2.rawHeaders).toEqual([ - "content-type", - "text/plain", - "user-agent", - "Bun", - "set-cookie", - "foo=bar", - "set-cookie", - "bar=baz", - "x-test", - "test", - "real-header", - "test", - "header-0", - "value-0", - "header-1", - "value-1", - "header-2", - "value-2", - "header-3", - "value-3", - "header-4", - "value-4", - "header-5", - "value-5", - "header-6", - "value-6", - "header-7", - "value-7", - "header-8", - "value-8", - "header-9", - "value-9", - "header-10", - "value-10", - "header-11", - "value-11", - "header-12", - "value-12", - "header-13", - "value-13", - "header-14", - "value-14", - "header-15", - "value-15", - "header-16", - "value-16", - "header-17", - "value-17", - "header-18", - "value-18", - "header-19", - "value-19", - "header-20", - "value-20", - "header-21", - "value-21", - "header-22", - "value-22", - "header-23", - "value-23", - "header-24", - "value-24", - "header-25", - "value-25", - "header-26", - "value-26", - "header-27", - "value-27", - "header-28", - "value-28", - "header-29", - "value-29", - "header-30", - "value-30", - "header-31", - "value-31", - "header-32", - "value-32", - "header-33", - "value-33", - "header-34", - "value-34", - "header-35", - "value-35", - "header-36", - "value-36", - "header-37", - "value-37", - "header-38", - "value-38", - "header-39", - "value-39", - "header-40", - "value-40", - "header-41", - "value-41", - "header-42", - "value-42", - "header-43", - "value-43", - "header-44", - "value-44", - "header-45", - "value-45", - "header-46", - "value-46", - "header-47", - "value-47", - "header-48", - "value-48", - "header-49", - "value-49", - "header-50", - "value-50", - "header-51", - "value-51", - "header-52", - "value-52", - "header-53", - "value-53", - "header-54", - "value-54", - "header-55", - "value-55", - "header-56", - "value-56", - "header-57", - "value-57", - "header-58", - "value-58", - "header-59", - "value-59", - "header-60", - "value-60", - "header-61", - "value-61", - "header-62", - "value-62", - "header-63", - "value-63", - ]); -}); - -it("#6892", () => { - const totallyValid = ["*", "/", "/foo", "/foo/bar"]; - for (const url of totallyValid) { - const req = new IncomingMessage({ url }); - expect(req.url).toBe(url); - expect(req.method).toBeNull(); - } -}); - it("#4415.1 ServerResponse es6", () => { class Response extends ServerResponse { constructor(req) { @@ -1811,10 +1405,43 @@ it("#4415.3 Server es5", done => { }); }); -it("#4415.4 IncomingMessage es5", () => { +it("#4415.4 IncomingMessage es5", done => { + // This matches Node.js: const im = Object.create(IncomingMessage.prototype); IncomingMessage.call(im, { url: "/foo" }); - expect(im.url).toBe("/foo"); + expect(im.url).toBe(""); + + let didCall = false; + function Subclass(...args) { + IncomingMessage.apply(this, args); + didCall = true; + } + Object.setPrototypeOf(Subclass.prototype, IncomingMessage.prototype); + Object.setPrototypeOf(Subclass, IncomingMessage); + + const server = new Server( + { + IncomingMessage: Subclass, + }, + (req, res) => { + if (req instanceof Subclass && didCall) { + expect(req.url).toBe("/foo"); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello"); + } else { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("bye"); + } + }, + ); + server.listen(0, () => { + fetch(`http://localhost:${server.address().port}/foo`, { + method: "GET", + }).then(response => { + expect(response.status).toBe(200); + server.close(done); + }); + }); }); it("#9242.1 Server has constructor", () => { @@ -1885,19 +1512,18 @@ it("should emit events in the right order", async () => { expect(err).toBeEmpty(); const out = await new Response(stdout).text(); // TODO prefinish and socket are not emitted in the right order - expect(out.split("\n")).toEqual([ - `[ "req", "prefinish" ]`, - `[ "req", "socket" ]`, - `[ "req", "finish" ]`, - `[ "req", "response" ]`, + expect(out.split("\n").filter(Boolean).map(JSON.parse)).toStrictEqual([ + ["req", "socket"], + ["req", "prefinish"], + ["req", "finish"], + ["req", "response"], "STATUS: 200", - // `[ "res", "resume" ]`, - // `[ "res", "readable" ]`, - // `[ "res", "end" ]`, - `[ "req", "close" ]`, - `[ "res", Symbol(kConstruct) ]`, - // `[ "res", "close" ]`, - "", + // TODO: not totally right: + ["req", "close"], + ["res", "resume"], + ["res", "readable"], + ["res", "end"], + ["res", "close"], ]); }); @@ -2332,6 +1958,7 @@ it("should emit close, and complete should be true only after close #13373", asy const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); + console.log("ok 1"); const closeEvent = once(req, "close"); res.end("hi"); @@ -2344,6 +1971,7 @@ it("should emit close, and complete should be true only after close #13373", asy it("should emit close when connection is aborted", async () => { const server = http.createServer().listen(0); + server.unref(); try { await once(server, "listening"); const controller = new AbortController(); @@ -2352,11 +1980,13 @@ it("should emit close when connection is aborted", async () => { .catch(() => {}); const [req, res] = await once(server, "request"); - expect(req.complete).toBe(false); - const closeEvent = once(req, "close"); + const closeEvent = Promise.withResolvers(); + req.once("close", () => { + closeEvent.resolve(); + }); controller.abort(); - await closeEvent; - expect(req.complete).toBe(true); + await closeEvent.promise; + expect(req.aborted).toBe(true); } finally { server.close(); } @@ -2373,7 +2003,7 @@ it("should emit timeout event", async () => { const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); let callBackCalled = false; - req.setTimeout(1000, () => { + req.setTimeout(100, () => { callBackCalled = true; }); await once(req, "timeout"); @@ -2388,16 +2018,19 @@ it("should emit timeout event when using server.setTimeout", async () => { try { await once(server, "listening"); let callBackCalled = false; - server.setTimeout(1000, () => { + server.setTimeout(100, () => { callBackCalled = true; + console.log("Called timeout"); }); - fetch(`http://localhost:${server.address().port}`) + + fetch(`http://localhost:${server.address().port}`, { verbose: true }) .then(res => res.text()) - .catch(() => {}); + .catch(err => { + console.log(err); + }); const [req, res] = await once(server, "request"); expect(req.complete).toBe(false); - await once(server, "timeout"); expect(callBackCalled).toBe(true); } finally { diff --git a/test/js/node/test/parallel/http-hex-write.test.js b/test/js/node/test/parallel/http-hex-write.test.js new file mode 100644 index 0000000000000..1606bcac188a7 --- /dev/null +++ b/test/js/node/test/parallel/http-hex-write.test.js @@ -0,0 +1,60 @@ +//#FILE: test-http-hex-write.js +//#SHA1: 77a5322a8fe08e8505f39d42614167d223c9fbb0 +//----------------- +// 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 http = require("http"); + +const expectedResponse = "hex\nutf8\n"; + +test("HTTP server writes hex and utf8", async () => { + const server = http.createServer((req, res) => { + res.setHeader("content-length", expectedResponse.length); + res.write("6865780a", "hex"); + res.write("utf8\n"); + res.end(); + server.close(); + }); + + await new Promise(resolve => { + server.listen(0, () => { + const port = server.address().port; + http + .request({ port }) + .on("response", res => { + let data = ""; + res.setEncoding("ascii"); + res.on("data", chunk => { + data += chunk; + }); + res.on("end", () => { + expect(data).toBe(expectedResponse); + resolve(); + }); + }) + .end(); + }); + }); +}); + +//<#END_FILE: test-http-hex-write.js diff --git a/test/js/node/test/parallel/http-keepalive-request.test.js b/test/js/node/test/parallel/http-keepalive-request.test.js new file mode 100644 index 0000000000000..956c421a4a50a --- /dev/null +++ b/test/js/node/test/parallel/http-keepalive-request.test.js @@ -0,0 +1,99 @@ +//#FILE: test-http-keepalive-request.js +//#SHA1: 31cc9b875e1ead9a0b98fb07b672b536f6d06fba +//----------------- +// 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 http = require("http"); + +let serverSocket = null; +let clientSocket = null; +const expectRequests = 10; +let actualRequests = 0; + +const server = http.createServer((req, res) => { + // They should all come in on the same server socket. + if (serverSocket) { + expect(req.socket).toBe(serverSocket); + } else { + serverSocket = req.socket; + } + + res.end(req.url); +}); + +const agent = new http.Agent({ keepAlive: true }); + +function makeRequest(n) { + return new Promise(resolve => { + if (n === 0) { + server.close(); + agent.destroy(); + resolve(); + return; + } + + const req = http.request({ + port: server.address().port, + path: `/${n}`, + agent: agent, + }); + + req.end(); + + req.on("socket", sock => { + if (clientSocket) { + expect(sock).toBe(clientSocket); + } else { + clientSocket = sock; + } + }); + + req.on("response", res => { + let data = ""; + res.setEncoding("utf8"); + res.on("data", c => { + data += c; + }); + res.on("end", () => { + expect(data).toBe(`/${n}`); + setTimeout(() => { + actualRequests++; + resolve(makeRequest(n - 1)); + }, 1); + }); + }); + }); +} + +test("HTTP keep-alive requests", async () => { + await new Promise(resolve => { + server.listen(0, () => { + resolve(makeRequest(expectRequests)); + }); + }); + + expect(actualRequests).toBe(expectRequests); +}); + +//<#END_FILE: test-http-keepalive-request.js diff --git a/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js b/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js new file mode 100644 index 0000000000000..23234f80bc181 --- /dev/null +++ b/test/js/node/test/parallel/http-pipeline-requests-connection-leak.test.js @@ -0,0 +1,50 @@ +//#FILE: test-http-pipeline-requests-connection-leak.js +//#SHA1: fc3e33a724cc7a499c7716fe8af6b78e7f72e943 +//----------------- +"use strict"; + +const http = require("http"); +const net = require("net"); + +const big = Buffer.alloc(16 * 1024, "A"); + +const COUNT = 1e4; + +test("HTTP pipeline requests do not cause connection leak", done => { + let client; + const server = http.createServer((req, res) => { + res.end(big, () => { + countdown.dec(); + }); + }); + + const countdown = new Countdown(COUNT, () => { + server.close(); + client.end(); + done(); + }); + + server.listen(0, () => { + const req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".repeat(COUNT); + client = net.connect(server.address().port, () => { + client.write(req); + }); + client.resume(); + }); +}); + +class Countdown { + constructor(count, callback) { + this.count = count; + this.callback = callback; + } + + dec() { + this.count--; + if (this.count === 0) { + this.callback(); + } + } +} + +//<#END_FILE: test-http-pipeline-requests-connection-leak.js diff --git a/test/js/node/test/parallel/http-readable-data-event.test.js b/test/js/node/test/parallel/http-readable-data-event.test.js new file mode 100644 index 0000000000000..238c44878b8bd --- /dev/null +++ b/test/js/node/test/parallel/http-readable-data-event.test.js @@ -0,0 +1,75 @@ +//#FILE: test-http-readable-data-event.js +//#SHA1: a094638e155550b5bc5ecdf5b11c54e8124d1dc1 +//----------------- +"use strict"; + +const http = require("http"); +const helloWorld = "Hello World!"; +const helloAgainLater = "Hello again later!"; + +let next = null; + +test("HTTP readable data event", async () => { + const server = http.createServer((req, res) => { + res.writeHead(200, { + "Content-Length": `${helloWorld.length + helloAgainLater.length}`, + }); + + // We need to make sure the data is flushed + // before writing again + next = () => { + res.end(helloAgainLater); + next = () => {}; + }; + + res.write(helloWorld); + }); + + await new Promise(resolve => server.listen(0, resolve)); + + const opts = { + hostname: "localhost", + port: server.address().port, + path: "/", + }; + + const expectedData = [helloWorld, helloAgainLater]; + const expectedRead = [helloWorld, null, helloAgainLater, null, null]; + + await new Promise((resolve, reject) => { + const req = http.request(opts, res => { + res.on("error", reject); + + const readableSpy = jest.fn(); + res.on("readable", readableSpy); + + res.on("readable", () => { + let data; + + do { + data = res.read(); + expect(data).toBe(expectedRead.shift()); + next(); + } while (data !== null); + }); + + res.setEncoding("utf8"); + const dataSpy = jest.fn(); + res.on("data", dataSpy); + + res.on("data", data => { + expect(data).toBe(expectedData.shift()); + }); + + res.on("end", () => { + expect(readableSpy).toHaveBeenCalledTimes(3); + expect(dataSpy).toHaveBeenCalledTimes(2); + server.close(() => resolve()); + }); + }); + + req.end(); + }); +}); + +//<#END_FILE: test-http-readable-data-event.js diff --git a/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js b/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js new file mode 100644 index 0000000000000..c01808e44067b --- /dev/null +++ b/test/js/node/test/parallel/http-response-remove-header-after-sent.test.js @@ -0,0 +1,30 @@ +//#FILE: test-http-response-remove-header-after-sent.js +//#SHA1: df9a9a2f545c88b70d6d33252a1568339ea6f5b3 +//----------------- +"use strict"; + +const http = require("http"); + +test("remove header after response is sent", done => { + const server = http.createServer((req, res) => { + res.removeHeader("header1", 1); + res.write("abc"); + expect(() => res.removeHeader("header2", 2)).toThrow( + expect.objectContaining({ + code: "ERR_HTTP_HEADERS_SENT", + name: "Error", + message: expect.any(String), + }), + ); + res.end(); + }); + + server.listen(0, () => { + http.get({ port: server.address().port }, () => { + server.close(); + done(); + }); + }); +}); + +//<#END_FILE: test-http-response-remove-header-after-sent.js diff --git a/test/js/third_party/body-parser/express-body-parser-test.test.ts b/test/js/third_party/body-parser/express-body-parser-test.test.ts index 841d2f8c1dcce..99e2fed2e2125 100644 --- a/test/js/third_party/body-parser/express-body-parser-test.test.ts +++ b/test/js/third_party/body-parser/express-body-parser-test.test.ts @@ -35,9 +35,21 @@ test("httpServer", async () => { }); app.use(json()); + let closeCount = 0; + let responseCloseCount = 0; var reached = false; // This throws a TypeError since it uses body-parser.json app.post("/ping", (request: Request, response: Response) => { + request.on("close", () => { + if (closeCount++ === 1) { + throw new Error("request Close called multiple times"); + } + }); + response.on("close", () => { + if (responseCloseCount++ === 1) { + throw new Error("response Close called multiple times"); + } + }); expect(request.body).toEqual({ hello: "world" }); expect(request.query).toStrictEqual({ hello: "123", diff --git a/test/js/web/fetch/client-fetch.test.ts b/test/js/web/fetch/client-fetch.test.ts index bf6d19e3285bd..b4064f0a9535f 100644 --- a/test/js/web/fetch/client-fetch.test.ts +++ b/test/js/web/fetch/client-fetch.test.ts @@ -287,17 +287,20 @@ test("redirect with body", async () => { let count = 0; await using server = createServer(async (req, res) => { let body = ""; - for await (const chunk of req) { + req.on("data", chunk => { body += chunk; - } - expect(body).toBe("asd"); - if (count++ === 0) { - res.setHeader("location", "asd"); - res.statusCode = 302; - res.end(); - } else { - res.end(String(count)); - } + }); + + req.on("end", () => { + expect(body).toBe("asd"); + if (count++ === 0) { + res.setHeader("location", "asd"); + res.statusCode = 302; + res.end(); + } else { + res.end(String(count)); + } + }); }).listen(0); await once(server, "listening"); diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 21a72ede53362..301e6a9b0c791 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -221,7 +221,7 @@ describe("fetch() with streaming", () => { expect(true).toBe(true); } finally { - server?.close(); + server?.closeAllConnections(); } }); } diff --git a/test/regression/issue/04298/04298.fixture.js b/test/regression/issue/04298/04298.fixture.js index 77161560d6f82..6c8374ce42ca7 100644 --- a/test/regression/issue/04298/04298.fixture.js +++ b/test/regression/issue/04298/04298.fixture.js @@ -5,11 +5,13 @@ const server = createServer((req, res) => { throw new Error("Oops!"); }); -server.listen({ port: 0 }, async (err, host, port) => { +server.listen({ port: 0 }, async err => { + const { port, address: host } = server.address(); if (err) { console.error(err); process.exit(1); } const hostname = isIPv6(host) ? `[${host}]` : host; - process.send(`http://${hostname}:${port}/`); + + (process?.connected ? process.send : console.log)(`http://${hostname}:${port}/`); }); diff --git a/test/regression/issue/04298/04298.test.ts b/test/regression/issue/04298/04298.test.ts index ea41f5bcce486..d4def8502863a 100644 --- a/test/regression/issue/04298/04298.test.ts +++ b/test/regression/issue/04298/04298.test.ts @@ -2,13 +2,13 @@ import { spawn } from "bun"; import { expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; -test("node:http should not crash when server throws", async () => { +test("node:http should not crash when server throws, and should abruptly close the socket", async () => { const { promise, resolve, reject } = Promise.withResolvers(); await using server = spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "04298.fixture.js"], env: bunEnv, - stderr: "pipe", + stderr: "inherit", ipc(url) { resolve(url); }, @@ -20,5 +20,4 @@ test("node:http should not crash when server throws", async () => { }); const url = await promise; const response = await fetch(url); - expect(response.status).toBe(500); }); diff --git a/test/regression/issue/04298/node-fixture.mjs b/test/regression/issue/04298/node-fixture.mjs new file mode 100644 index 0000000000000..cfbdca3c11c95 --- /dev/null +++ b/test/regression/issue/04298/node-fixture.mjs @@ -0,0 +1,41 @@ +const { spawn } = await import("child_process"); +const assert = await import("assert"); +const http = await import("http"); + +async function runTest() { + return new Promise((resolve, reject) => { + const server = spawn("node", ["04298.fixture.js"], { + cwd: import.meta.dirname, + stdio: ["inherit", "inherit", "inherit", "ipc"], + }); + + server.on("message", url => { + http + .get(url, res => { + assert.strictEqual(res.statusCode, 500); + server.kill(); + resolve(); + }) + .on("error", reject); + }); + + server.on("error", reject); + server.on("exit", (code, signal) => { + if (code !== null && code !== 0) { + reject(new Error(`Server exited with code ${code}`)); + } else if (signal) { + reject(new Error(`Server was killed with signal ${signal}`)); + } + }); + }); +} + +runTest() + .then(() => { + console.log("Test passed"); + process.exit(0); + }) + .catch(error => { + console.error("Test failed:", error); + process.exit(1); + });