Compare commits

..

7 Commits

Author SHA1 Message Date
Claude Bot
a9b7188379 refactor(test): use tls from harness instead of inlining certs
Replace duplicated inline TLS certificates with the shared `tls`
export from harness to improve test maintainability.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 00:44:07 +00:00
autofix-ci[bot]
4083d6fa27 [autofix.ci] apply automated fixes 2025-11-27 09:05:54 +00:00
Claude Bot
0c5443ae0f fix(http2): return server from setTimeout for method chaining (#24924)
Make Http2Server.setTimeout() and Http2SecureServer.setTimeout() return
`this` to enable method chaining, matching Node.js behavior.

Before: server.setTimeout(1000).listen() // TypeError: undefined
After:  server.setTimeout(1000).listen() // works

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 09:03:51 +00:00
robobun
908ab9ce30 feat(fetch): add proxy object format with headers support (#25090)
## Summary

- Extends `fetch()` proxy option to accept an object format: `proxy: {
url: string, headers?: Headers }`
- Allows sending custom headers to the proxy server (useful for proxy
authentication, custom routing headers, etc.)
- Headers are sent in CONNECT requests (for HTTPS targets) and direct
proxy requests (for HTTP targets)
- User-provided `Proxy-Authorization` header overrides auto-generated
credentials from URL

## Usage

```typescript
// Old format (still works)
fetch(url, { proxy: "http://proxy.example.com:8080" });

// New object format with headers
fetch(url, {
  proxy: {
    url: "http://proxy.example.com:8080",
    headers: {
      "Proxy-Authorization": "Bearer token",
      "X-Custom-Proxy-Header": "value"
    }
  }
});
```

## Test plan

- [x] Test proxy object with url string works same as string proxy
- [x] Test proxy object with headers sends headers to proxy (HTTP
target)
- [x] Test proxy object with headers sends headers in CONNECT request
(HTTPS target)
- [x] Test proxy object with Headers instance
- [x] Test proxy object with empty headers
- [x] Test proxy object with undefined headers
- [x] Test user-provided Proxy-Authorization overrides URL credentials
- [x] All existing proxy tests pass (25 total)

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-26 15:11:45 -08:00
robobun
43c46b1f77 fix(FormData): throw error instead of assertion failure on very large input (#25006)
## Summary

- Fix crash in `FormData.from()` when called with very large ArrayBuffer
input
- Add length check in C++ `toString` function against both Bun's
synthetic limit and WebKit's `String::MaxLength`
- For UTF-8 tagged strings, use simdutf to calculate actual UTF-16
length only when byte length exceeds the limit

## Root Cause

When `FormData.from()` was called with a very large ArrayBuffer (e.g.,
`new Uint32Array(913148244)` = ~3.6GB), the code would crash with:

```
ASSERTION FAILED: data.size() <= MaxLength
vendor/WebKit/Source/WTF/wtf/text/StringImpl.h(886)
```

The `toString()` function in `helpers.h` was only checking against
`Bun__stringSyntheticAllocationLimit` (which defaults to ~4GB), but not
against WebKit's `String::MaxLength` (INT32_MAX, ~2GB). When the input
exceeded `String::MaxLength`, `createWithoutCopying()` would fail with
an assertion.

## Changes

1. **helpers.h**: Added `|| str.len > WTF::String::MaxLength` checks to
all three code paths in `toString()`:
- UTF-8 tagged pointer path (with simdutf length calculation only when
needed)
   - External pointer path
   - Non-copying creation path

2. **url.zig**: Reverted the incorrect Zig-side check (UTF-8 byte length
!= UTF-16 character length)

## Test plan

- [x] Added test that verifies FormData.from with oversized input
doesn't crash
- [x] Verified original crash case now returns empty FormData instead of
crashing:
  ```js
  const v3 = new Uint32Array(913148244);
  FormData.from(v3); // No longer crashes
  ```

🤖 Generated with [Claude Code](https://claude.com/claude-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>
2025-11-26 13:46:08 -08:00
robobun
a0c5f3dc69 fix(mmap): use coerceToInt64 for offset/size to prevent assertion failure (#25101)
## Summary

- Fix assertion failure in `Bun.mmap` when `offset` or `size` options
are non-numeric values
- Add validation to reject negative `offset`/`size` with clear error
messages

Minimal reproduction: `Bun.mmap("", { offset: null });`

## Root Cause

`Bun.mmap` was calling `toInt64()` directly on the `offset` and `size`
options without validating they are numbers first. `toInt64()` has an
assertion that the value must be a number or BigInt, which fails when
non-numeric values like `null` or functions are passed.

## Test plan

- [x] Added tests for negative offset/size rejection
- [x] Added tests for non-number inputs (null, undefined)
- [x] `bun bd test test/js/bun/util/mmap.test.js` passes

Closes ENG-22413

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-26 13:37:41 -08:00
robobun
5965ff18ea fix(test): fix assertion failure in expect.extend with non-JSFunction callables (#25099)
## Summary

- Fix debug assertion failure in `JSWrappingFunction` when
`expect.extend()` is called with objects containing non-`JSFunction`
callables
- The crash occurred because `jsCast<JSFunction*>` was used, which
asserts the value inherits from `JSFunction`, but callable class
constructors (like `Expect`) inherit from `InternalFunction` instead

## Changes

- Change `JSWrappingFunction` to store `JSObject*` instead of
`JSFunction*`
- Use `jsDynamicCast` instead of `jsCast` in `getWrappedFunction`
- Use `getObject()` instead of `jsCast` in `create()`

## Reproduction

```js
const jest = Bun.jest();
jest.expect.extend(jest);
```

Before fix (debug build):
```
ASSERTION FAILED: !from || from->JSCell::inherits(std::remove_pointer<To>::type::info())
JSCast.h(40) : To JSC::jsCast(From *) [To = JSC::JSFunction *, From = JSC::JSCell]
```

After fix: Properly throws `TypeError: expect.extend: 'jest' is not a
valid matcher`

## Test plan

- [x] Added regression test
`test/regression/issue/fuzzer-ENG-22942.test.ts`
- [x] Existing `expect-extend.test.js` tests pass (27 tests)
- [x] Build succeeds

Fixes ENG-22942

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-26 13:34:02 -08:00
22 changed files with 866 additions and 103 deletions

View File

@@ -9,18 +9,42 @@ In Bun, `fetch` supports sending requests through an HTTP or HTTPS proxy. This i
```ts proxy.ts icon="/icons/typescript.svg"
await fetch("https://example.com", {
// The URL of the proxy server
proxy: "https://usertitle:password@proxy.example.com:8080",
proxy: "https://username:password@proxy.example.com:8080",
});
```
---
The `proxy` option is a URL string that specifies the proxy server. It can include the username and password if the proxy requires authentication. It can be `http://` or `https://`.
The `proxy` option can be a URL string or an object with `url` and optional `headers`. The URL can include the username and password if the proxy requires authentication. It can be `http://` or `https://`.
---
## Custom proxy headers
To send custom headers to the proxy server (useful for proxy authentication tokens, custom routing, etc.), use the object format:
```ts proxy-headers.ts icon="/icons/typescript.svg"
await fetch("https://example.com", {
proxy: {
url: "https://proxy.example.com:8080",
headers: {
"Proxy-Authorization": "Bearer my-token",
"X-Proxy-Region": "us-east-1",
},
},
});
```
The `headers` property accepts a plain object or a `Headers` instance. These headers are sent directly to the proxy server in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets).
If you provide a `Proxy-Authorization` header, it will override any credentials specified in the proxy URL.
---
## Environment variables
You can also set the `$HTTP_PROXY` or `$HTTPS_PROXY` environment variable to the proxy URL. This is useful when you want to use the same proxy for all requests.
```sh terminal icon="terminal"
HTTPS_PROXY=https://usertitle:password@proxy.example.com:8080 bun run index.ts
HTTPS_PROXY=https://username:password@proxy.example.com:8080 bun run index.ts
```

View File

@@ -51,7 +51,7 @@ const response = await fetch("http://example.com", {
### Proxying requests
To proxy a request, pass an object with the `proxy` property set to a URL.
To proxy a request, pass an object with the `proxy` property set to a URL string:
```ts
const response = await fetch("http://example.com", {
@@ -59,6 +59,22 @@ const response = await fetch("http://example.com", {
});
```
You can also use an object format to send custom headers to the proxy server:
```ts
const response = await fetch("http://example.com", {
proxy: {
url: "http://proxy.com",
headers: {
"Proxy-Authorization": "Bearer my-token",
"X-Custom-Proxy-Header": "value",
},
},
});
```
The `headers` are sent directly to the proxy in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets). If you provide a `Proxy-Authorization` header, it overrides any credentials in the proxy URL.
### Custom headers
To set custom headers, pass an object with the `headers` property set to an object.

View File

@@ -1920,14 +1920,44 @@ interface BunFetchRequestInit extends RequestInit {
* Override http_proxy or HTTPS_PROXY
* This is a custom property that is not part of the Fetch API specification.
*
* Can be a string URL or an object with `url` and optional `headers`.
*
* @example
* ```js
* // String format
* const response = await fetch("http://example.com", {
* proxy: "https://username:password@127.0.0.1:8080"
* });
*
* // Object format with custom headers sent to the proxy
* const response = await fetch("http://example.com", {
* proxy: {
* url: "https://127.0.0.1:8080",
* headers: {
* "Proxy-Authorization": "Bearer token",
* "X-Custom-Proxy-Header": "value"
* }
* }
* });
* ```
*
* If a `Proxy-Authorization` header is provided in `proxy.headers`, it takes
* precedence over credentials parsed from the proxy URL.
*/
proxy?: string;
proxy?:
| string
| {
/**
* The proxy URL
*/
url: string;
/**
* Custom headers to send to the proxy server.
* These headers are sent in the CONNECT request (for HTTPS targets)
* or in the proxy request (for HTTP targets).
*/
headers?: Bun.HeadersInit;
};
/**
* Override the default S3 options

View File

@@ -1212,11 +1212,19 @@ pub fn mmapFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.
}
if (try opts.get(globalThis, "size")) |value| {
map_size = @as(usize, @intCast(value.toInt64()));
const size_value = try value.coerceToInt64(globalThis);
if (size_value < 0) {
return globalThis.throwInvalidArguments("size must be a non-negative integer", .{});
}
map_size = @intCast(size_value);
}
if (try opts.get(globalThis, "offset")) |value| {
offset = @as(usize, @intCast(value.toInt64()));
const offset_value = try value.coerceToInt64(globalThis);
if (offset_value < 0) {
return globalThis.throwInvalidArguments("offset must be a non-negative integer", .{});
}
offset = @intCast(offset_value);
offset = std.mem.alignBackwardAnyAlign(usize, offset, std.heap.pageSize());
}
}

View File

@@ -24,7 +24,7 @@ JS_EXPORT_PRIVATE JSWrappingFunction* JSWrappingFunction::create(
Zig::NativeFunctionPtr functionPointer,
JSC::JSValue wrappedFnValue)
{
JSC::JSFunction* wrappedFn = jsCast<JSC::JSFunction*>(wrappedFnValue.asCell());
JSC::JSObject* wrappedFn = wrappedFnValue.getObject();
ASSERT(wrappedFn != nullptr);
auto nameStr = symbolName->tag == BunStringTag::Empty ? WTF::emptyString() : symbolName->toWTFString();
@@ -75,9 +75,9 @@ extern "C" JSC::EncodedJSValue Bun__JSWrappingFunction__getWrappedFunction(
Zig::GlobalObject* globalObject)
{
JSC::JSValue thisValue = JSC::JSValue::decode(thisValueEncoded);
JSWrappingFunction* thisObject = jsCast<JSWrappingFunction*>(thisValue.asCell());
JSWrappingFunction* thisObject = jsDynamicCast<JSWrappingFunction*>(thisValue.asCell());
if (thisObject != nullptr) {
JSC::JSFunction* wrappedFn = thisObject->m_wrappedFn.get();
JSC::JSObject* wrappedFn = thisObject->m_wrappedFn.get();
return JSC::JSValue::encode(wrappedFn);
}
return {};

View File

@@ -59,7 +59,7 @@ public:
}
private:
JSWrappingFunction(JSC::VM& vm, JSC::NativeExecutable* native, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSFunction* wrappedFn)
JSWrappingFunction(JSC::VM& vm, JSC::NativeExecutable* native, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* wrappedFn)
: Base(vm, native, globalObject, structure)
, m_wrappedFn(wrappedFn, JSC::WriteBarrierEarlyInit)
{
@@ -69,7 +69,7 @@ private:
DECLARE_VISIT_CHILDREN;
JSC::WriteBarrier<JSC::JSFunction> m_wrappedFn;
JSC::WriteBarrier<JSC::JSObject> m_wrappedFn;
};
}

View File

@@ -5677,7 +5677,15 @@ CPP_DECL JSC::EncodedJSValue WebCore__DOMFormData__createFromURLQuery(JSC::JSGlo
{
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(arg0);
// don't need to copy the string because it internally does.
auto formData = DOMFormData::create(globalObject->scriptExecutionContext(), toString(*arg1));
auto str = toString(*arg1);
// toString() in helpers.h returns an empty string when the input exceeds
// String::MaxLength or Bun's synthetic allocation limit. This is the only
// condition under which toString() returns empty for non-empty input.
if (str.isEmpty() && arg1->len > 0) {
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
return Bun::ERR::STRING_TOO_LONG(scope, globalObject);
}
auto formData = DOMFormData::create(globalObject->scriptExecutionContext(), WTFMove(str));
return JSValue::encode(toJSNewlyCreated(arg0, globalObject, WTFMove(formData)));
}

View File

@@ -2,6 +2,7 @@
#include "root.h"
#include "wtf/text/ASCIILiteral.h"
#include "wtf/SIMDUTF.h"
#include <JavaScriptCore/Error.h>
#include <JavaScriptCore/Exception.h>
@@ -79,12 +80,24 @@ static const WTF::String toString(ZigString str)
}
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
ASSERT_WITH_MESSAGE(!isTaggedExternalPtr(str.ptr), "UTF8 and external ptr are mutually exclusive. The external will never be freed.");
// Check if the resulting UTF-16 string could possibly exceed the maximum length.
// For valid UTF-8, the number of UTF-16 code units is <= the number of UTF-8 bytes
// (ASCII is 1:1; other code points use multiple UTF-8 bytes per UTF-16 code unit).
// We only need to compute the actual UTF-16 length when the byte length exceeds the limit.
size_t maxLength = std::min(Bun__stringSyntheticAllocationLimit, static_cast<size_t>(WTF::String::MaxLength));
if (str.len > maxLength) [[unlikely]] {
// UTF-8 byte length != UTF-16 length, so use simdutf to calculate the actual UTF-16 length.
size_t utf16Length = simdutf::utf16_length_from_utf8(reinterpret_cast<const char*>(untag(str.ptr)), str.len);
if (utf16Length > maxLength) {
return {};
}
}
return WTF::String::fromUTF8ReplacingInvalidSequences(std::span { untag(str.ptr), str.len });
}
if (isTaggedExternalPtr(str.ptr)) [[unlikely]] {
// This will fail if the string is too long. Let's make it explicit instead of an ASSERT.
if (str.len > Bun__stringSyntheticAllocationLimit) [[unlikely]] {
if (str.len > Bun__stringSyntheticAllocationLimit || str.len > WTF::String::MaxLength) [[unlikely]] {
free_global_string(nullptr, reinterpret_cast<void*>(const_cast<unsigned char*>(untag(str.ptr))), static_cast<unsigned>(str.len));
return {};
}
@@ -95,7 +108,7 @@ static const WTF::String toString(ZigString str)
}
// This will fail if the string is too long. Let's make it explicit instead of an ASSERT.
if (str.len > Bun__stringSyntheticAllocationLimit) [[unlikely]] {
if (str.len > Bun__stringSyntheticAllocationLimit || str.len > WTF::String::MaxLength) [[unlikely]] {
return {};
}
@@ -121,11 +134,19 @@ static const WTF::String toString(ZigString str, StringPointer ptr)
return WTF::String();
}
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
// Check if the resulting UTF-16 string could possibly exceed the maximum length.
size_t maxLength = std::min(Bun__stringSyntheticAllocationLimit, static_cast<size_t>(WTF::String::MaxLength));
if (ptr.len > maxLength) [[unlikely]] {
size_t utf16Length = simdutf::utf16_length_from_utf8(reinterpret_cast<const char*>(&untag(str.ptr)[ptr.off]), ptr.len);
if (utf16Length > maxLength) {
return {};
}
}
return WTF::String::fromUTF8ReplacingInvalidSequences(std::span { &untag(str.ptr)[ptr.off], ptr.len });
}
// This will fail if the string is too long. Let's make it explicit instead of an ASSERT.
if (str.len > Bun__stringSyntheticAllocationLimit) [[unlikely]] {
if (ptr.len > Bun__stringSyntheticAllocationLimit || ptr.len > WTF::String::MaxLength) [[unlikely]] {
return {};
}
@@ -141,11 +162,19 @@ static const WTF::String toStringCopy(ZigString str, StringPointer ptr)
return WTF::String();
}
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
// Check if the resulting UTF-16 string could possibly exceed the maximum length.
size_t maxLength = std::min(Bun__stringSyntheticAllocationLimit, static_cast<size_t>(WTF::String::MaxLength));
if (ptr.len > maxLength) [[unlikely]] {
size_t utf16Length = simdutf::utf16_length_from_utf8(reinterpret_cast<const char*>(&untag(str.ptr)[ptr.off]), ptr.len);
if (utf16Length > maxLength) {
return {};
}
}
return WTF::String::fromUTF8ReplacingInvalidSequences(std::span { &untag(str.ptr)[ptr.off], ptr.len });
}
// This will fail if the string is too long. Let's make it explicit instead of an ASSERT.
if (str.len > Bun__stringSyntheticAllocationLimit) [[unlikely]] {
if (ptr.len > Bun__stringSyntheticAllocationLimit || ptr.len > WTF::String::MaxLength) [[unlikely]] {
return {};
}
@@ -161,6 +190,14 @@ static const WTF::String toStringCopy(ZigString str)
return WTF::String();
}
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
// Check if the resulting UTF-16 string could possibly exceed the maximum length.
size_t maxLength = std::min(Bun__stringSyntheticAllocationLimit, static_cast<size_t>(WTF::String::MaxLength));
if (str.len > maxLength) [[unlikely]] {
size_t utf16Length = simdutf::utf16_length_from_utf8(reinterpret_cast<const char*>(untag(str.ptr)), str.len);
if (utf16Length > maxLength) {
return {};
}
}
return WTF::String::fromUTF8ReplacingInvalidSequences(std::span { untag(str.ptr), str.len });
}
@@ -188,6 +225,14 @@ static void appendToBuilder(ZigString str, WTF::StringBuilder& builder)
return;
}
if (isTaggedUTF8Ptr(str.ptr)) [[unlikely]] {
// Check if the resulting UTF-16 string could possibly exceed the maximum length.
size_t maxLength = std::min(Bun__stringSyntheticAllocationLimit, static_cast<size_t>(WTF::String::MaxLength));
if (str.len > maxLength) [[unlikely]] {
size_t utf16Length = simdutf::utf16_length_from_utf8(reinterpret_cast<const char*>(untag(str.ptr)), str.len);
if (utf16Length > maxLength) {
return;
}
}
WTF::String converted = WTF::String::fromUTF8ReplacingInvalidSequences(std::span { untag(str.ptr), str.len });
builder.append(converted);
return;

View File

@@ -632,7 +632,11 @@ pub fn Bun__fetch_(
break :extract_verbose verbose;
};
// proxy: string | undefined;
// proxy: string | { url: string, headers?: Headers } | undefined;
var proxy_headers: ?Headers = null;
defer if (proxy_headers) |*hdrs| {
hdrs.deinit();
};
url_proxy_buffer = extract_proxy: {
const objects_to_try = [_]jsc.JSValue{
options_object orelse .zero,
@@ -641,6 +645,7 @@ pub fn Bun__fetch_(
inline for (0..2) |i| {
if (objects_to_try[i] != .zero) {
if (try objects_to_try[i].get(globalThis, "proxy")) |proxy_arg| {
// Handle string format: proxy: "http://proxy.example.com:8080"
if (proxy_arg.isString() and try proxy_arg.getLength(ctx) > 0) {
var href = try jsc.URL.hrefFromJS(proxy_arg, globalThis);
if (href.tag == .Dead) {
@@ -661,6 +666,54 @@ pub fn Bun__fetch_(
allocator.free(url_proxy_buffer);
break :extract_proxy buffer;
}
// Handle object format: proxy: { url: "http://proxy.example.com:8080", headers?: Headers }
if (proxy_arg.isObject()) {
// Get the URL from the proxy object
const proxy_url_arg = try proxy_arg.get(globalThis, "url");
if (proxy_url_arg == null or proxy_url_arg.?.isUndefinedOrNull()) {
const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy object requires a 'url' property", .{});
is_error = true;
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err);
}
if (proxy_url_arg.?.isString() and try proxy_url_arg.?.getLength(ctx) > 0) {
var href = try jsc.URL.hrefFromJS(proxy_url_arg.?, globalThis);
if (href.tag == .Dead) {
const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{});
is_error = true;
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err);
}
defer href.deref();
const buffer = try std.fmt.allocPrint(allocator, "{s}{f}", .{ url_proxy_buffer, href });
url = ZigURL.parse(buffer[0..url.href.len]);
if (url.isFile()) {
url_type = URLType.file;
} else if (url.isBlob()) {
url_type = URLType.blob;
}
proxy = ZigURL.parse(buffer[url.href.len..]);
allocator.free(url_proxy_buffer);
url_proxy_buffer = buffer;
// Get the headers from the proxy object (optional)
if (try proxy_arg.get(globalThis, "headers")) |headers_value| {
if (!headers_value.isUndefinedOrNull()) {
if (headers_value.as(FetchHeaders)) |fetch_hdrs| {
proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err);
} else if (try FetchHeaders.createFromJS(ctx, headers_value)) |fetch_hdrs| {
defer fetch_hdrs.deref();
proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err);
}
}
}
break :extract_proxy url_proxy_buffer;
} else {
const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy.url must be a non-empty string", .{});
is_error = true;
return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err);
}
}
}
if (globalThis.hasException()) {
@@ -1338,6 +1391,7 @@ pub fn Bun__fetch_(
.redirect_type = redirect_type,
.verbose = verbose,
.proxy = proxy,
.proxy_headers = proxy_headers,
.url_proxy_buffer = url_proxy_buffer,
.signal = signal,
.globalThis = globalThis,
@@ -1372,6 +1426,7 @@ pub fn Bun__fetch_(
body = FetchTasklet.HTTPRequestBody.Empty;
}
proxy = null;
proxy_headers = null;
url_proxy_buffer = "";
signal = null;
ssl_config = null;

View File

@@ -1049,6 +1049,7 @@ pub const FetchTasklet = struct {
fetch_options.redirect_type,
.{
.http_proxy = proxy,
.proxy_headers = fetch_options.proxy_headers,
.hostname = fetch_options.hostname,
.signals = fetch_tasklet.signals,
.unix_socket_path = fetch_options.unix_socket_path,
@@ -1222,6 +1223,7 @@ pub const FetchTasklet = struct {
verbose: http.HTTPVerboseLevel = .none,
redirect_type: FetchRedirect = FetchRedirect.follow,
proxy: ?ZigURL = null,
proxy_headers: ?Headers = null,
url_proxy_buffer: []const u8 = "",
signal: ?*jsc.WebCore.AbortSignal = null,
globalThis: ?*JSGlobalObject,

View File

@@ -328,10 +328,29 @@ fn writeProxyConnect(
_ = writer.write("\r\nProxy-Connection: Keep-Alive\r\n") catch 0;
// Check if user provided Proxy-Authorization in custom headers
const user_provided_proxy_auth = if (client.proxy_headers) |hdrs| hdrs.get("proxy-authorization") != null else false;
// Only write auto-generated proxy_authorization if user didn't provide one
if (client.proxy_authorization) |auth| {
_ = writer.write("Proxy-Authorization: ") catch 0;
_ = writer.write(auth) catch 0;
_ = writer.write("\r\n") catch 0;
if (!user_provided_proxy_auth) {
_ = writer.write("Proxy-Authorization: ") catch 0;
_ = writer.write(auth) catch 0;
_ = writer.write("\r\n") catch 0;
}
}
// Write custom proxy headers
if (client.proxy_headers) |hdrs| {
const slice = hdrs.entries.slice();
const names = slice.items(.name);
const values = slice.items(.value);
for (names, 0..) |name_ptr, idx| {
_ = writer.write(hdrs.asStr(name_ptr)) catch 0;
_ = writer.write(": ") catch 0;
_ = writer.write(hdrs.asStr(values[idx])) catch 0;
_ = writer.write("\r\n") catch 0;
}
}
_ = writer.write("\r\n") catch 0;
@@ -359,11 +378,31 @@ fn writeProxyRequest(
_ = writer.write(request.path) catch 0;
_ = writer.write(" HTTP/1.1\r\nProxy-Connection: Keep-Alive\r\n") catch 0;
// Check if user provided Proxy-Authorization in custom headers
const user_provided_proxy_auth = if (client.proxy_headers) |hdrs| hdrs.get("proxy-authorization") != null else false;
// Only write auto-generated proxy_authorization if user didn't provide one
if (client.proxy_authorization) |auth| {
_ = writer.write("Proxy-Authorization: ") catch 0;
_ = writer.write(auth) catch 0;
_ = writer.write("\r\n") catch 0;
if (!user_provided_proxy_auth) {
_ = writer.write("Proxy-Authorization: ") catch 0;
_ = writer.write(auth) catch 0;
_ = writer.write("\r\n") catch 0;
}
}
// Write custom proxy headers
if (client.proxy_headers) |hdrs| {
const slice = hdrs.entries.slice();
const names = slice.items(.name);
const values = slice.items(.value);
for (names, 0..) |name_ptr, idx| {
_ = writer.write(hdrs.asStr(name_ptr)) catch 0;
_ = writer.write(": ") catch 0;
_ = writer.write(hdrs.asStr(values[idx])) catch 0;
_ = writer.write("\r\n") catch 0;
}
}
for (request.headers) |header| {
_ = writer.write(header.name) catch 0;
_ = writer.write(": ") catch 0;
@@ -450,6 +489,7 @@ if_modified_since: string = "",
request_content_len_buf: ["-4294967295".len]u8 = undefined,
http_proxy: ?URL = null,
proxy_headers: ?Headers = null,
proxy_authorization: ?[]u8 = null,
proxy_tunnel: ?*ProxyTunnel = null,
signals: Signals = .{},
@@ -466,6 +506,10 @@ pub fn deinit(this: *HTTPClient) void {
this.allocator.free(auth);
this.proxy_authorization = null;
}
if (this.proxy_headers) |*hdrs| {
hdrs.deinit();
this.proxy_headers = null;
}
if (this.proxy_tunnel) |tunnel| {
this.proxy_tunnel = null;
tunnel.detachAndDeref();

View File

@@ -93,6 +93,7 @@ const AtomicState = std.atomic.Value(State);
pub const Options = struct {
http_proxy: ?URL = null,
proxy_headers: ?Headers = null,
hostname: ?[]u8 = null,
signals: ?Signals = null,
unix_socket_path: ?jsc.ZigString.Slice = null,
@@ -185,6 +186,7 @@ pub fn init(
.signals = options.signals orelse this.signals,
.async_http_id = this.async_http_id,
.http_proxy = this.http_proxy,
.proxy_headers = options.proxy_headers,
.redirect_type = redirect_type,
};
if (options.unix_socket_path) |val| {

View File

@@ -666,33 +666,19 @@ pub fn runTasks(
switch (version.tag) {
.git => {
version.value.git.package_name = pkg.name;
try manager.processDependencyListItem(dep, &any_root, install_peer);
},
.github => {
version.value.github.package_name = pkg.name;
try manager.processDependencyListItem(dep, &any_root, install_peer);
},
.tarball => {
version.value.tarball.package_name = pkg.name;
try manager.processDependencyListItem(dep, &any_root, install_peer);
},
// `else` is reachable if this package is from `overrides`. Version in `lockfile.buffer.dependencies`
// will still have the original (e.g., npm semver like "^14.14.1" overridden with "github:...").
// In this case, we can directly assign the resolution since we just extracted this package.
else => {
switch (dep) {
.root_dependency => {
assignRootResolution(manager, id, package_id);
any_root = true;
},
.dependency => {
assignResolution(manager, id, package_id);
},
else => {},
}
},
// will still have the original.
else => {},
}
try manager.processDependencyListItem(dep, &any_root, install_peer);
},
else => {
// if it's a node_module folder to install, handle that after we process all the dependencies within the onExtract callback.
@@ -1136,5 +1122,3 @@ const PackageManager = bun.install.PackageManager;
const Options = PackageManager.Options;
const PackageInstaller = PackageManager.PackageInstaller;
const ProgressStrings = PackageManager.ProgressStrings;
const assignResolution = PackageManager.assignResolution;
const assignRootResolution = PackageManager.assignRootResolution;

View File

@@ -3807,6 +3807,7 @@ class Http2Server extends net.Server {
if (typeof callback === "function") {
this.on("timeout", callback);
}
return this;
}
updateSettings(settings) {
assertSettings(settings);
@@ -3900,6 +3901,7 @@ class Http2SecureServer extends tls.Server {
if (typeof callback === "function") {
this.on("timeout", callback);
}
return this;
}
updateSettings(settings) {
assertSettings(settings);

View File

@@ -984,7 +984,12 @@ pub const FormData = struct {
switch (encoding) {
.URLEncoded => {
var str = jsc.ZigString.fromUTF8(strings.withoutUTF8BOM(input));
return jsc.DOMFormData.createFromURLQuery(globalThis, &str);
const result = jsc.DOMFormData.createFromURLQuery(globalThis, &str);
// Check if an exception was thrown (e.g., string too long)
if (result == .zero) {
return error.JSError;
}
return result;
},
.Multipart => |boundary| return toJSFromMultipartData(globalThis, input, boundary),
}
@@ -1041,7 +1046,11 @@ pub const FormData = struct {
return globalThis.throwInvalidArguments("input must be a string or ArrayBufferView", .{});
}
return FormData.toJS(globalThis, input, encoding) catch |err| return globalThis.throwError(err, "while parsing FormData");
return FormData.toJS(globalThis, input, encoding) catch |err| {
if (err == error.JSError) return error.JSError;
if (err == error.JSTerminated) return error.JSTerminated;
return globalThis.throwError(err, "while parsing FormData");
};
}
comptime {

View File

@@ -247,61 +247,3 @@ test("overrides do not apply to workspaces", async () => {
expect(await exited).toBe(0);
expect(await stderr.text()).not.toContain("Saved lockfile");
});
test("resolutions with github dependency override (monorepo)", async () => {
// This test verifies that using "resolutions" to override a semver dependency
// with a GitHub dependency works correctly, even when the GitHub repo's
// root package.json has a DIFFERENT name than the requested package.
// This is the case for monorepos like discord.js where:
// - The user requests "discord.js" (the npm package name)
// - The GitHub repo's root package.json has name "@discordjs/discord.js"
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
// Use a semver version that would normally resolve from npm
"discord.js": "^14.14.1",
},
resolutions: {
// Override with a GitHub commit from the monorepo
// The root package.json in this repo has name "@discordjs/discord.js"
"discord.js": "github:discordjs/discord.js#8dd69cf2811d86ed8da2a7b1e9a6a94022554e96",
},
}),
);
// Install should succeed (previously this would fail with "discord.js@^14.14.1 failed to resolve")
install(tmp, ["install"]);
// Verify the package was installed from GitHub
// Note: The installed package will have the name from the root package.json
const discordPkg = JSON.parse(readFileSync(join(tmp, "node_modules/discord.js/package.json"), "utf-8"));
expect(discordPkg.name).toBe("@discordjs/discord.js");
// Verify the lockfile is stable
ensureLockfileDoesntChangeOnBunI(tmp);
});
test("overrides with github dependency (using overrides field, monorepo)", async () => {
// Same as above but using the npm-style "overrides" field instead of "resolutions"
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
"discord.js": "^14.14.1",
},
overrides: {
"discord.js": "github:discordjs/discord.js#8dd69cf2811d86ed8da2a7b1e9a6a94022554e96",
},
}),
);
install(tmp, ["install"]);
const discordPkg = JSON.parse(readFileSync(join(tmp, "node_modules/discord.js/package.json"), "utf-8"));
expect(discordPkg.name).toBe("@discordjs/discord.js");
ensureLockfileDoesntChangeOnBunI(tmp);
});

View File

@@ -246,3 +246,80 @@ if (typeof process !== "undefined") {
// @ts-expect-error - -Infinity
fetch("https://example.com", { body: -Infinity });
}
// Proxy option types
{
// String proxy URL is valid
fetch("https://example.com", { proxy: "http://proxy.example.com:8080" });
fetch("https://example.com", { proxy: "https://user:pass@proxy.example.com:8080" });
}
{
// Object proxy with url is valid
fetch("https://example.com", {
proxy: {
url: "http://proxy.example.com:8080",
},
});
}
{
// Object proxy with url and headers (plain object) is valid
fetch("https://example.com", {
proxy: {
url: "http://proxy.example.com:8080",
headers: {
"Proxy-Authorization": "Bearer token",
"X-Custom-Header": "value",
},
},
});
}
{
// Object proxy with url and headers (Headers instance) is valid
fetch("https://example.com", {
proxy: {
url: "http://proxy.example.com:8080",
headers: new Headers({ "Proxy-Authorization": "Bearer token" }),
},
});
}
{
// Object proxy with url and headers (array of tuples) is valid
fetch("https://example.com", {
proxy: {
url: "http://proxy.example.com:8080",
headers: [
["Proxy-Authorization", "Bearer token"],
["X-Custom", "value"],
],
},
});
}
{
// @ts-expect-error - Proxy object without url is invalid
fetch("https://example.com", { proxy: { headers: { "X-Custom": "value" } } });
}
{
// @ts-expect-error - Proxy url must be string, not number
fetch("https://example.com", { proxy: { url: 8080 } });
}
{
// @ts-expect-error - Proxy must be string or object, not number
fetch("https://example.com", { proxy: 8080 });
}
{
// @ts-expect-error - Proxy must be string or object, not boolean
fetch("https://example.com", { proxy: true });
}
{
// @ts-expect-error - Proxy must be string or object, not array
fetch("https://example.com", { proxy: ["http://proxy.example.com"] });
}

View File

@@ -337,3 +337,366 @@ test("HTTPS origin close-delimited body via HTTP proxy does not ECONNRESET", asy
await once(originServer, "close");
}
});
describe("proxy object format with headers", () => {
test("proxy object with url string works same as string proxy", async () => {
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: httpProxyServer.url,
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
});
test("proxy object with url and headers sends headers to proxy (HTTP proxy)", async () => {
// Create a proxy server that captures headers
const capturedHeaders: string[] = [];
const proxyServerWithCapture = net.createServer((clientSocket: net.Socket) => {
clientSocket.once("data", data => {
const request = data.toString();
// Capture headers
const lines = request.split("\r\n");
for (const line of lines) {
if (line.toLowerCase().startsWith("x-proxy-")) {
capturedHeaders.push(line.toLowerCase());
}
}
const [method, path] = request.split(" ");
let host: string;
let port: number | string = 0;
let request_path = "";
if (path.indexOf("http") !== -1) {
const url = new URL(path);
host = url.hostname;
port = url.port;
request_path = url.pathname + (url.search || "");
} else {
[host, port] = path.split(":");
}
const destinationPort = Number.parseInt((port || (method === "CONNECT" ? "443" : "80")).toString(), 10);
const destinationHost = host || "";
const serverSocket = net.connect(destinationPort, destinationHost, () => {
if (method === "CONNECT") {
clientSocket.write("HTTP/1.1 200 OK\r\nHost: localhost\r\n\r\n");
clientSocket.pipe(serverSocket);
serverSocket.pipe(clientSocket);
} else {
serverSocket.write(`${method} ${request_path} HTTP/1.1\r\n`);
serverSocket.write(data.slice(request.indexOf("\r\n") + 2));
serverSocket.pipe(clientSocket);
}
});
clientSocket.on("error", () => {});
serverSocket.on("error", () => {
clientSocket.end();
});
});
});
proxyServerWithCapture.listen(0);
await once(proxyServerWithCapture, "listening");
const proxyPort = (proxyServerWithCapture.address() as net.AddressInfo).port;
const proxyUrl = `http://localhost:${proxyPort}`;
try {
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: proxyUrl,
headers: {
"X-Proxy-Custom-Header": "custom-value",
"X-Proxy-Another": "another-value",
},
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
// Verify the custom headers were sent to the proxy (case-insensitive check)
expect(capturedHeaders).toContainEqual(expect.stringContaining("x-proxy-custom-header: custom-value"));
expect(capturedHeaders).toContainEqual(expect.stringContaining("x-proxy-another: another-value"));
} finally {
proxyServerWithCapture.close();
await once(proxyServerWithCapture, "close");
}
});
test("proxy object with url and headers sends headers in CONNECT request (HTTPS target)", async () => {
// Create a proxy server that captures headers
const capturedHeaders: string[] = [];
const proxyServerWithCapture = net.createServer((clientSocket: net.Socket) => {
clientSocket.once("data", data => {
const request = data.toString();
// Capture headers
const lines = request.split("\r\n");
for (const line of lines) {
if (line.toLowerCase().startsWith("x-proxy-")) {
capturedHeaders.push(line.toLowerCase());
}
}
const [method, path] = request.split(" ");
let host: string;
let port: number | string = 0;
if (path.indexOf("http") !== -1) {
const url = new URL(path);
host = url.hostname;
port = url.port;
} else {
[host, port] = path.split(":");
}
const destinationPort = Number.parseInt((port || (method === "CONNECT" ? "443" : "80")).toString(), 10);
const destinationHost = host || "";
const serverSocket = net.connect(destinationPort, destinationHost, () => {
if (method === "CONNECT") {
clientSocket.write("HTTP/1.1 200 OK\r\nHost: localhost\r\n\r\n");
clientSocket.pipe(serverSocket);
serverSocket.pipe(clientSocket);
} else {
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
clientSocket.end();
}
});
clientSocket.on("error", () => {});
serverSocket.on("error", () => {
clientSocket.end();
});
});
});
proxyServerWithCapture.listen(0);
await once(proxyServerWithCapture, "listening");
const proxyPort = (proxyServerWithCapture.address() as net.AddressInfo).port;
const proxyUrl = `http://localhost:${proxyPort}`;
try {
const response = await fetch(httpsServer.url, {
method: "GET",
proxy: {
url: proxyUrl,
headers: new Headers({
"X-Proxy-Auth-Token": "secret-token-123",
}),
},
keepalive: false,
tls: {
ca: tlsCert.cert,
rejectUnauthorized: false,
},
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
// Verify the custom headers were sent in the CONNECT request (case-insensitive check)
expect(capturedHeaders).toContainEqual(expect.stringContaining("x-proxy-auth-token: secret-token-123"));
} finally {
proxyServerWithCapture.close();
await once(proxyServerWithCapture, "close");
}
});
test("proxy object without url throws error", async () => {
await expect(
fetch(httpServer.url, {
method: "GET",
proxy: {
headers: { "X-Test": "value" },
} as any,
keepalive: false,
}),
).rejects.toThrow("fetch() proxy object requires a 'url' property");
});
test("proxy object with null url throws error", async () => {
await expect(
fetch(httpServer.url, {
method: "GET",
proxy: {
url: null,
headers: { "X-Test": "value" },
} as any,
keepalive: false,
}),
).rejects.toThrow("fetch() proxy object requires a 'url' property");
});
test("proxy object with empty string url throws error", async () => {
await expect(
fetch(httpServer.url, {
method: "GET",
proxy: {
url: "",
headers: { "X-Test": "value" },
} as any,
keepalive: false,
}),
).rejects.toThrow("fetch() proxy.url must be a non-empty string");
});
test("proxy object with empty headers object works", async () => {
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: httpProxyServer.url,
headers: {},
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
});
test("proxy object with undefined headers works", async () => {
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: httpProxyServer.url,
headers: undefined,
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
});
test("proxy object with headers as Headers instance", async () => {
const capturedHeaders: string[] = [];
const proxyServerWithCapture = net.createServer((clientSocket: net.Socket) => {
clientSocket.once("data", data => {
const request = data.toString();
const lines = request.split("\r\n");
for (const line of lines) {
if (line.toLowerCase().startsWith("x-custom-")) {
capturedHeaders.push(line.toLowerCase());
}
}
const [method, path] = request.split(" ");
let host: string;
let port: number | string = 0;
let request_path = "";
if (path.indexOf("http") !== -1) {
const url = new URL(path);
host = url.hostname;
port = url.port;
request_path = url.pathname + (url.search || "");
} else {
[host, port] = path.split(":");
}
const destinationPort = Number.parseInt((port || "80").toString(), 10);
const destinationHost = host || "";
const serverSocket = net.connect(destinationPort, destinationHost, () => {
serverSocket.write(`${method} ${request_path} HTTP/1.1\r\n`);
serverSocket.write(data.slice(request.indexOf("\r\n") + 2));
serverSocket.pipe(clientSocket);
});
clientSocket.on("error", () => {});
serverSocket.on("error", () => {
clientSocket.end();
});
});
});
proxyServerWithCapture.listen(0);
await once(proxyServerWithCapture, "listening");
const proxyPort = (proxyServerWithCapture.address() as net.AddressInfo).port;
const proxyUrl = `http://localhost:${proxyPort}`;
try {
const headers = new Headers();
headers.set("X-Custom-Header-1", "value1");
headers.set("X-Custom-Header-2", "value2");
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: proxyUrl,
headers: headers,
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
// Case-insensitive check
expect(capturedHeaders).toContainEqual(expect.stringContaining("x-custom-header-1: value1"));
expect(capturedHeaders).toContainEqual(expect.stringContaining("x-custom-header-2: value2"));
} finally {
proxyServerWithCapture.close();
await once(proxyServerWithCapture, "close");
}
});
test("user-provided Proxy-Authorization header overrides URL credentials", async () => {
const capturedHeaders: string[] = [];
const proxyServerWithCapture = net.createServer((clientSocket: net.Socket) => {
clientSocket.once("data", data => {
const request = data.toString();
const lines = request.split("\r\n");
for (const line of lines) {
if (line.toLowerCase().startsWith("proxy-authorization:")) {
capturedHeaders.push(line.toLowerCase());
}
}
const [method, path] = request.split(" ");
let host: string;
let port: number | string = 0;
let request_path = "";
if (path.indexOf("http") !== -1) {
const url = new URL(path);
host = url.hostname;
port = url.port;
request_path = url.pathname + (url.search || "");
} else {
[host, port] = path.split(":");
}
const destinationPort = Number.parseInt((port || "80").toString(), 10);
const destinationHost = host || "";
const serverSocket = net.connect(destinationPort, destinationHost, () => {
serverSocket.write(`${method} ${request_path} HTTP/1.1\r\n`);
serverSocket.write(data.slice(request.indexOf("\r\n") + 2));
serverSocket.pipe(clientSocket);
});
clientSocket.on("error", () => {});
serverSocket.on("error", () => {
clientSocket.end();
});
});
});
proxyServerWithCapture.listen(0);
await once(proxyServerWithCapture, "listening");
const proxyPort = (proxyServerWithCapture.address() as net.AddressInfo).port;
// Proxy URL with credentials that would generate Basic auth
const proxyUrl = `http://urluser:urlpass@localhost:${proxyPort}`;
try {
const response = await fetch(httpServer.url, {
method: "GET",
proxy: {
url: proxyUrl,
headers: {
// User-provided Proxy-Authorization should override the URL-based one
"Proxy-Authorization": "Bearer custom-token-12345",
},
},
keepalive: false,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
// Should only have one Proxy-Authorization header (the user-provided one)
expect(capturedHeaders.length).toBe(1);
expect(capturedHeaders[0]).toBe("proxy-authorization: bearer custom-token-12345");
} finally {
proxyServerWithCapture.close();
await once(proxyServerWithCapture, "close");
}
});
});

View File

@@ -68,4 +68,32 @@ describe.skipIf(isWindows)("Bun.mmap", async () => {
expect(map[0]).toBe(old);
await gcTick();
});
it("mmap rejects negative offset", () => {
expect(() => Bun.mmap(path, { offset: -1 })).toThrow("offset must be a non-negative integer");
});
it("mmap rejects negative size", () => {
expect(() => Bun.mmap(path, { size: -1 })).toThrow("size must be a non-negative integer");
});
it("mmap handles non-number offset/size without crashing", () => {
// These should not crash - non-number values coerce to 0 per JavaScript semantics
// Previously these caused assertion failures (issue ENG-22413)
// null coerces to 0, which is valid for offset
expect(() => {
Bun.mmap(path, { offset: null });
}).not.toThrow();
// size: null coerces to 0, which is invalid (EINVAL), but shouldn't crash
expect(() => {
Bun.mmap(path, { size: null });
}).toThrow("EINVAL");
// undefined is ignored (property not set)
expect(() => {
Bun.mmap(path, { offset: undefined });
}).not.toThrow();
});
});

View File

@@ -277,6 +277,23 @@ describe("FormData", () => {
expect(fd.toJSON()).toEqual({ "1": "1" });
});
test("FormData.from throws on very large input instead of crashing", () => {
// This test verifies that FormData.from throws an exception instead of crashing
// when given input larger than WebKit's String::MaxLength (INT32_MAX ~= 2GB).
// We use a smaller test case with the synthetic limit to avoid actually allocating 2GB+.
const { setSyntheticAllocationLimitForTesting } = require("bun:internal-for-testing");
// Set a small limit so we can test the boundary without allocating gigabytes
const originalLimit = setSyntheticAllocationLimitForTesting(1024 * 1024); // 1MB limit
try {
// Create a buffer larger than the limit
const largeBuffer = new Uint8Array(2 * 1024 * 1024); // 2MB
// @ts-expect-error - FormData.from is a Bun extension
expect(() => FormData.from(largeBuffer)).toThrow("Cannot create a string longer than");
} finally {
setSyntheticAllocationLimitForTesting(originalLimit);
}
});
it("should throw on bad boundary", async () => {
const response = new Response('foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n', {
headers: {

View File

@@ -0,0 +1,67 @@
import { expect, test } from "bun:test";
import { tls } from "harness";
import * as http2 from "http2";
test("Http2Server.setTimeout returns server instance for method chaining", () => {
const server = http2.createServer();
try {
const result = server.setTimeout(1000);
expect(result).toBe(server);
} finally {
server.close();
}
});
test("Http2Server.setTimeout with callback returns server instance", () => {
const server = http2.createServer();
const callback = () => {};
try {
const result = server.setTimeout(1000, callback);
expect(result).toBe(server);
} finally {
server.close();
}
});
test("Http2Server.setTimeout allows method chaining with close", () => {
const server = http2.createServer();
// This should not throw - chaining should work
expect(() => {
server.setTimeout(1000).close();
}).not.toThrow();
});
test("Http2SecureServer.setTimeout returns server instance for method chaining", () => {
const server = http2.createSecureServer(tls);
try {
const result = server.setTimeout(1000);
expect(result).toBe(server);
} finally {
server.close();
}
});
test("Http2SecureServer.setTimeout with callback returns server instance", () => {
const server = http2.createSecureServer(tls);
const callback = () => {};
try {
const result = server.setTimeout(1000, callback);
expect(result).toBe(server);
} finally {
server.close();
}
});
test("Http2SecureServer.setTimeout allows method chaining with close", () => {
const server = http2.createSecureServer(tls);
// This should not throw - chaining should work
expect(() => {
server.setTimeout(1000).close();
}).not.toThrow();
});

View File

@@ -0,0 +1,40 @@
import { expect, test } from "bun:test";
// Regression test for ENG-22942: Crash when calling expect.extend with non-function values
// The crash occurred because JSWrappingFunction assumed all callable objects are JSFunction,
// but class constructors like Expect are callable but not JSFunction instances.
test("expect.extend with jest object should throw TypeError, not crash", () => {
const jest = Bun.jest(import.meta.path);
expect(() => {
jest.expect.extend(jest);
}).toThrow(TypeError);
});
test("expect.extend with object containing non-function values should throw", () => {
const jest = Bun.jest(import.meta.path);
expect(() => {
jest.expect.extend({
notAFunction: "string value",
});
}).toThrow("expect.extend: `notAFunction` is not a valid matcher");
});
test("expect.extend with valid matchers still works", () => {
const jest = Bun.jest(import.meta.path);
jest.expect.extend({
toBeEven(received: number) {
const pass = received % 2 === 0;
return {
message: () => `expected ${received} ${pass ? "not " : ""}to be even`,
pass,
};
},
});
jest.expect(4).toBeEven();
jest.expect(3).not.toBeEven();
});