Compare commits

...

8 Commits

Author SHA1 Message Date
Sosuke Suzuki
4c0c35529a Add tests for switch and try-catch 2025-09-04 00:04:54 +09:00
Sosuke Suzuki
f0d4838e9f JSC basic block ranges inclusively 2025-09-03 15:28:02 +09:00
Jarred Sumner
48ebc15e63 Implement RFC 6455 compliant WebSocket subprotocol handling (#22323)
## Summary

- Implements proper WebSocket subprotocol negotiation per RFC 6455 and
WHATWG standards
- Adds HeaderValueIterator utility for parsing comma-separated header
values
- Fixes WebSocket client to correctly validate server subprotocol
responses
- Sets WebSocket.protocol property to negotiated subprotocol per WHATWG
spec
- Includes comprehensive test coverage for all subprotocol scenarios

## Changes

**Core Implementation:**
- Add `HeaderValueIterator` utility for parsing comma-separated HTTP
header values
- Replace hash-based protocol matching with proper string set comparison
- Implement WHATWG compliant protocol property setting on successful
negotiation

**WebSocket Client (`WebSocketUpgradeClient.zig`):**
- Parse client subprotocols into StringSet using HeaderValueIterator
- Validate server response against requested protocols
- Set protocol property when server selects a matching subprotocol
- Allow connections when server omits Sec-WebSocket-Protocol header (per
spec)
- Reject connections when server sends unknown or empty subprotocol
values

**C++ Bindings:**
- Add `setProtocol` method to WebSocket class for updating protocol
property
- Export C binding for Zig integration

## Test Plan

Comprehensive test coverage for all subprotocol scenarios:
-  Server omits Sec-WebSocket-Protocol header (connection allowed,
protocol="")
-  Server sends empty Sec-WebSocket-Protocol header (connection
rejected)
-  Server selects valid subprotocol from multiple client options
(protocol set correctly)
-  Server responds with unknown subprotocol (connection rejected with
code 1002)
-  Validates CloseEvent objects don't trigger [Circular] console bugs

All tests use proper WebSocket handshake implementation and validate
both client and server behavior per RFC 6455 requirements.

## Issues Fixed

Fixes #10459 - WebSocket client does not retrieve the protocol sent by
the server
Fixes #10672 - `obs-websocket-js` is not compatible with Bun  
Fixes #17707 - Incompatibility with NodeJS when using obs-websocket-js
library
Fixes #19785 - Mismatch client protocol when connecting with multiple
Sec-WebSocket-Protocol

This enables obs-websocket-js and other libraries that rely on proper
RFC 6455 subprotocol negotiation to work correctly with Bun.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-02 03:47:25 -07:00
robobun
2e8e7a000c Fix WebSocket to emit error event before close on handshake failure (#22325)
## Summary
This PR fixes WebSocket to correctly emit an `error` event before the
`close` event when the handshake fails (e.g., 302 redirects, non-101
status codes, missing headers).

Fixes #14338

## Problem
Previously, when a WebSocket connection failed during handshake (like
receiving a 302 redirect or connecting to a non-WebSocket server), Bun
would only emit a `close` event. This behavior differed from the WHATWG
WebSocket specification and other runtimes (browsers, Node.js with `ws`,
Deno) which emit both `error` and `close` events.

## Solution
Modified `WebSocket::didFailWithErrorCode()` in `WebSocket.cpp` to pass
`isConnectionError = true` for all handshake failure error codes,
ensuring an error event is dispatched before the close event when the
connection is in the CONNECTING state.

## Changes
- Updated error handling in `src/bun.js/bindings/webcore/WebSocket.cpp`
to emit error events for handshake failures
- Added comprehensive test coverage in
`test/regression/issue/14338.test.ts`

## Test Coverage
The test file includes:
1. **Negative test**: 302 redirect response - verifies error event is
emitted
2. **Negative test**: Non-WebSocket HTTP server - verifies error event
is emitted
3. **Positive test**: Successful WebSocket connection - verifies NO
error event is emitted

All tests pass with the fix applied.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-02 03:26:51 -07:00
robobun
c1584b8a35 Fix spawnSync crash when stdio is set to process.stderr (#22329)
## Summary
- Fixes #20321 - spawnSync crashes with RangeError when stdio is set to
process.stderr
- Handles file descriptors in stdio array correctly by treating them as
non-captured output

## Problem
When `spawnSync` is called with `process.stderr` or `process.stdout` in
the stdio array, Bun.spawnSync returns the file descriptor number (e.g.,
2 for stderr) instead of a buffer or null. This causes a RangeError when
the code tries to call `toString(encoding)` on the number, since
`Number.prototype.toString()` expects a radix between 2 and 36, not an
encoding string.

This was blocking AWS CDK usage with Bun, as CDK internally uses
`spawnSync` with `stdio: ['ignore', process.stderr, 'inherit']`.

## Solution
Check if stdout/stderr from Bun.spawnSync are numbers (file descriptors)
and treat them as null (no captured output) instead of trying to convert
them to strings.

This aligns with Node.js's behavior where in
`lib/internal/child_process.js` (lines 1051-1055), when a stdio option
is a number or has an `fd` property, it's treated as a file descriptor:
```javascript
} else if (typeof stdio === 'number' || typeof stdio.fd === 'number') {
  ArrayPrototypePush(acc, {
    type: 'fd',
    fd: typeof stdio === 'number' ? stdio : stdio.fd,
  });
```

And when stdio is a stream object (like process.stderr), Node.js
extracts the fd from it (lines 1056-1067) and uses it as a file
descriptor, which means the output isn't captured in the result.

## Test plan
Added comprehensive regression tests in
`test/regression/issue/20321.test.ts` that cover:
- process.stderr as stdout
- process.stdout as stderr  
- All process streams in stdio array
- Mixed stdio options
- Direct file descriptor numbers
- The exact AWS CDK use case

All tests pass with the fix.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-02 03:26:25 -07:00
robobun
a0f13ea5bb Fix HTMLRewriter error handling (issue #19219) (#22326)
## Summary
- Fixed HTMLRewriter to throw proper errors instead of `[native code:
Exception]`
- The issue was incorrect error handling in the `transform_` function -
it wasn't properly checking for errors from `beginTransform()`
- Added proper error checking using `toError()` method on JSValue to
normalize Exception and Error instances

## Test plan
- Added regression test in `test/regression/issue/19219.test.ts`
- Test verifies that HTMLRewriter throws proper TypeError with
descriptive message when handlers throw
- All existing HTMLRewriter tests continue to pass

Fixes #19219

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-02 01:59:06 -07:00
robobun
c2bd4095eb Add vi export for Vitest compatibility in bun:test (#22304)
## Summary

- Add `vi` export to `bun:test` TypeScript definitions for **partial**
Vitest compatibility
- Provides Vitest-style mocking API aliases for existing Jest functions

## Changes

Added `vi` object export in `packages/bun-types/test.d.ts` with
TypeScript interface for the methods Bun actually supports.

**Note**: This is a **limited subset** of Vitest's full `vi` API. Bun
currently implements only these 5 methods:

 **Implemented in Bun:**
- `vi.fn()` - Create mock functions (alias for `jest.fn`)
- `vi.spyOn()` - Create spies (alias for `spyOn`)  
- `vi.module()` - Mock modules (alias for `mock.module`)
- `vi.restoreAllMocks()` - Restore all mocks (alias for
`jest.restoreAllMocks`)
- `vi.clearAllMocks()` - Clear mock state (alias for
`jest.clearAllMocks`)

 **NOT implemented** (full Vitest supports ~30+ methods):
- Timer mocking (`vi.useFakeTimers`, `vi.advanceTimersByTime`, etc.)
- Environment mocking (`vi.stubEnv`, `vi.stubGlobal`, etc.) 
- Advanced module mocking (`vi.doMock`, `vi.importActual`, etc.)
- Utility methods (`vi.waitFor`, `vi.hoisted`, etc.)

## Test plan

- [x] Verified `vi` can be imported: `import { vi } from "bun:test"`
- [x] Tested all 5 implemented `vi` methods work correctly
- [x] Confirmed TypeScript types work with generics and proper type
inference
- [x] Validated compatibility with basic Vitest usage patterns

## Migration Benefits

This enables easier migration for **simple** Vitest tests that only use
basic mocking:

```typescript
// Basic Vitest tests work in Bun now
import { vi } from 'bun:test'  // Previously would fail

const mockFn = vi.fn()          //  Works
const spy = vi.spyOn(obj, 'method')  //  Works
vi.clearAllMocks()              //  Works

// Advanced Vitest features still need porting to Jest-style APIs
// vi.useFakeTimers()           //  Not supported yet
// vi.stubEnv()                 //  Not supported yet
```

This is a first step toward Vitest compatibility - more advanced
features would need additional implementation in Bun core.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-02 01:54:12 -07:00
robobun
0a7313e66c Fix missing Jest mock functions in bun:test (#22306)
## Summary
- Fixes missing Jest API functions that were marked as implemented but
undefined
- Adds `jest.mock()` to the jest object (was missing despite being
marked as )
- Adds `jest.resetAllMocks()` to the jest object (implemented as alias
to clearAllMocks)
- Adds `vi.mock()` to the vi object for Vitest compatibility

## Test plan
- [x] Added regression test in
`test/regression/issue/issue-1825-jest-mock-functions.test.ts`
- [x] Verified `jest.mock("module", factory)` works correctly
- [x] Verified `jest.resetAllMocks()` doesn't throw and is available
- [x] Verified `mockReturnThis()` returns the mock function itself
- [x] All tests pass

## Related Issue
Fixes discrepancies found in #1825 where these functions were marked as
working but were actually undefined.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-02 01:53:39 -07:00
23 changed files with 958 additions and 55 deletions

View File

@@ -667,6 +667,7 @@ src/http/ETag.zig
src/http/FetchRedirect.zig
src/http/HeaderBuilder.zig
src/http/Headers.zig
src/http/HeaderValueIterator.zig
src/http/HTTPCertError.zig
src/http/HTTPContext.zig
src/http/HTTPRequestBody.zig

View File

@@ -152,11 +152,41 @@ declare module "bun:test" {
type SpiedSetter<T> = JestMock.SpiedSetter<T>;
}
/**
* Create a spy on an object property or method
*/
export function spyOn<T extends object, K extends keyof T>(
obj: T,
methodOrPropertyValue: K,
): Mock<Extract<T[K], (...args: any[]) => any>>;
/**
* Vitest-compatible mocking utilities
* Provides Vitest-style mocking API for easier migration from Vitest to Bun
*/
export const vi: {
/**
* Create a mock function
*/
fn: typeof jest.fn;
/**
* Create a spy on an object property or method
*/
spyOn: typeof spyOn;
/**
* Mock a module
*/
module: typeof mock.module;
/**
* Restore all mocks to their original implementation
*/
restoreAllMocks: typeof jest.restoreAllMocks;
/**
* Clear all mock state (calls, results, etc.) without restoring original implementation
*/
clearAllMocks: typeof jest.clearAllMocks;
};
interface FunctionLike {
readonly name: string;
}

View File

@@ -2050,6 +2050,12 @@ pub const Formatter = struct {
return error.JSError;
}
// If we call
// `return try this.printAs`
//
// Then we can get a spurious `[Circular]` due to the value already being present in the map.
var remove_before_recurse = false;
var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length };
defer {
if (writer.failed) {
@@ -2075,12 +2081,16 @@ pub const Formatter = struct {
if (entry.found_existing) {
writer.writeAll(comptime Output.prettyFmt("<r><cyan>[Circular]<r>", enable_ansi_colors));
return;
} else {
remove_before_recurse = true;
}
}
defer {
if (comptime Format.canHaveCircularReferences()) {
_ = this.map.remove(value);
if (remove_before_recurse) {
_ = this.map.remove(value);
}
}
}
@@ -2617,12 +2627,25 @@ pub const Formatter = struct {
} else if (try JestPrettyFormat.printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) {
return;
} else if (jsType != .DOMWrapper) {
if (remove_before_recurse) {
remove_before_recurse = false;
_ = this.map.remove(value);
}
if (value.isCallable()) {
remove_before_recurse = true;
return try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors);
}
remove_before_recurse = true;
return try this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors);
}
if (remove_before_recurse) {
remove_before_recurse = false;
_ = this.map.remove(value);
}
remove_before_recurse = true;
return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors);
},
.NativeCode => {
@@ -2887,6 +2910,12 @@ pub const Formatter = struct {
const event_type = switch (try EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) {
.MessageEvent, .ErrorEvent => |evt| evt,
else => {
if (remove_before_recurse) {
_ = this.map.remove(value);
}
// We must potentially remove it again.
remove_before_recurse = true;
return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors);
},
};

View File

@@ -178,7 +178,10 @@ pub const HTMLRewriter = struct {
return global.throwInvalidArguments("Response body already used", .{});
}
const out = try this.beginTransform(global, response);
if (out.toError()) |err| return global.throwValue(err);
// Check if the returned value is an error and throw it properly
if (out.toError()) |err| {
return global.throwValue(err);
}
return out;
}
@@ -203,6 +206,10 @@ pub const HTMLRewriter = struct {
});
defer resp.finalize();
const out_response_value = try this.beginTransform(global, resp);
// Check if the returned value is an error and throw it properly
if (out_response_value.toError()) |err| {
return global.throwValue(err);
}
out_response_value.ensureStillAlive();
var out_response = out_response_value.as(Response) orelse return out_response_value;
var blob = out_response.body.value.useAsAnyBlobAllowNonUTF8String();

View File

@@ -1352,39 +1352,39 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
break;
}
case Bun::WebSocketErrorCode::invalid_response: {
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid response"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid response"_s, true);
break;
}
case Bun::WebSocketErrorCode::expected_101_status_code: {
didReceiveClose(CleanStatus::NotClean, 1002, "Expected 101 status code"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Expected 101 status code"_s, true);
break;
}
case Bun::WebSocketErrorCode::missing_upgrade_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Missing upgrade header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Missing upgrade header"_s, true);
break;
}
case Bun::WebSocketErrorCode::missing_connection_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Missing connection header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Missing connection header"_s, true);
break;
}
case Bun::WebSocketErrorCode::missing_websocket_accept_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Missing websocket accept header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Missing websocket accept header"_s, true);
break;
}
case Bun::WebSocketErrorCode::invalid_upgrade_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid upgrade header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid upgrade header"_s, true);
break;
}
case Bun::WebSocketErrorCode::invalid_connection_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid connection header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid connection header"_s, true);
break;
}
case Bun::WebSocketErrorCode::invalid_websocket_version: {
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid websocket version"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid websocket version"_s, true);
break;
}
case Bun::WebSocketErrorCode::mismatch_websocket_accept_header: {
didReceiveClose(CleanStatus::NotClean, 1002, "Mismatch websocket accept header"_s);
didReceiveClose(CleanStatus::NotClean, 1002, "Mismatch websocket accept header"_s, true);
break;
}
case Bun::WebSocketErrorCode::missing_client_protocol: {
@@ -1412,11 +1412,11 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
break;
}
case Bun::WebSocketErrorCode::headers_too_large: {
didReceiveClose(CleanStatus::NotClean, 1007, "Headers too large"_s);
didReceiveClose(CleanStatus::NotClean, 1007, "Headers too large"_s, true);
break;
}
case Bun::WebSocketErrorCode::ended: {
didReceiveClose(CleanStatus::NotClean, 1006, "Connection ended"_s);
didReceiveClose(CleanStatus::NotClean, 1006, "Connection ended"_s, true);
break;
}
@@ -1457,7 +1457,7 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
break;
}
case Bun::WebSocketErrorCode::tls_handshake_failed: {
didReceiveClose(CleanStatus::NotClean, 1015, "TLS handshake failed"_s);
didReceiveClose(CleanStatus::NotClean, 1015, "TLS handshake failed"_s, true);
break;
}
case Bun::WebSocketErrorCode::message_too_big: {
@@ -1596,3 +1596,13 @@ WebCore::ExceptionOr<void> WebCore::WebSocket::pong(WebCore::JSBlob* blob)
return {};
}
void WebCore::WebSocket::setProtocol(const String& protocol)
{
m_subprotocol = protocol;
}
extern "C" void WebSocket__setProtocol(WebCore::WebSocket* webSocket, BunString* protocol)
{
webSocket->setProtocol(protocol->transferToWTFString());
}

View File

@@ -116,6 +116,8 @@ public:
ExceptionOr<void> close(std::optional<unsigned short> code, const String& reason);
ExceptionOr<void> terminate();
void setProtocol(const String& protocol);
const URL& url() const;
State readyState() const;
unsigned bufferedAmount() const;

View File

@@ -498,11 +498,13 @@ pub const Jest = struct {
mockFn.put(globalObject, ZigString.static("restore"), restoreAllMocks);
mockFn.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
const jest = JSValue.createEmptyObject(globalObject, 8);
const jest = JSValue.createEmptyObject(globalObject, 10);
jest.put(globalObject, ZigString.static("fn"), mockFn);
jest.put(globalObject, ZigString.static("mock"), mockModuleFn);
jest.put(globalObject, ZigString.static("spyOn"), spyOn);
jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks);
jest.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
jest.put(globalObject, ZigString.static("resetAllMocks"), clearAllMocks);
jest.put(
globalObject,
ZigString.static("setSystemTime"),
@@ -529,10 +531,10 @@ pub const Jest = struct {
Expect.js.getConstructor(globalObject),
);
const vi = JSValue.createEmptyObject(globalObject, 3);
const vi = JSValue.createEmptyObject(globalObject, 5);
vi.put(globalObject, ZigString.static("fn"), mockFn);
vi.put(globalObject, ZigString.static("mock"), mockModuleFn);
vi.put(globalObject, ZigString.static("spyOn"), spyOn);
vi.put(globalObject, ZigString.static("module"), mockModuleFn);
vi.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks);
vi.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
module.put(globalObject, ZigString.static("vi"), vi);

View File

@@ -1734,7 +1734,7 @@ pub const StringSet = struct {
pub const Map = StringArrayHashMap(void);
pub fn clone(self: StringSet) !StringSet {
pub fn clone(self: *const StringSet) !StringSet {
var new_map = Map.init(self.map.allocator);
try new_map.ensureTotalCapacity(self.map.count());
for (self.map.keys()) |key| {
@@ -1751,7 +1751,15 @@ pub const StringSet = struct {
};
}
pub fn keys(self: StringSet) []const []const u8 {
pub fn isEmpty(self: *const StringSet) bool {
return self.count() == 0;
}
pub fn count(self: *const StringSet) usize {
return self.map.count();
}
pub fn keys(self: *const StringSet) []const []const u8 {
return self.map.keys();
}
@@ -1770,6 +1778,13 @@ pub const StringSet = struct {
return self.map.swapRemove(key);
}
pub fn clearAndFree(self: *StringSet) void {
for (self.map.keys()) |key| {
self.map.allocator.free(key);
}
self.map.clearAndFree();
}
pub fn deinit(self: *StringSet) void {
for (self.map.keys()) |key| {
self.map.allocator.free(key);

View File

@@ -2449,6 +2449,7 @@ pub const FetchRedirect = @import("./http/FetchRedirect.zig").FetchRedirect;
pub const InitError = @import("./http/InitError.zig").InitError;
pub const HTTPRequestBody = @import("./http/HTTPRequestBody.zig").HTTPRequestBody;
pub const SendFile = @import("./http/SendFile.zig");
pub const HeaderValueIterator = @import("./http/HeaderValueIterator.zig");
const string = []const u8;

View File

@@ -0,0 +1,18 @@
const HeaderValueIterator = @This();
iterator: std.mem.TokenIterator(u8, .scalar),
pub fn init(input: []const u8) HeaderValueIterator {
return HeaderValueIterator{
.iterator = std.mem.tokenizeScalar(u8, std.mem.trim(u8, input, " \t"), ','),
};
}
pub fn next(self: *HeaderValueIterator) ?[]const u8 {
const slice = std.mem.trim(u8, self.iterator.next() orelse return null, " \t");
if (slice.len == 0) return self.next();
return slice;
}
const std = @import("std");

View File

@@ -58,6 +58,7 @@ pub const CppWebSocket = opaque {
}
extern fn WebSocket__incrementPendingActivity(websocket_context: *CppWebSocket) void;
extern fn WebSocket__decrementPendingActivity(websocket_context: *CppWebSocket) void;
extern fn WebSocket__setProtocol(websocket_context: *CppWebSocket, protocol: *bun.String) void;
pub fn ref(this: *CppWebSocket) void {
jsc.markBinding(@src());
WebSocket__incrementPendingActivity(this);
@@ -67,6 +68,10 @@ pub const CppWebSocket = opaque {
jsc.markBinding(@src());
WebSocket__decrementPendingActivity(this);
}
pub fn setProtocol(this: *CppWebSocket, protocol: *bun.String) void {
jsc.markBinding(@src());
WebSocket__setProtocol(this, protocol);
}
};
const WebSocketDeflate = @import("./WebSocketDeflate.zig");

View File

@@ -34,15 +34,14 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
tcp: Socket,
outgoing_websocket: ?*CppWebSocket,
input_body_buf: []u8 = &[_]u8{},
client_protocol: []const u8 = "",
to_send: []const u8 = "",
read_length: usize = 0,
headers_buf: [128]PicoHTTP.Header = undefined,
body: std.ArrayListUnmanaged(u8) = .{},
websocket_protocol: u64 = 0,
hostname: [:0]const u8 = "",
poll_ref: Async.KeepAlive = Async.KeepAlive.init(),
state: State = .initializing,
subprotocols: bun.StringSet,
const State = enum { initializing, reading, failed };
@@ -90,7 +89,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
bun.assert(vm.event_loop_handle != null);
var client_protocol_hash: u64 = 0;
const body = buildRequestBody(
vm,
pathname,
@@ -98,7 +96,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
host,
port,
client_protocol,
&client_protocol_hash,
NonUTF8Headers.init(header_names, header_values, header_count),
) catch return null;
@@ -107,8 +104,15 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
.tcp = .{ .socket = .{ .detached = {} } },
.outgoing_websocket = websocket,
.input_body_buf = body,
.websocket_protocol = client_protocol_hash,
.state = .initializing,
.subprotocols = brk: {
var subprotocols = bun.StringSet.init(bun.default_allocator);
var it = bun.http.HeaderValueIterator.init(client_protocol.slice());
while (it.next()) |protocol| {
subprotocols.insert(protocol) catch |e| bun.handleOom(e);
}
break :brk subprotocols;
},
});
var host_ = host.toSlice(bun.default_allocator);
@@ -162,6 +166,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
pub fn clearData(this: *HTTPClient) void {
this.poll_ref.unref(jsc.VirtualMachine.get());
this.subprotocols.clearAndFree();
this.clearInput();
this.body.clearAndFree(bun.default_allocator);
}
@@ -346,7 +351,8 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
var upgrade_header = PicoHTTP.Header{ .name = "", .value = "" };
var connection_header = PicoHTTP.Header{ .name = "", .value = "" };
var websocket_accept_header = PicoHTTP.Header{ .name = "", .value = "" };
var visited_protocol = this.websocket_protocol == 0;
var protocol_header_seen = false;
// var visited_version = false;
var deflate_result = DeflateNegotiationResult{};
@@ -382,11 +388,36 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
},
"Sec-WebSocket-Protocol".len => {
if (strings.eqlCaseInsensitiveASCII(header.name, "Sec-WebSocket-Protocol", false)) {
if (this.websocket_protocol == 0 or bun.hash(header.value) != this.websocket_protocol) {
const valid = brk: {
// Can't have multiple protocol headers in the response.
if (protocol_header_seen) break :brk false;
protocol_header_seen = true;
var iterator = bun.http.HeaderValueIterator.init(header.value);
const protocol = iterator.next()
// Can't be empty.
orelse break :brk false;
// Can't have multiple protocols.
if (iterator.next() != null) break :brk false;
// Protocol must be in the list of allowed protocols.
if (!this.subprotocols.contains(protocol)) break :brk false;
if (this.outgoing_websocket) |ws| {
var protocol_str = bun.String.init(protocol);
defer protocol_str.deref();
ws.setProtocol(&protocol_str);
}
break :brk true;
};
if (!valid) {
this.terminate(ErrorCode.mismatch_client_protocol);
return;
}
visited_protocol = true;
}
},
"Sec-WebSocket-Extensions".len => {
@@ -469,11 +500,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
return;
}
if (!visited_protocol) {
this.terminate(ErrorCode.mismatch_client_protocol);
return;
}
if (!strings.eqlCaseInsensitiveASCII(connection_header.value, "Upgrade", true)) {
this.terminate(ErrorCode.invalid_connection_header);
return;
@@ -622,7 +648,6 @@ fn buildRequestBody(
host: *const jsc.ZigString,
port: u16,
client_protocol: *const jsc.ZigString,
client_protocol_hash: *u64,
extra_headers: NonUTF8Headers,
) std.mem.Allocator.Error![]u8 {
const allocator = vm.allocator;
@@ -642,9 +667,6 @@ fn buildRequestBody(
},
};
if (client_protocol.len > 0)
client_protocol_hash.* = bun.hash(static_headers[1].value);
const pathname_ = pathname.toSlice(allocator);
const host_ = host.toSlice(allocator);
defer {

View File

@@ -550,11 +550,16 @@ function spawnSync(file, args, options) {
stderr = null;
}
// When stdio is redirected to a file descriptor, Bun.spawnSync returns the fd number
// instead of the actual output. We should treat this as no output available.
const outputStdout = typeof stdout === "number" ? null : stdout;
const outputStderr = typeof stderr === "number" ? null : stderr;
const result = {
signal: signalCode ?? null,
status: exitCode,
// TODO: Need to expose extra pipes from Bun.spawnSync to child_process
output: [null, stdout, stderr],
output: [null, outputStdout, outputStderr],
pid,
};
@@ -562,11 +567,11 @@ function spawnSync(file, args, options) {
result.error = error;
}
if (stdout && encoding && encoding !== "buffer") {
if (outputStdout && encoding && encoding !== "buffer") {
result.output[1] = result.output[1]?.toString(encoding);
}
if (stderr && encoding && encoding !== "buffer") {
if (outputStderr && encoding && encoding !== "buffer") {
result.output[2] = result.output[2]?.toString(encoding);
}

View File

@@ -457,7 +457,7 @@ pub const ByteRangeMapping = struct {
const has_executed = block.hasExecuted or block.executionCount > 0;
for (min..max) |byte_offset| {
for (min..(max + 1)) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
@@ -494,7 +494,7 @@ pub const ByteRangeMapping = struct {
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
for (min..max) |byte_offset| {
for (min..(max + 1)) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
@@ -546,7 +546,7 @@ pub const ByteRangeMapping = struct {
var max_line: u32 = 0;
const has_executed = block.hasExecuted or block.executionCount > 0;
for (min..max) |byte_offset| {
for (min..(max + 1)) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {
@@ -589,7 +589,7 @@ pub const ByteRangeMapping = struct {
var min_line: u32 = std.math.maxInt(u32);
var max_line: u32 = 0;
for (min..max) |byte_offset| {
for (min..(max + 1)) |byte_offset| {
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
const line_start_byte_offset = line_starts[new_line_index];
if (line_start_byte_offset >= byte_offset) {

View File

@@ -589,3 +589,99 @@ Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});
test("coverage accuracy - switch statement", () => {
const dir = tempDirWithFiles("cov", {
"switch-func.ts": `
export function func(value: string): string {
switch (value) {
case "A":
return "Alpha";
case "B":
return "Beta";
case "C":
return "Charlie";
default:
return "Unknown";
}
}
`,
"switch-test.test.ts": `
import { test, expect } from "bun:test";
import { func } from "./switch-func";
test("switch test - case A only", () => {
expect(func("A")).toBe("Alpha");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
// Lines 6-9 (case "B", "C", and default) should be uncovered
expect(stderr).toContain("55.56");
expect(stderr).toContain("6-9");
expect(result.exitCode).toBe(0);
});
test("coverage accuracy - try-catch statement", () => {
const dir = tempDirWithFiles("cov", {
"try-catch-func.ts": `
export function func(value: number): string | number {
try {
if (value > 100) {
throw new Error("Too large");
}
if (value < 0) {
throw new Error("Negative");
}
return value * 2;
} catch (e) {
if (e instanceof Error) {
if (e.message === "Too large") {
return "error: large";
} else if (e.message === "Negative") {
return "error: negative";
}
}
return "unknown error";
}
}
`,
"try-catch-test.test.ts": `
import { test, expect } from "bun:test";
import { func } from "./try-catch-func";
test("try-catch test - normal value", () => {
expect(func(50)).toBe(100);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
// Lines 5,8,11-18 (error conditions and catch block) should be uncovered
expect(stderr).toContain("44.44");
expect(stderr).toContain("5,8,11-18");
expect(result.exitCode).toBe(0);
});

View File

@@ -1231,15 +1231,25 @@ describe("websocket and routes test", () => {
resolve(event.data);
ws.close();
};
ws.onerror = reject;
let errorFired = false;
ws.onerror = e => {
errorFired = true;
// Don't reject on error, we expect both error and close for failed upgrade
};
ws.onclose = event => {
reject(event.code);
if (!shouldBeUpgraded) {
// For failed upgrade, resolve with the close code
resolve(event.code);
} else {
reject(event.code);
}
};
if (shouldBeUpgraded) {
const result = await promise;
expect(result).toBe("recv: Hello server");
} else {
const result = await promise.catch(e => e);
const result = await promise;
expect(errorFired).toBe(true); // Error event should fire for failed upgrade
expect(result).toBe(1002);
}
if (hasPOST) {

View File

@@ -1,8 +1,7 @@
import { describe, expect, it } from "bun:test";
import { tmpdirSync } from "harness";
import { normalizeBunSnapshot, tmpdirSync } from "harness";
import { join } from "path";
import util from "util";
it("prototype", () => {
const prototypes = [
Request.prototype,
@@ -607,3 +606,120 @@ it("Symbol", () => {
expect(Bun.inspect(Symbol())).toBe("Symbol()");
expect(Bun.inspect(Symbol(""))).toBe("Symbol()");
});
it("CloseEvent", () => {
const closeEvent = new CloseEvent("close", {
code: 1000,
reason: "Normal",
});
expect(Bun.inspect(closeEvent)).toMatchInlineSnapshot(`
"CloseEvent {
isTrusted: false,
wasClean: false,
code: 1000,
reason: "Normal",
type: "close",
target: null,
currentTarget: null,
eventPhase: 0,
cancelBubble: false,
bubbles: false,
cancelable: false,
defaultPrevented: false,
composed: false,
timeStamp: 0,
srcElement: null,
returnValue: true,
composedPath: [Function: composedPath],
stopPropagation: [Function: stopPropagation],
stopImmediatePropagation: [Function: stopImmediatePropagation],
preventDefault: [Function: preventDefault],
initEvent: [Function: initEvent],
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3,
}"
`);
});
it("ErrorEvent", () => {
const errorEvent = new ErrorEvent("error", {
message: "Something went wrong",
filename: "script.js",
lineno: 42,
colno: 10,
error: new Error("Test error"),
});
expect(normalizeBunSnapshot(Bun.inspect(errorEvent)).replace(/\d+ \| /gim, "NNN |")).toMatchInlineSnapshot(`
"ErrorEvent {
type: "error",
message: "Something went wrong",
error: NNN | const errorEvent = new ErrorEvent("error", {
NNN | message: "Something went wrong",
NNN | filename: "script.js",
NNN | lineno: 42,
NNN | colno: 10,
NNN | error: new Error("Test error"),
^
error: Test error
at <anonymous> (file:NN:NN)
,
}"
`);
});
it("MessageEvent", () => {
const messageEvent = new MessageEvent("message", {
data: "Hello, world!",
origin: "https://example.com",
lastEventId: "123",
source: null,
ports: [],
});
expect(Bun.inspect(messageEvent)).toMatchInlineSnapshot(`
"MessageEvent {
type: "message",
data: "Hello, world!",
}"
`);
});
it("CustomEvent", () => {
const customEvent = new CustomEvent("custom", {
detail: { value: 42, name: "test" },
bubbles: true,
cancelable: true,
});
expect(Bun.inspect(customEvent)).toMatchInlineSnapshot(`
"CustomEvent {
isTrusted: false,
detail: {
value: 42,
name: "test",
},
initCustomEvent: [Function: initCustomEvent],
type: "custom",
target: null,
currentTarget: null,
eventPhase: 0,
cancelBubble: false,
bubbles: true,
cancelable: true,
defaultPrevented: false,
composed: false,
timeStamp: 0,
srcElement: null,
returnValue: true,
composedPath: [Function: composedPath],
stopPropagation: [Function: stopPropagation],
stopImmediatePropagation: [Function: stopImmediatePropagation],
preventDefault: [Function: preventDefault],
initEvent: [Function: initEvent],
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3,
}"
`);
});

View File

@@ -0,0 +1,191 @@
import { describe, expect, it, mock } from "bun:test";
import crypto from "node:crypto";
import net from "node:net";
describe("WebSocket strict RFC 6455 subprotocol handling", () => {
async function createTestServer(
responseHeaders: string[],
): Promise<{ port: number; [Symbol.asyncDispose]: () => Promise<void> }> {
const server = net.createServer();
let port: number;
await new Promise<void>(resolve => {
server.listen(0, () => {
port = (server.address() as any).port;
resolve();
});
});
server.on("connection", socket => {
let requestData = "";
socket.on("data", data => {
requestData += data.toString();
if (requestData.includes("\r\n\r\n")) {
const lines = requestData.split("\r\n");
let websocketKey = "";
for (const line of lines) {
if (line.startsWith("Sec-WebSocket-Key:")) {
websocketKey = line.split(":")[1].trim();
break;
}
}
const acceptKey = crypto
.createHash("sha1")
.update(websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.digest("base64");
const response = [
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${acceptKey}`,
...responseHeaders,
"\r\n",
].join("\r\n");
socket.write(response);
}
});
});
return {
port: port!,
[Symbol.asyncDispose]: async () => {
server.close();
},
};
}
async function expectConnectionFailure(port: number, protocols: string[], expectedCode = 1002) {
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers();
const ws = new WebSocket(`ws://localhost:${port}`, protocols);
const onopenMock = mock(() => {});
ws.onopen = onopenMock;
ws.onclose = close => {
expect(close.code).toBe(expectedCode);
expect(close.reason).toBe("Mismatch client protocol");
resolveClose();
};
await closePromise;
expect(onopenMock).not.toHaveBeenCalled();
}
async function expectConnectionSuccess(port: number, protocols: string[], expectedProtocol: string) {
const { promise: openPromise, resolve: resolveOpen, reject } = Promise.withResolvers();
const ws = new WebSocket(`ws://localhost:${port}`, protocols);
try {
ws.onopen = () => resolveOpen();
ws.onerror = reject;
await openPromise;
expect(ws.protocol).toBe(expectedProtocol);
} finally {
ws.terminate();
}
}
// Multiple protocols in single header (comma-separated) - should fail
it("should reject multiple comma-separated protocols", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat, echo"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject multiple comma-separated protocols with spaces", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat , echo , binary"]);
await expectConnectionFailure(server.port, ["chat", "echo", "binary"]);
});
it("should reject multiple comma-separated protocols (3 protocols)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: a,b,c"]);
await expectConnectionFailure(server.port, ["a", "b", "c"]);
});
// Multiple headers - should fail
it("should reject duplicate Sec-WebSocket-Protocol headers (same value)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat", "Sec-WebSocket-Protocol: chat"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject duplicate Sec-WebSocket-Protocol headers (different values)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat", "Sec-WebSocket-Protocol: echo"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject three Sec-WebSocket-Protocol headers", async () => {
await using server = await createTestServer([
"Sec-WebSocket-Protocol: a",
"Sec-WebSocket-Protocol: b",
"Sec-WebSocket-Protocol: c",
]);
await expectConnectionFailure(server.port, ["a", "b", "c"]);
});
// Empty values - should fail
it("should reject empty Sec-WebSocket-Protocol header", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: "]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject Sec-WebSocket-Protocol with only comma", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: ,"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject Sec-WebSocket-Protocol with only spaces", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: "]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
// Unknown protocols - should fail
it("should reject unknown single protocol", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: unknown"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
it("should reject unknown protocol (not in client list)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: binary"]);
await expectConnectionFailure(server.port, ["chat", "echo"]);
});
// Valid cases - should succeed
it("should accept single valid protocol (first in client list)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat"]);
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "chat");
});
it("should accept single valid protocol (middle in client list)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: echo"]);
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "echo");
});
it("should accept single valid protocol (last in client list)", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: binary"]);
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "binary");
});
it("should accept single protocol with extra whitespace", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: echo "]);
await expectConnectionSuccess(server.port, ["chat", "echo"], "echo");
});
it("should accept single protocol with single character", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: a"]);
await expectConnectionSuccess(server.port, ["a", "b"], "a");
});
// Edge cases with special characters
it("should handle protocol with special characters", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat-v2.0"]);
await expectConnectionSuccess(server.port, ["chat-v1.0", "chat-v2.0"], "chat-v2.0");
});
it("should handle protocol with dots", async () => {
await using server = await createTestServer(["Sec-WebSocket-Protocol: com.example.chat"]);
await expectConnectionSuccess(server.port, ["com.example.chat", "other"], "com.example.chat");
});
});

View File

@@ -131,15 +131,19 @@ describe("WebSocket", () => {
function testClient(client) {
const { promise, resolve, reject } = Promise.withResolvers();
let messages = [];
let errorFired = false;
client.onopen = () => {
client.send("Hello from client!");
};
client.onmessage = e => {
messages.push(e.data);
};
client.onerror = reject;
client.onerror = e => {
errorFired = true;
// Don't reject, we expect both error and close events
};
client.onclose = e => {
resolve({ result: e, messages });
resolve({ result: e, messages, errorFired });
};
return promise;
}
@@ -147,7 +151,8 @@ describe("WebSocket", () => {
{
// by default rejectUnauthorized is true
const client = new WebSocket(url);
const { result, messages } = await testClient(client);
const { result, messages, errorFired } = await testClient(client);
expect(errorFired).toBe(true); // Error event should fire
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
expect(result.code).toBe(1015);
expect(result.reason).toBe("TLS handshake failed");
@@ -156,7 +161,8 @@ describe("WebSocket", () => {
{
// just in case we change the default to true and test
const client = new WebSocket(url, { tls: { rejectUnauthorized: true } });
const { result, messages } = await testClient(client);
const { result, messages, errorFired } = await testClient(client);
expect(errorFired).toBe(true); // Error event should fire
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
expect(result.code).toBe(1015);
expect(result.reason).toBe("TLS handshake failed");
@@ -248,22 +254,27 @@ describe("WebSocket", () => {
function testClient(client) {
const { promise, resolve, reject } = Promise.withResolvers();
let messages = [];
let errorFired = false;
client.onopen = () => {
client.send("Hello from client!");
};
client.onmessage = e => {
messages.push(e.data);
};
client.onerror = reject;
client.onerror = e => {
errorFired = true;
// Don't reject, we expect both error and close events
};
client.onclose = e => {
resolve({ result: e, messages });
resolve({ result: e, messages, errorFired });
};
return promise;
}
const url = `wss://localhost:${server.address.port}`;
{
const client = new WebSocket(url);
const { result, messages } = await testClient(client);
const { result, messages, errorFired } = await testClient(client);
expect(errorFired).toBe(true); // Error event should fire
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
expect(result.code).toBe(1015);
expect(result.reason).toBe("TLS handshake failed");

View File

@@ -0,0 +1,150 @@
import { expect, test } from "bun:test";
test("WebSocket should emit error event before close event on handshake failure (issue #14338)", async () => {
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Event>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
const events: string[] = [];
// Create a server that returns a 302 redirect response instead of a WebSocket upgrade
await using server = Bun.serve({
port: 0,
fetch(req) {
// Return a 302 redirect response to simulate handshake failure
return new Response(null, {
status: 302,
headers: {
Location: "http://example.com",
},
});
},
});
const ws = new WebSocket(`ws://localhost:${server.port}`);
ws.addEventListener("error", event => {
events.push("error");
resolveError(event);
});
ws.addEventListener("close", event => {
events.push("close");
resolveClose(event);
});
ws.addEventListener("open", () => {
events.push("open");
});
// Wait for close event (which should always fire)
await closePromise;
// After the fix, both error and close events should be emitted
// The error event should come before the close event
expect(events).toEqual(["error", "close"]);
});
test("WebSocket successful connection should NOT emit error event", async () => {
const { promise: openPromise, resolve: resolveOpen } = Promise.withResolvers<Event>();
const { promise: messagePromise, resolve: resolveMessage } = Promise.withResolvers<MessageEvent>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
const events: string[] = [];
// Create a proper WebSocket server
await using server = Bun.serve({
port: 0,
websocket: {
message(ws, message) {
ws.send(message);
},
},
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Not found", { status: 404 });
},
});
const ws = new WebSocket(`ws://localhost:${server.port}`);
ws.addEventListener("error", event => {
events.push("error");
});
ws.addEventListener("open", event => {
events.push("open");
resolveOpen(event);
});
ws.addEventListener("message", event => {
events.push("message");
resolveMessage(event);
});
ws.addEventListener("close", event => {
events.push("close");
resolveClose(event);
});
// Wait for connection to open
await openPromise;
// Send a test message
ws.send("test");
// Wait for echo
const msg = await messagePromise;
expect(msg.data).toBe("test");
// Close the connection normally
ws.close();
// Wait for close event
await closePromise;
// Should have open, message, and close events, but NO error event
expect(events).toContain("open");
expect(events).toContain("message");
expect(events).toContain("close");
expect(events).not.toContain("error");
});
test("WebSocket should emit error and close events on connection to non-WebSocket server", async () => {
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
const events: string[] = [];
// Create a regular HTTP server (not WebSocket)
await using server = Bun.serve({
port: 0,
fetch(req) {
// Return a normal HTTP response
return new Response("Not a WebSocket server", {
status: 200,
headers: {
"Content-Type": "text/plain",
},
});
},
});
const ws = new WebSocket(`ws://localhost:${server.port}`);
ws.addEventListener("error", event => {
events.push("error");
});
ws.addEventListener("close", event => {
events.push("close");
resolveClose(event);
});
ws.addEventListener("open", () => {
events.push("open");
});
// Wait for close event
await closePromise;
// After the fix, both error and close events should be emitted
expect(events).toEqual(["error", "close"]);
});

View File

@@ -0,0 +1,58 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/19219
test("HTMLRewriter should throw proper errors instead of [native code: Exception]", () => {
const rewriter = new HTMLRewriter().on("p", {
element(element) {
// This will cause an error by trying to call a non-existent method
(element as any).nonExistentMethod();
},
});
const html = "<html><body><p>Hello</p></body></html>";
// Should throw a proper TypeError, not [native code: Exception]
expect(() => {
rewriter.transform(html);
}).toThrow(TypeError);
// Verify the error message is descriptive
try {
rewriter.transform(html);
} catch (error: any) {
expect(error).toBeInstanceOf(TypeError);
expect(error.message).toContain("nonExistentMethod");
expect(error.message).toContain("is not a function");
// Make sure it's not the generic [native code: Exception] message
expect(error.toString()).not.toContain("[native code: Exception]");
}
});
test("HTMLRewriter should propagate errors from handlers correctly", () => {
const rewriter = new HTMLRewriter().on("div", {
element() {
throw new Error("Custom error from handler");
},
});
const html = "<div>test</div>";
expect(() => {
rewriter.transform(html);
}).toThrow("Custom error from handler");
});
test("HTMLRewriter should handle errors in async handlers", async () => {
const rewriter = new HTMLRewriter().on("div", {
async element() {
throw new Error("Async handler error");
},
});
const html = "<div>test</div>";
const response = new Response(html);
expect(() => {
rewriter.transform(response);
}).toThrow("Async handler error");
});

View File

@@ -0,0 +1,95 @@
import { expect, test } from "bun:test";
import { spawnSync } from "child_process";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("spawnSync should not crash when stdout is set to process.stderr (issue #20321)", () => {
// Test with process.stderr as stdout
const proc1 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
encoding: "utf-8",
stdio: ["ignore", process.stderr, "inherit"],
env: bunEnv,
});
expect(proc1.error).toBeUndefined();
expect(proc1.status).toBe(0);
// When redirecting to a file descriptor, we don't capture the output
expect(proc1.stdout).toBeNull();
});
test("spawnSync should not crash when stderr is set to process.stdout", () => {
// Test with process.stdout as stderr
const proc2 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
encoding: "utf-8",
stdio: ["ignore", "pipe", process.stdout],
env: bunEnv,
});
expect(proc2.error).toBeUndefined();
expect(proc2.status).toBe(0);
expect(proc2.stdout).toBe("hello\n");
// When redirecting to a file descriptor, we don't capture the output
expect(proc2.stderr).toBeNull();
});
test("spawnSync should handle process.stdin/stdout/stderr in stdio array", () => {
// Test with all process streams
const proc3 = spawnSync(bunExe(), ["-e", 'console.log("test")'], {
encoding: "utf-8",
stdio: [process.stdin, process.stdout, process.stderr],
env: bunEnv,
});
expect(proc3.error).toBeUndefined();
expect(proc3.status).toBe(0);
// When redirecting to file descriptors, we don't capture the output
expect(proc3.stdout).toBeNull();
expect(proc3.stderr).toBeNull();
});
test("spawnSync with mixed stdio options including process streams", () => {
// Mix of different stdio options
const proc4 = spawnSync(bunExe(), ["-e", 'console.log("mixed")'], {
encoding: "utf-8",
stdio: ["pipe", process.stderr, "pipe"],
env: bunEnv,
});
expect(proc4.error).toBeUndefined();
expect(proc4.status).toBe(0);
// stdout redirected to stderr fd, so no capture
expect(proc4.stdout).toBeNull();
// stderr is piped, should be empty for echo
expect(proc4.stderr).toBe("");
});
test("spawnSync should work with file descriptors directly", () => {
// Test with raw file descriptors (same as what process.stderr resolves to)
const proc5 = spawnSync(bunExe(), ["-e", 'console.log("fd-test")'], {
encoding: "utf-8",
stdio: ["ignore", 2, "inherit"], // 2 is stderr fd
env: bunEnv,
});
expect(proc5.error).toBeUndefined();
expect(proc5.status).toBe(0);
expect(proc5.stdout).toBeNull();
});
test("spawnSync should handle the AWS CDK use case", () => {
// This is the exact use case from AWS CDK that was failing
const dir = tempDirWithFiles("spawnsync-cdk", {
"test.js": `console.log("CDK output");`,
});
const proc = spawnSync(bunExe(), ["test.js"], {
encoding: "utf-8",
stdio: ["ignore", process.stderr, "inherit"],
cwd: dir,
env: bunEnv,
});
expect(proc.error).toBeUndefined();
expect(proc.status).toBe(0);
// Output goes to stderr, not captured
expect(proc.stdout).toBeNull();
});

View File

@@ -0,0 +1,29 @@
import { describe, expect, jest, test } from "bun:test";
describe("Jest mock functions from issue #1825", () => {
test("jest.mock should be available and work with factory function", () => {
// Should not throw - jest.mock should be available
expect(() => {
jest.mock("fs", () => ({ readFile: jest.fn() }));
}).not.toThrow();
});
test("jest.resetAllMocks should be available and not throw", () => {
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalledTimes(1);
// Should not throw - jest.resetAllMocks should be available
expect(() => {
jest.resetAllMocks();
}).not.toThrow();
});
test("mockReturnThis should return the mock function itself", () => {
const mockFn = jest.fn();
const result = mockFn.mockReturnThis();
// mockReturnThis should return the mock function itself
expect(result).toBe(mockFn);
});
});