Files
bun.sh/src/http/HTTPContext.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

505 lines
20 KiB
Zig

pub fn NewHTTPContext(comptime ssl: bool) type {
return struct {
const pool_size = 64;
const PooledSocket = struct {
http_socket: HTTPSocket,
hostname_buf: [MAX_KEEPALIVE_HOSTNAME]u8 = undefined,
hostname_len: u8 = 0,
port: u16 = 0,
/// If you set `rejectUnauthorized` to `false`, the connection fails to verify,
did_have_handshaking_error_while_reject_unauthorized_is_false: bool = false,
};
pub fn markTaggedSocketAsDead(socket: HTTPSocket, tagged: ActiveSocket) void {
if (tagged.is(PooledSocket)) {
Handler.addMemoryBackToPool(tagged.as(PooledSocket));
}
if (socket.ext(**anyopaque)) |ctx| {
ctx.* = bun.cast(**anyopaque, ActiveSocket.init(dead_socket).ptr());
}
}
pub fn markSocketAsDead(socket: HTTPSocket) void {
markTaggedSocketAsDead(socket, getTaggedFromSocket(socket));
}
pub fn terminateSocket(socket: HTTPSocket) void {
markSocketAsDead(socket);
socket.close(.failure);
}
pub fn closeSocket(socket: HTTPSocket) void {
markSocketAsDead(socket);
socket.close(.normal);
}
fn getTagged(ptr: *anyopaque) ActiveSocket {
return ActiveSocket.from(bun.cast(**anyopaque, ptr).*);
}
pub fn getTaggedFromSocket(socket: HTTPSocket) ActiveSocket {
if (socket.ext(anyopaque)) |ctx| {
return getTagged(ctx);
}
return ActiveSocket.init(dead_socket);
}
pub const PooledSocketHiveAllocator = bun.HiveArray(PooledSocket, pool_size);
pending_sockets: PooledSocketHiveAllocator,
us_socket_context: *uws.SocketContext,
const Context = @This();
pub const HTTPSocket = uws.NewSocketHandler(ssl);
pub fn context() *@This() {
if (comptime ssl) {
return &bun.http.http_thread.https_context;
} else {
return &bun.http.http_thread.http_context;
}
}
const ActiveSocket = TaggedPointerUnion(.{
DeadSocket,
HTTPClient,
PooledSocket,
});
const ssl_int = @as(c_int, @intFromBool(ssl));
const MAX_KEEPALIVE_HOSTNAME = 128;
pub fn sslCtx(this: *@This()) *BoringSSL.SSL_CTX {
if (comptime !ssl) {
unreachable;
}
return @as(*BoringSSL.SSL_CTX, @ptrCast(this.us_socket_context.getNativeHandle(true)));
}
pub fn deinit(this: *@This()) void {
this.us_socket_context.deinit(ssl);
bun.default_allocator.destroy(this);
}
pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) InitError!void {
if (!comptime ssl) {
@compileError("ssl only");
}
var opts = client.tls_props.?.asUSockets();
opts.request_cert = 1;
opts.reject_unauthorized = 0;
try this.initWithOpts(&opts);
}
fn initWithOpts(this: *@This(), opts: *const uws.SocketContext.BunSocketContextOptions) InitError!void {
if (!comptime ssl) {
@compileError("ssl only");
}
var err: uws.create_bun_socket_error_t = .none;
const socket = uws.SocketContext.createSSLContext(bun.http.http_thread.loop.loop, @sizeOf(usize), opts.*, &err);
if (socket == null) {
return switch (err) {
.load_ca_file => error.LoadCAFile,
.invalid_ca_file => error.InvalidCAFile,
.invalid_ca => error.InvalidCA,
else => error.FailedToOpenSocket,
};
}
this.us_socket_context = socket.?;
this.sslCtx().setup();
HTTPSocket.configure(
this.us_socket_context,
false,
anyopaque,
Handler,
);
}
pub fn initWithThreadOpts(this: *@This(), init_opts: *const HTTPThread.InitOpts) InitError!void {
if (!comptime ssl) {
@compileError("ssl only");
}
var opts: uws.SocketContext.BunSocketContextOptions = .{
.ca = if (init_opts.ca.len > 0) @ptrCast(init_opts.ca) else null,
.ca_count = @intCast(init_opts.ca.len),
.ca_file_name = if (init_opts.abs_ca_file_name.len > 0) init_opts.abs_ca_file_name else null,
.request_cert = 1,
};
try this.initWithOpts(&opts);
}
pub fn init(this: *@This()) void {
if (comptime ssl) {
const opts: uws.SocketContext.BunSocketContextOptions = .{
// we request the cert so we load root certs and can verify it
.request_cert = 1,
// we manually abort the connection if the hostname doesn't match
.reject_unauthorized = 0,
};
var err: uws.create_bun_socket_error_t = .none;
this.us_socket_context = uws.SocketContext.createSSLContext(bun.http.http_thread.loop.loop, @sizeOf(usize), opts, &err).?;
this.sslCtx().setup();
} else {
this.us_socket_context = uws.SocketContext.createNoSSLContext(bun.http.http_thread.loop.loop, @sizeOf(usize)).?;
}
HTTPSocket.configure(
this.us_socket_context,
false,
anyopaque,
Handler,
);
}
/// Attempt to keep the socket alive by reusing it for another request.
/// If no space is available, close the socket.
///
/// If `did_have_handshaking_error_while_reject_unauthorized_is_false`
/// is set, then we can only reuse the socket for HTTP Keep Alive if
/// `reject_unauthorized` is set to `false`.
pub fn releaseSocket(this: *@This(), socket: HTTPSocket, did_have_handshaking_error_while_reject_unauthorized_is_false: bool, hostname: []const u8, port: u16) void {
// log("releaseSocket(0x{f})", .{bun.fmt.hexIntUpper(@intFromPtr(socket.socket))});
if (comptime Environment.allow_assert) {
assert(!socket.isClosed());
assert(!socket.isShutdown());
assert(socket.isEstablished());
}
assert(hostname.len > 0);
assert(port > 0);
if (hostname.len <= MAX_KEEPALIVE_HOSTNAME and !socket.isClosedOrHasError() and socket.isEstablished()) {
if (this.pending_sockets.get()) |pending| {
if (socket.ext(**anyopaque)) |ctx| {
ctx.* = bun.cast(**anyopaque, ActiveSocket.init(pending).ptr());
}
socket.flush();
socket.timeout(0);
socket.setTimeoutMinutes(5);
pending.http_socket = socket;
pending.did_have_handshaking_error_while_reject_unauthorized_is_false = did_have_handshaking_error_while_reject_unauthorized_is_false;
@memcpy(pending.hostname_buf[0..hostname.len], hostname);
pending.hostname_len = @as(u8, @truncate(hostname.len));
pending.port = port;
log("Keep-Alive release {s}:{d}", .{
hostname,
port,
});
return;
}
}
log("close socket", .{});
closeSocket(socket);
}
pub const Handler = struct {
pub fn onOpen(
ptr: *anyopaque,
socket: HTTPSocket,
) void {
const active = getTagged(ptr);
if (active.get(HTTPClient)) |client| {
if (client.onOpen(comptime ssl, socket)) |_| {
return;
} else |_| {
log("Unable to open socket", .{});
terminateSocket(socket);
return;
}
}
log("Unexpected open on unknown socket", .{});
terminateSocket(socket);
}
pub fn onHandshake(
ptr: *anyopaque,
socket: HTTPSocket,
success: i32,
ssl_error: uws.us_bun_verify_error_t,
) void {
const handshake_success = if (success == 1) true else false;
const handshake_error = HTTPCertError{
.error_no = ssl_error.error_no,
.code = if (ssl_error.code == null) "" else ssl_error.code[0..bun.len(ssl_error.code) :0],
.reason = if (ssl_error.code == null) "" else ssl_error.reason[0..bun.len(ssl_error.reason) :0],
};
const active = getTagged(ptr);
if (active.get(HTTPClient)) |client| {
// handshake completed but we may have ssl errors
client.flags.did_have_handshaking_error = handshake_error.error_no != 0;
if (handshake_success) {
if (client.flags.reject_unauthorized) {
// only reject the connection if reject_unauthorized == true
if (client.flags.did_have_handshaking_error) {
client.closeAndFail(BoringSSL.getCertErrorFromNo(handshake_error.error_no), comptime ssl, socket);
return;
}
// if checkServerIdentity returns false, we dont call open this means that the connection was rejected
const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(socket.getNativeHandle()));
if (!client.checkServerIdentity(comptime ssl, socket, handshake_error, ssl_ptr, true)) {
client.flags.did_have_handshaking_error = true;
client.unregisterAbortTracker();
if (!socket.isClosed()) terminateSocket(socket);
return;
}
}
return client.firstCall(comptime ssl, socket);
} else {
// if we are here is because server rejected us, and the error_no is the cause of this
// if we set reject_unauthorized == false this means the server requires custom CA aka NODE_EXTRA_CA_CERTS
if (client.flags.did_have_handshaking_error) {
client.closeAndFail(BoringSSL.getCertErrorFromNo(handshake_error.error_no), comptime ssl, socket);
return;
}
// if handshake_success it self is false, this means that the connection was rejected
client.closeAndFail(error.ConnectionRefused, comptime ssl, socket);
return;
}
}
if (socket.isClosed()) {
markSocketAsDead(socket);
return;
}
if (handshake_success) {
if (active.is(PooledSocket)) {
// Allow pooled sockets to be reused if the handshake was successful.
socket.setTimeout(0);
socket.setTimeoutMinutes(5);
return;
}
}
terminateSocket(socket);
}
pub fn onClose(
ptr: *anyopaque,
socket: HTTPSocket,
_: c_int,
_: ?*anyopaque,
) void {
const tagged = getTagged(ptr);
markSocketAsDead(socket);
if (tagged.get(HTTPClient)) |client| {
return client.onClose(comptime ssl, socket);
}
}
fn addMemoryBackToPool(pooled: *PooledSocket) void {
assert(context().pending_sockets.put(pooled));
}
pub fn onData(
ptr: *anyopaque,
socket: HTTPSocket,
buf: []const u8,
) void {
const tagged = getTagged(ptr);
if (tagged.get(HTTPClient)) |client| {
return client.onData(
comptime ssl,
buf,
if (comptime ssl) &bun.http.http_thread.https_context else &bun.http.http_thread.http_context,
socket,
);
} else if (tagged.is(PooledSocket)) {
// trailing zero is fine to ignore
if (strings.eqlComptime(buf, bun.http.end_of_chunked_http1_1_encoding_response_body)) {
return;
}
log("Unexpected data on socket", .{});
return;
}
log("Unexpected data on unknown socket", .{});
terminateSocket(socket);
}
pub fn onWritable(
ptr: *anyopaque,
socket: HTTPSocket,
) void {
const tagged = getTagged(ptr);
if (tagged.get(HTTPClient)) |client| {
return client.onWritable(
false,
comptime ssl,
socket,
);
} else if (tagged.is(PooledSocket)) {
// it's a keep-alive socket
} else {
// don't know what this is, let's close it
log("Unexpected writable on socket", .{});
terminateSocket(socket);
}
}
pub fn onLongTimeout(
ptr: *anyopaque,
socket: HTTPSocket,
) void {
const tagged = getTagged(ptr);
if (tagged.get(HTTPClient)) |client| {
return client.onTimeout(comptime ssl, socket);
}
terminateSocket(socket);
}
pub fn onConnectError(
ptr: *anyopaque,
socket: HTTPSocket,
_: c_int,
) void {
const tagged = getTagged(ptr);
markTaggedSocketAsDead(socket, tagged);
if (tagged.get(HTTPClient)) |client| {
client.onConnectError();
}
// us_connecting_socket_close is always called internally by uSockets
}
pub fn onEnd(
ptr: *anyopaque,
socket: HTTPSocket,
) void {
// TCP fin must be closed, but we must keep the original tagged
// pointer so that their onClose callback is called.
//
// Three possible states:
// 1. HTTP Keep-Alive socket: it must be removed from the pool
// 2. HTTP Client socket: it might need to be retried
// 3. Dead socket: it is already marked as dead
const tagged = getTagged(ptr);
markTaggedSocketAsDead(socket, tagged);
socket.close(.failure);
if (tagged.get(HTTPClient)) |client| {
client.onClose(comptime ssl, socket);
return;
}
}
};
fn existingSocket(this: *@This(), reject_unauthorized: bool, hostname: []const u8, port: u16) ?HTTPSocket {
if (hostname.len > MAX_KEEPALIVE_HOSTNAME)
return null;
var iter = this.pending_sockets.used.iterator(.{ .kind = .set });
while (iter.next()) |pending_socket_index| {
var socket = this.pending_sockets.at(@as(u16, @intCast(pending_socket_index)));
if (socket.port != port) {
continue;
}
if (socket.did_have_handshaking_error_while_reject_unauthorized_is_false and reject_unauthorized) {
continue;
}
if (strings.eqlLong(socket.hostname_buf[0..socket.hostname_len], hostname, true)) {
const http_socket = socket.http_socket;
if (http_socket.isClosed()) {
markSocketAsDead(http_socket);
continue;
}
if (http_socket.isShutdown() or http_socket.getError() != 0) {
terminateSocket(http_socket);
continue;
}
assert(context().pending_sockets.put(socket));
log("+ Keep-Alive reuse {s}:{d}", .{ hostname, port });
return http_socket;
}
}
return null;
}
pub fn connectSocket(this: *@This(), client: *HTTPClient, socket_path: []const u8) !HTTPSocket {
client.connected_url = if (client.http_proxy) |proxy| proxy else client.url;
const socket = try HTTPSocket.connectUnixAnon(
socket_path,
this.us_socket_context,
ActiveSocket.init(client).ptr(),
false, // dont allow half-open sockets
);
client.allow_retry = false;
return socket;
}
pub fn connect(this: *@This(), client: *HTTPClient, hostname_: []const u8, port: u16) !HTTPSocket {
const hostname = if (FeatureFlags.hardcode_localhost_to_127_0_0_1 and strings.eqlComptime(hostname_, "localhost"))
"127.0.0.1"
else
hostname_;
client.connected_url = if (client.http_proxy) |proxy| proxy else client.url;
client.connected_url.hostname = hostname;
if (client.isKeepAlivePossible()) {
if (this.existingSocket(client.flags.reject_unauthorized, hostname, port)) |sock| {
if (sock.ext(**anyopaque)) |ctx| {
ctx.* = bun.cast(**anyopaque, ActiveSocket.init(client).ptr());
}
client.allow_retry = true;
try client.onOpen(comptime ssl, sock);
if (comptime ssl) {
client.firstCall(comptime ssl, sock);
}
return sock;
}
}
const socket = try HTTPSocket.connectAnon(
hostname,
port,
this.us_socket_context,
ActiveSocket.init(client).ptr(),
false,
);
client.allow_retry = false;
return socket;
}
};
}
const DeadSocket = struct {
garbage: u8 = 0,
pub var dead_socket: DeadSocket = .{};
};
var dead_socket = &DeadSocket.dead_socket;
const log = bun.Output.scoped(.HTTPContext, .hidden);
const HTTPCertError = @import("./HTTPCertError.zig");
const HTTPThread = @import("./HTTPThread.zig");
const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion;
const bun = @import("bun");
const Environment = bun.Environment;
const FeatureFlags = bun.FeatureFlags;
const assert = bun.assert;
const strings = bun.strings;
const uws = bun.uws;
const BoringSSL = bun.BoringSSL.c;
const HTTPClient = bun.http;
const InitError = HTTPClient.InitError;