mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(napi): return napi_function for AsyncContextFrame in napi_typeof (#26511)
## Summary - `napi_typeof` was returning `napi_object` for `AsyncContextFrame` values, which are internally callable JSObjects - Native addons that check callback types (e.g. encore.dev's runtime) would fail with `expect Function, got: Object` and panic - Added a `jsDynamicCast<AsyncContextFrame*>` check before the final `napi_object` fallback to correctly report these values as `napi_function` Closes #25933 ## Test plan - [x] Verify encore.dev + supertokens reproduction from the issue no longer panics - [ ] Existing napi tests continue to pass Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ JSValue AsyncContextFrame::withAsyncContextIfNeeded(JSGlobalObject* globalObject
|
|||||||
return callback;
|
return callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If already wrapped in an AsyncContextFrame, return as-is to avoid double-wrapping.
|
||||||
|
if (jsDynamicCast<AsyncContextFrame*>(callback)) {
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
// Construct a low-overhead wrapper
|
// Construct a low-overhead wrapper
|
||||||
auto& vm = JSC::getVM(globalObject);
|
auto& vm = JSC::getVM(globalObject);
|
||||||
return AsyncContextFrame::create(
|
return AsyncContextFrame::create(
|
||||||
|
|||||||
@@ -2427,6 +2427,11 @@ extern "C" napi_status napi_typeof(napi_env env, napi_value val,
|
|||||||
return napi_clear_last_error(env);
|
return napi_clear_last_error(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (JSC::jsDynamicCast<AsyncContextFrame*>(value)) {
|
||||||
|
*result = napi_function;
|
||||||
|
return napi_clear_last_error(env);
|
||||||
|
}
|
||||||
|
|
||||||
*result = napi_object;
|
*result = napi_object;
|
||||||
return napi_clear_last_error(env);
|
return napi_clear_last_error(env);
|
||||||
|
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ pub export fn napi_make_callback(env_: napi_env, _: *anyopaque, recv_: napi_valu
|
|||||||
return envIsNull();
|
return envIsNull();
|
||||||
};
|
};
|
||||||
const recv, const func = .{ recv_.get(), func_.get() };
|
const recv, const func = .{ recv_.get(), func_.get() };
|
||||||
if (func.isEmptyOrUndefinedOrNull() or !func.isCallable()) {
|
if (func.isEmptyOrUndefinedOrNull() or (!func.isCallable() and !func.isAsyncContextFrame())) {
|
||||||
return env.setLastError(.function_expected);
|
return env.setLastError(.function_expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,7 +1762,7 @@ pub export fn napi_create_threadsafe_function(
|
|||||||
};
|
};
|
||||||
const func = func_.get();
|
const func = func_.get();
|
||||||
|
|
||||||
if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable())) {
|
if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or (!func.isCallable() and !func.isAsyncContextFrame()))) {
|
||||||
return env.setLastError(.function_expected);
|
return env.setLastError(.function_expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -895,4 +895,52 @@ nativeTests.test_napi_typeof_boxed_primitives = () => {
|
|||||||
console.log("All boxed primitive tests passed!");
|
console.log("All boxed primitive tests passed!");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// https://github.com/oven-sh/bun/issues/25933
|
||||||
|
// Test that napi_typeof returns napi_function for callbacks wrapped in
|
||||||
|
// AsyncContextFrame (which happens inside AsyncLocalStorage.run()).
|
||||||
|
nativeTests.test_napi_typeof_async_context_frame = async () => {
|
||||||
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||||
|
const als = new AsyncLocalStorage();
|
||||||
|
|
||||||
|
await als.run({ key: "value" }, () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
// Pass a callback to the native addon. Because we're inside
|
||||||
|
// AsyncLocalStorage.run(), Bun wraps it in AsyncContextFrame.
|
||||||
|
// The native call_js_cb will call napi_typeof on the received
|
||||||
|
// js_callback and print the result.
|
||||||
|
nativeTests.test_issue_25933(() => {});
|
||||||
|
// The threadsafe function callback fires asynchronously.
|
||||||
|
setTimeout(resolve, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test that napi_make_callback works when the func is an AsyncContextFrame
|
||||||
|
// (received by a threadsafe function's call_js_cb inside AsyncLocalStorage.run()).
|
||||||
|
nativeTests.test_make_callback_with_async_context = async () => {
|
||||||
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||||
|
const als = new AsyncLocalStorage();
|
||||||
|
|
||||||
|
await als.run({ key: "value" }, () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
nativeTests.test_napi_make_callback_async_context_frame(() => {});
|
||||||
|
setTimeout(resolve, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test that napi_create_threadsafe_function with call_js_cb=NULL accepts an
|
||||||
|
// AsyncContextFrame as the func (received from another threadsafe function's call_js_cb).
|
||||||
|
nativeTests.test_create_tsfn_with_async_context = async () => {
|
||||||
|
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||||
|
const als = new AsyncLocalStorage();
|
||||||
|
|
||||||
|
await als.run({ key: "value" }, () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
nativeTests.test_napi_create_tsfn_async_context_frame(() => {});
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = nativeTests;
|
module.exports = nativeTests;
|
||||||
|
|||||||
@@ -1998,6 +1998,127 @@ static napi_value test_napi_get_named_property_copied_string(const Napi::Callbac
|
|||||||
return ok(env);
|
return ok(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/oven-sh/bun/issues/25933
|
||||||
|
// When a threadsafe function is created inside AsyncLocalStorage.run(),
|
||||||
|
// the js_callback gets wrapped in AsyncContextFrame. napi_typeof must
|
||||||
|
// still report it as napi_function, not napi_object.
|
||||||
|
static napi_threadsafe_function tsfn_25933 = nullptr;
|
||||||
|
|
||||||
|
static void test_issue_25933_callback(napi_env env, napi_value js_callback,
|
||||||
|
void *context, void *data) {
|
||||||
|
napi_valuetype type;
|
||||||
|
napi_status status = napi_typeof(env, js_callback, &type);
|
||||||
|
if (status != napi_ok) {
|
||||||
|
printf("FAIL: napi_typeof returned error status %d\n", status);
|
||||||
|
} else if (type == napi_function) {
|
||||||
|
printf("PASS: napi_typeof returned napi_function\n");
|
||||||
|
} else {
|
||||||
|
printf("FAIL: napi_typeof returned %d, expected napi_function (%d)\n",
|
||||||
|
type, napi_function);
|
||||||
|
}
|
||||||
|
napi_release_threadsafe_function(tsfn_25933, napi_tsfn_release);
|
||||||
|
tsfn_25933 = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static napi_value test_issue_25933(const Napi::CallbackInfo &info) {
|
||||||
|
Napi::Env env = info.Env();
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
// The first argument is the JS callback function.
|
||||||
|
// When called inside AsyncLocalStorage.run(), Bun wraps this in
|
||||||
|
// AsyncContextFrame via withAsyncContextIfNeeded.
|
||||||
|
napi_value js_cb = info[0];
|
||||||
|
napi_value name = Napi::String::New(env, "tsfn_typeof_test");
|
||||||
|
|
||||||
|
NODE_API_CALL(env,
|
||||||
|
napi_create_threadsafe_function(
|
||||||
|
env, js_cb, nullptr, name, 0, 1, nullptr, nullptr,
|
||||||
|
nullptr, &test_issue_25933_callback, &tsfn_25933));
|
||||||
|
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn_25933, nullptr,
|
||||||
|
napi_tsfn_nonblocking));
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a threadsafe function's call_js_cb receives a js_callback that is an
|
||||||
|
// AsyncContextFrame, calling napi_make_callback on it should work (not fail
|
||||||
|
// with function_expected).
|
||||||
|
static napi_threadsafe_function tsfn_make_callback = nullptr;
|
||||||
|
|
||||||
|
static void test_make_callback_tsfn_cb(napi_env env, napi_value js_callback,
|
||||||
|
void *context, void *data) {
|
||||||
|
napi_value recv;
|
||||||
|
napi_get_global(env, &recv);
|
||||||
|
|
||||||
|
napi_value result;
|
||||||
|
napi_status status = napi_make_callback(env, nullptr, recv, js_callback, 0, nullptr, &result);
|
||||||
|
if (status == napi_ok) {
|
||||||
|
printf("PASS: napi_make_callback succeeded\n");
|
||||||
|
} else {
|
||||||
|
printf("FAIL: napi_make_callback returned status %d\n", status);
|
||||||
|
}
|
||||||
|
napi_release_threadsafe_function(tsfn_make_callback, napi_tsfn_release);
|
||||||
|
tsfn_make_callback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static napi_value test_napi_make_callback_async_context_frame(const Napi::CallbackInfo &info) {
|
||||||
|
Napi::Env env = info.Env();
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
napi_value js_cb = info[0];
|
||||||
|
napi_value name = Napi::String::New(env, "tsfn_make_callback_test");
|
||||||
|
|
||||||
|
NODE_API_CALL(env,
|
||||||
|
napi_create_threadsafe_function(
|
||||||
|
env, js_cb, nullptr, name, 0, 1, nullptr, nullptr,
|
||||||
|
nullptr, &test_make_callback_tsfn_cb, &tsfn_make_callback));
|
||||||
|
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn_make_callback, nullptr,
|
||||||
|
napi_tsfn_nonblocking));
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a threadsafe function's call_js_cb receives a js_callback that is an
|
||||||
|
// AsyncContextFrame, passing it to a second napi_create_threadsafe_function
|
||||||
|
// with call_js_cb=NULL should succeed (not fail with function_expected).
|
||||||
|
static napi_threadsafe_function tsfn_create_outer = nullptr;
|
||||||
|
|
||||||
|
static void test_create_tsfn_outer_cb(napi_env env, napi_value js_callback,
|
||||||
|
void *context, void *data) {
|
||||||
|
// js_callback here is an AsyncContextFrame in Bun.
|
||||||
|
// Try to create a new threadsafe function with it and call_js_cb=NULL.
|
||||||
|
napi_value name;
|
||||||
|
napi_create_string_utf8(env, "inner_tsfn", NAPI_AUTO_LENGTH, &name);
|
||||||
|
|
||||||
|
napi_threadsafe_function inner_tsfn = nullptr;
|
||||||
|
napi_status status = napi_create_threadsafe_function(
|
||||||
|
env, js_callback, nullptr, name, 0, 1, nullptr, nullptr,
|
||||||
|
nullptr, /* call_js_cb */ nullptr, &inner_tsfn);
|
||||||
|
if (status != napi_ok) {
|
||||||
|
printf("FAIL: napi_create_threadsafe_function returned status %d\n", status);
|
||||||
|
} else {
|
||||||
|
printf("PASS: napi_create_threadsafe_function accepted AsyncContextFrame\n");
|
||||||
|
// Release immediately — we only needed to verify creation succeeds.
|
||||||
|
napi_release_threadsafe_function(inner_tsfn, napi_tsfn_release);
|
||||||
|
}
|
||||||
|
napi_release_threadsafe_function(tsfn_create_outer, napi_tsfn_release);
|
||||||
|
tsfn_create_outer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static napi_value test_napi_create_tsfn_async_context_frame(const Napi::CallbackInfo &info) {
|
||||||
|
Napi::Env env = info.Env();
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
napi_value js_cb = info[0];
|
||||||
|
napi_value name = Napi::String::New(env, "tsfn_create_test");
|
||||||
|
|
||||||
|
NODE_API_CALL(env,
|
||||||
|
napi_create_threadsafe_function(
|
||||||
|
env, js_cb, nullptr, name, 0, 1, nullptr, nullptr,
|
||||||
|
nullptr, &test_create_tsfn_outer_cb, &tsfn_create_outer));
|
||||||
|
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn_create_outer, nullptr,
|
||||||
|
napi_tsfn_nonblocking));
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
void register_standalone_tests(Napi::Env env, Napi::Object exports) {
|
void register_standalone_tests(Napi::Env env, Napi::Object exports) {
|
||||||
REGISTER_FUNCTION(env, exports, test_issue_7685);
|
REGISTER_FUNCTION(env, exports, test_issue_7685);
|
||||||
REGISTER_FUNCTION(env, exports, test_issue_11949);
|
REGISTER_FUNCTION(env, exports, test_issue_11949);
|
||||||
@@ -2033,6 +2154,9 @@ void register_standalone_tests(Napi::Env env, Napi::Object exports) {
|
|||||||
REGISTER_FUNCTION(env, exports, napi_get_typeof);
|
REGISTER_FUNCTION(env, exports, napi_get_typeof);
|
||||||
REGISTER_FUNCTION(env, exports, test_external_buffer_data_lifetime);
|
REGISTER_FUNCTION(env, exports, test_external_buffer_data_lifetime);
|
||||||
REGISTER_FUNCTION(env, exports, test_napi_get_named_property_copied_string);
|
REGISTER_FUNCTION(env, exports, test_napi_get_named_property_copied_string);
|
||||||
|
REGISTER_FUNCTION(env, exports, test_issue_25933);
|
||||||
|
REGISTER_FUNCTION(env, exports, test_napi_make_callback_async_context_frame);
|
||||||
|
REGISTER_FUNCTION(env, exports, test_napi_create_tsfn_async_context_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace napitests
|
} // namespace napitests
|
||||||
|
|||||||
@@ -798,6 +798,30 @@ describe("cleanup hooks", () => {
|
|||||||
expect(output).toContain("napi_typeof");
|
expect(output).toContain("napi_typeof");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return napi_function for AsyncContextFrame in threadsafe callback", async () => {
|
||||||
|
// Test for https://github.com/oven-sh/bun/issues/25933
|
||||||
|
// When a threadsafe function is created inside AsyncLocalStorage.run(),
|
||||||
|
// the callback gets wrapped in AsyncContextFrame. napi_typeof must
|
||||||
|
// report it as napi_function, not napi_object.
|
||||||
|
const output = await checkSameOutput("test_napi_typeof_async_context_frame", []);
|
||||||
|
expect(output).toContain("PASS: napi_typeof returned napi_function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle AsyncContextFrame in napi_make_callback", async () => {
|
||||||
|
// When a threadsafe function's call_js_cb receives an AsyncContextFrame
|
||||||
|
// as js_callback and passes it to napi_make_callback, it should succeed.
|
||||||
|
const output = await checkSameOutput("test_make_callback_with_async_context", []);
|
||||||
|
expect(output).toContain("PASS: napi_make_callback succeeded");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept AsyncContextFrame in napi_create_threadsafe_function with null call_js_cb", async () => {
|
||||||
|
// When a threadsafe function's call_js_cb receives an AsyncContextFrame
|
||||||
|
// and passes it to a second napi_create_threadsafe_function with
|
||||||
|
// call_js_cb=NULL, it should not reject with function_expected.
|
||||||
|
const output = await checkSameOutput("test_create_tsfn_with_async_context", []);
|
||||||
|
expect(output).toContain("PASS: napi_create_threadsafe_function accepted AsyncContextFrame");
|
||||||
|
});
|
||||||
|
|
||||||
it("should return napi_object for boxed primitives (String, Number, Boolean)", async () => {
|
it("should return napi_object for boxed primitives (String, Number, Boolean)", async () => {
|
||||||
// Regression test for https://github.com/oven-sh/bun/issues/25351
|
// Regression test for https://github.com/oven-sh/bun/issues/25351
|
||||||
// napi_typeof was incorrectly returning napi_string for String objects (new String("hello"))
|
// napi_typeof was incorrectly returning napi_string for String objects (new String("hello"))
|
||||||
|
|||||||
Reference in New Issue
Block a user