mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
1 Commits
claude/fix
...
claude/rep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e70d01c9 |
11
test/napi/napi-throw-exception-segfault/binding.gyp
Normal file
11
test/napi/napi-throw-exception-segfault/binding.gyp
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "verify_node_behavior",
|
||||
"sources": ["verify_node_behavior.cpp"],
|
||||
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
8
test/napi/napi-throw-exception-segfault/binding_c.gyp
Normal file
8
test/napi/napi-throw-exception-segfault/binding_c.gyp
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_c_version",
|
||||
"sources": ["test_c_version.c"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_comprehensive",
|
||||
"sources": ["test_comprehensive.cpp"],
|
||||
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
11
test/napi/napi-throw-exception-segfault/binding_rapid.gyp
Normal file
11
test/napi/napi-throw-exception-segfault/binding_rapid.gyp
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_rapid_throws",
|
||||
"sources": ["test_rapid_throws.cpp"],
|
||||
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
11
test/napi/napi-throw-exception-segfault/binding_verify.gyp
Normal file
11
test/napi/napi-throw-exception-segfault/binding_verify.gyp
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "verify_node_behavior",
|
||||
"sources": ["verify_node_behavior.cpp"],
|
||||
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
9
test/napi/napi-throw-exception-segfault/package.json
Normal file
9
test/napi/napi-throw-exception-segfault/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "test-throw-exception-segfault",
|
||||
"version": "1.0.0",
|
||||
"main": "test.js",
|
||||
"gypfile": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.5.0"
|
||||
}
|
||||
}
|
||||
24
test/napi/napi-throw-exception-segfault/test.js
Normal file
24
test/napi/napi-throw-exception-segfault/test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const assert = require('assert');
|
||||
const testModule = require('./build/Release/test_throw_exception_segfault.node');
|
||||
|
||||
// Test the segfault case
|
||||
console.log("Testing ThrowAsJavaScriptException (expected to segfault in Bun)...");
|
||||
try {
|
||||
testModule.throwException();
|
||||
assert.fail("Expected exception to be thrown");
|
||||
} catch (error) {
|
||||
console.log("Caught exception:", error.message);
|
||||
assert.strictEqual(error.message, "Test error message");
|
||||
}
|
||||
|
||||
// Test the workaround case
|
||||
console.log("Testing workaround (throw in C++ space)...");
|
||||
try {
|
||||
testModule.throwExceptionWorkaround();
|
||||
assert.fail("Expected exception to be thrown");
|
||||
} catch (error) {
|
||||
console.log("Caught exception:", error.message);
|
||||
assert.strictEqual(error.message, "Test error message");
|
||||
}
|
||||
|
||||
console.log("All tests passed!");
|
||||
29
test/napi/napi-throw-exception-segfault/test_c.js
Normal file
29
test/napi/napi-throw-exception-segfault/test_c.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const assert = require('assert');
|
||||
|
||||
try {
|
||||
const testModule = require('./build/Release/test_c_version.node');
|
||||
|
||||
// Test throwing with napi_throw
|
||||
console.log("Testing napi_throw...");
|
||||
try {
|
||||
testModule.throwError();
|
||||
assert.fail("Expected exception to be thrown");
|
||||
} catch (error) {
|
||||
console.log("Caught exception:", error.message);
|
||||
}
|
||||
|
||||
// Test throwing with napi_throw_error
|
||||
console.log("Testing napi_throw_error...");
|
||||
try {
|
||||
testModule.throwErrorString();
|
||||
assert.fail("Expected exception to be thrown");
|
||||
} catch (error) {
|
||||
console.log("Caught exception:", error.message);
|
||||
assert.strictEqual(error.message, "Test error string from C NAPI");
|
||||
}
|
||||
|
||||
console.log("All C NAPI tests passed!");
|
||||
} catch (loadError) {
|
||||
console.error("Failed to load C module:", loadError.message);
|
||||
console.log("Building C module...");
|
||||
}
|
||||
48
test/napi/napi-throw-exception-segfault/test_c_version.c
Normal file
48
test/napi/napi-throw-exception-segfault/test_c_version.c
Normal file
@@ -0,0 +1,48 @@
|
||||
#include <assert.h>
|
||||
#include <js_native_api.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// This reproduces the issue from https://github.com/oven-sh/bun/issues/4526
|
||||
// using C NAPI to avoid any C++ wrapper issues
|
||||
|
||||
static napi_value ThrowError(napi_env env, napi_callback_info info) {
|
||||
napi_value error;
|
||||
napi_value message;
|
||||
napi_status status;
|
||||
|
||||
// Create message string
|
||||
status = napi_create_string_utf8(env, "Test error from C NAPI", NAPI_AUTO_LENGTH, &message);
|
||||
if (status != napi_ok) return NULL;
|
||||
|
||||
// Create an error
|
||||
status = napi_create_error(env, NULL, message, &error);
|
||||
if (status != napi_ok) return NULL;
|
||||
|
||||
// Try to throw it
|
||||
status = napi_throw(env, error);
|
||||
if (status != napi_ok) return NULL;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static napi_value ThrowErrorString(napi_env env, napi_callback_info info) {
|
||||
// Use napi_throw_error directly
|
||||
napi_status status = napi_throw_error(env, NULL, "Test error string from C NAPI");
|
||||
if (status != napi_ok) return NULL;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor desc[] = {
|
||||
{ "throwError", NULL, ThrowError, NULL, NULL, NULL, napi_default, NULL },
|
||||
{ "throwErrorString", NULL, ThrowErrorString, NULL, NULL, NULL, napi_default, NULL },
|
||||
};
|
||||
|
||||
napi_status status = napi_define_properties(env, exports, 2, desc);
|
||||
assert(status == napi_ok);
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
NAPI_MODULE(test_c_version, Init)
|
||||
188
test/napi/napi-throw-exception-segfault/test_comprehensive.cpp
Normal file
188
test/napi/napi-throw-exception-segfault/test_comprehensive.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include <napi.h>
|
||||
#include <iostream>
|
||||
|
||||
// This reproduces issue #4526 more comprehensively using various C++ NAPI patterns
|
||||
|
||||
// Test 1: Direct ThrowAsJavaScriptException (the original issue)
|
||||
Napi::Value DirectThrowException(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing direct ThrowAsJavaScriptException..." << std::endl;
|
||||
|
||||
// This was the problematic line from the original issue
|
||||
Napi::Error::New(env, "Direct throw error message").ThrowAsJavaScriptException();
|
||||
|
||||
// This should never be reached if the exception is thrown properly
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 2: Create error then throw separately
|
||||
Napi::Value CreateThenThrow(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing create then throw..." << std::endl;
|
||||
|
||||
auto error = Napi::Error::New(env, "Created then thrown error");
|
||||
error.ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 3: Multiple exception types
|
||||
Napi::Value ThrowTypeError(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing TypeError throw..." << std::endl;
|
||||
|
||||
Napi::TypeError::New(env, "Type error message").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value ThrowRangeError(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing RangeError throw..." << std::endl;
|
||||
|
||||
Napi::RangeError::New(env, "Range error message").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 4: C++ exception (the workaround from original issue)
|
||||
Napi::Value ThrowCppException(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing C++ throw (workaround)..." << std::endl;
|
||||
|
||||
// This was mentioned as working in the original issue
|
||||
throw Napi::Error::New(env, "C++ thrown error");
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 5: Throw with error code
|
||||
Napi::Value ThrowWithCode(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing throw with error code..." << std::endl;
|
||||
|
||||
auto error = Napi::Error::New(env, "Error with code");
|
||||
error.Set("code", Napi::String::New(env, "TEST_ERROR_CODE"));
|
||||
error.ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 6: Nested function call with exception
|
||||
Napi::Value NestedThrow(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing nested function throw..." << std::endl;
|
||||
|
||||
auto throwError = [&env]() {
|
||||
Napi::Error::New(env, "Nested error").ThrowAsJavaScriptException();
|
||||
};
|
||||
|
||||
throwError();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 7: Exception in callback
|
||||
Napi::Value ThrowInCallback(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing throw in callback..." << std::endl;
|
||||
|
||||
if (info.Length() > 0 && info[0].IsFunction()) {
|
||||
auto callback = info[0].As<Napi::Function>();
|
||||
|
||||
// Call the callback which should trigger an exception
|
||||
try {
|
||||
callback.Call({});
|
||||
} catch (const Napi::Error& e) {
|
||||
// Re-throw using ThrowAsJavaScriptException
|
||||
Napi::Error::New(env, "Callback error: " + std::string(e.Message())).ThrowAsJavaScriptException();
|
||||
}
|
||||
} else {
|
||||
Napi::Error::New(env, "Callback required").ThrowAsJavaScriptException();
|
||||
}
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 8: Multiple rapid throws (stress test)
|
||||
Napi::Value RapidThrows(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing rapid throws..." << std::endl;
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
try {
|
||||
std::string message = "Rapid throw #" + std::to_string(i);
|
||||
Napi::Error::New(env, message).ThrowAsJavaScriptException();
|
||||
} catch (...) {
|
||||
// Continue throwing
|
||||
}
|
||||
}
|
||||
|
||||
// Final throw
|
||||
Napi::Error::New(env, "Final rapid throw").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 9: Empty/null message handling
|
||||
Napi::Value ThrowEmptyMessage(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing empty message throw..." << std::endl;
|
||||
|
||||
Napi::Error::New(env, "").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Test 10: Very long message
|
||||
Napi::Value ThrowLongMessage(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing long message throw..." << std::endl;
|
||||
|
||||
std::string longMessage(10000, 'A');
|
||||
longMessage += " - End of long message";
|
||||
|
||||
Napi::Error::New(env, longMessage).ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
// Initialize the module
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set(Napi::String::New(env, "directThrow"),
|
||||
Napi::Function::New(env, DirectThrowException));
|
||||
exports.Set(Napi::String::New(env, "createThenThrow"),
|
||||
Napi::Function::New(env, CreateThenThrow));
|
||||
exports.Set(Napi::String::New(env, "throwTypeError"),
|
||||
Napi::Function::New(env, ThrowTypeError));
|
||||
exports.Set(Napi::String::New(env, "throwRangeError"),
|
||||
Napi::Function::New(env, ThrowRangeError));
|
||||
exports.Set(Napi::String::New(env, "throwCppException"),
|
||||
Napi::Function::New(env, ThrowCppException));
|
||||
exports.Set(Napi::String::New(env, "throwWithCode"),
|
||||
Napi::Function::New(env, ThrowWithCode));
|
||||
exports.Set(Napi::String::New(env, "nestedThrow"),
|
||||
Napi::Function::New(env, NestedThrow));
|
||||
exports.Set(Napi::String::New(env, "throwInCallback"),
|
||||
Napi::Function::New(env, ThrowInCallback));
|
||||
exports.Set(Napi::String::New(env, "rapidThrows"),
|
||||
Napi::Function::New(env, RapidThrows));
|
||||
exports.Set(Napi::String::New(env, "throwEmptyMessage"),
|
||||
Napi::Function::New(env, ThrowEmptyMessage));
|
||||
exports.Set(Napi::String::New(env, "throwLongMessage"),
|
||||
Napi::Function::New(env, ThrowLongMessage));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(test_comprehensive, Init)
|
||||
147
test/napi/napi-throw-exception-segfault/test_comprehensive.js
Normal file
147
test/napi/napi-throw-exception-segfault/test_comprehensive.js
Normal file
@@ -0,0 +1,147 @@
|
||||
const assert = require('assert');
|
||||
|
||||
// Build and load the comprehensive test module
|
||||
let testModule;
|
||||
try {
|
||||
testModule = require('./build/Release/test_comprehensive.node');
|
||||
} catch (e) {
|
||||
console.error("Failed to load comprehensive test module:", e.message);
|
||||
console.log("Please build with: cp binding_comprehensive.gyp binding.gyp && node-gyp rebuild");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: "Direct ThrowAsJavaScriptException (original issue)",
|
||||
func: testModule.directThrow,
|
||||
expectedMessage: "Direct throw error message"
|
||||
},
|
||||
{
|
||||
name: "Create then throw separately",
|
||||
func: testModule.createThenThrow,
|
||||
expectedMessage: "Created then thrown error"
|
||||
},
|
||||
{
|
||||
name: "TypeError throw",
|
||||
func: testModule.throwTypeError,
|
||||
expectedMessage: "Type error message",
|
||||
expectedType: TypeError
|
||||
},
|
||||
{
|
||||
name: "RangeError throw",
|
||||
func: testModule.throwRangeError,
|
||||
expectedMessage: "Range error message",
|
||||
expectedType: RangeError
|
||||
},
|
||||
{
|
||||
name: "C++ exception (workaround)",
|
||||
func: testModule.throwCppException,
|
||||
expectedMessage: "C++ thrown error"
|
||||
},
|
||||
{
|
||||
name: "Throw with error code",
|
||||
func: testModule.throwWithCode,
|
||||
expectedMessage: "Error with code",
|
||||
expectCode: "TEST_ERROR_CODE"
|
||||
},
|
||||
{
|
||||
name: "Nested function throw",
|
||||
func: testModule.nestedThrow,
|
||||
expectedMessage: "Nested error"
|
||||
},
|
||||
{
|
||||
name: "Throw in callback",
|
||||
func: () => testModule.throwInCallback(() => { throw new Error("Callback error"); }),
|
||||
expectedMessage: /Callback error/
|
||||
},
|
||||
{
|
||||
name: "Rapid throws (stress test)",
|
||||
func: testModule.rapidThrows,
|
||||
expectedMessage: "Final rapid throw"
|
||||
},
|
||||
{
|
||||
name: "Empty message throw",
|
||||
func: testModule.throwEmptyMessage,
|
||||
expectedMessage: ""
|
||||
},
|
||||
{
|
||||
name: "Long message throw",
|
||||
func: testModule.throwLongMessage,
|
||||
expectedMessage: /End of long message$/
|
||||
}
|
||||
];
|
||||
|
||||
let passedTests = 0;
|
||||
let totalTests = tests.length;
|
||||
|
||||
console.log(`Running ${totalTests} comprehensive C++ NAPI exception tests...\n`);
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
console.log(`Testing: ${test.name}`);
|
||||
|
||||
test.func();
|
||||
|
||||
// If we get here, the function didn't throw
|
||||
console.log(`❌ FAILED: ${test.name} - Expected exception but none was thrown`);
|
||||
|
||||
} catch (error) {
|
||||
// Verify the exception properties
|
||||
let passed = true;
|
||||
let details = [];
|
||||
|
||||
// Check error type
|
||||
if (test.expectedType) {
|
||||
if (!(error instanceof test.expectedType)) {
|
||||
passed = false;
|
||||
details.push(`Expected ${test.expectedType.name} but got ${error.constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check error message
|
||||
if (test.expectedMessage instanceof RegExp) {
|
||||
if (!test.expectedMessage.test(error.message)) {
|
||||
passed = false;
|
||||
details.push(`Message doesn't match pattern: got "${error.message}"`);
|
||||
}
|
||||
} else if (typeof test.expectedMessage === 'string') {
|
||||
if (error.message !== test.expectedMessage) {
|
||||
passed = false;
|
||||
details.push(`Expected message "${test.expectedMessage}" but got "${error.message}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check error code if expected
|
||||
if (test.expectCode) {
|
||||
if (error.code !== test.expectCode) {
|
||||
passed = false;
|
||||
details.push(`Expected code "${test.expectCode}" but got "${error.code}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (passed) {
|
||||
console.log(`✅ PASSED: ${test.name}`);
|
||||
passedTests++;
|
||||
} else {
|
||||
console.log(`❌ FAILED: ${test.name}`);
|
||||
details.forEach(detail => console.log(` ${detail}`));
|
||||
}
|
||||
|
||||
console.log(` Caught: ${error.constructor.name}: ${error.message}`);
|
||||
if (error.code) console.log(` Code: ${error.code}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(`\n=== Test Summary ===`);
|
||||
console.log(`Passed: ${passedTests}/${totalTests}`);
|
||||
console.log(`Failed: ${totalTests - passedTests}/${totalTests}`);
|
||||
|
||||
if (passedTests === totalTests) {
|
||||
console.log("🎉 All comprehensive C++ NAPI exception tests passed!");
|
||||
console.log("The original issue #4526 appears to be FIXED ✅");
|
||||
} else {
|
||||
console.log("⚠️ Some tests failed - there may still be issues with NAPI exception handling");
|
||||
process.exit(1);
|
||||
}
|
||||
39
test/napi/napi-throw-exception-segfault/test_rapid.js
Normal file
39
test/napi/napi-throw-exception-segfault/test_rapid.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Test the rapid throws issue found in Bun
|
||||
|
||||
let testModule;
|
||||
try {
|
||||
testModule = require('./build/Release/test_rapid_throws.node');
|
||||
} catch (e) {
|
||||
console.error("Failed to load test module:", e.message);
|
||||
console.log("Please build with: cp binding_rapid.gyp binding.gyp && node-gyp rebuild");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("=== Testing Rapid Throws Issue ===\n");
|
||||
|
||||
// Test 1: Single throw (should work)
|
||||
console.log("Test 1: Single throw");
|
||||
try {
|
||||
testModule.singleThrow();
|
||||
console.log("❌ Expected exception");
|
||||
} catch (e) {
|
||||
console.log("✅ Caught:", e.message);
|
||||
}
|
||||
|
||||
console.log("\nTest 2: Throw after catch");
|
||||
try {
|
||||
testModule.throwAfterCatch();
|
||||
console.log("❌ Expected exception");
|
||||
} catch (e) {
|
||||
console.log("✅ Caught:", e.message);
|
||||
}
|
||||
|
||||
console.log("\nTest 3: Simple rapid throws (this may crash Bun)");
|
||||
try {
|
||||
testModule.simpleRapidThrows();
|
||||
console.log("❌ Expected exception");
|
||||
} catch (e) {
|
||||
console.log("✅ Caught:", e.message);
|
||||
}
|
||||
|
||||
console.log("\n=== Test completed ===");
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <napi.h>
|
||||
#include <iostream>
|
||||
|
||||
// Reproduces a new issue found in Bun's NAPI exception handling
|
||||
// The rapid throws test reveals an assertion failure in JavaScriptCore
|
||||
|
||||
Napi::Value SimpleRapidThrows(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing simple rapid throws..." << std::endl;
|
||||
|
||||
// Try multiple throws in a loop - this causes assertion failure in Bun
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
try {
|
||||
std::string message = "Rapid throw #" + std::to_string(i);
|
||||
std::cout << "Throwing: " << message << std::endl;
|
||||
Napi::Error::New(env, message).ThrowAsJavaScriptException();
|
||||
std::cout << "After throw (should not see this)" << std::endl;
|
||||
} catch (...) {
|
||||
std::cout << "Caught C++ exception for #" << i << std::endl;
|
||||
// Continue to next iteration
|
||||
}
|
||||
}
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value SingleThrow(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing single throw..." << std::endl;
|
||||
Napi::Error::New(env, "Single throw").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value ThrowAfterCatch(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "Testing throw after catch..." << std::endl;
|
||||
|
||||
try {
|
||||
Napi::Error::New(env, "First throw").ThrowAsJavaScriptException();
|
||||
} catch (...) {
|
||||
std::cout << "Caught first exception, throwing second..." << std::endl;
|
||||
}
|
||||
|
||||
// Second throw after catching the first
|
||||
Napi::Error::New(env, "Second throw").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set(Napi::String::New(env, "simpleRapidThrows"),
|
||||
Napi::Function::New(env, SimpleRapidThrows));
|
||||
exports.Set(Napi::String::New(env, "singleThrow"),
|
||||
Napi::Function::New(env, SingleThrow));
|
||||
exports.Set(Napi::String::New(env, "throwAfterCatch"),
|
||||
Napi::Function::New(env, ThrowAfterCatch));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(test_rapid_throws, Init)
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <napi.h>
|
||||
|
||||
// This reproduces the issue from https://github.com/oven-sh/bun/issues/4526
|
||||
// where Napi::Error::New(env, "MESSAGE").ThrowAsJavaScriptException() causes SIGSEGV
|
||||
|
||||
Napi::Value ThrowException(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
// This should cause a SIGSEGV in Bun (but works fine in Node.js)
|
||||
Napi::Error::New(env, "Test error message").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value ThrowExceptionWorkaround(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
// This is the workaround mentioned in the issue - throwing in C++ space
|
||||
throw Napi::Error::New(env, "Test error message");
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set(Napi::String::New(env, "throwException"),
|
||||
Napi::Function::New(env, ThrowException));
|
||||
exports.Set(Napi::String::New(env, "throwExceptionWorkaround"),
|
||||
Napi::Function::New(env, ThrowExceptionWorkaround));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(test_throw_exception_segfault, Init)
|
||||
33
test/napi/napi-throw-exception-segfault/verify_node.js
Normal file
33
test/napi/napi-throw-exception-segfault/verify_node.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Verify that Node.js handles "throw after catch" correctly
|
||||
|
||||
let testModule;
|
||||
try {
|
||||
testModule = require('./build/Release/verify_node_behavior.node');
|
||||
} catch (e) {
|
||||
console.error("Failed to load test module:", e.message);
|
||||
console.log("Please build with: cp binding_verify.gyp binding.gyp && node-gyp rebuild");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("=== Verifying Node.js Behavior ===\n");
|
||||
|
||||
// Test 1: Simple throw (baseline - should work in both Node.js and Bun)
|
||||
console.log("Test 1: Simple throw (baseline)");
|
||||
try {
|
||||
testModule.simpleThrow();
|
||||
console.log("❌ ERROR: Expected exception from simple throw");
|
||||
} catch (e) {
|
||||
console.log("✅ SUCCESS: Caught simple exception:", e.message);
|
||||
}
|
||||
|
||||
console.log("\nTest 2: Throw after catch (the issue)");
|
||||
try {
|
||||
testModule.throwAfterCatchClean();
|
||||
console.log("❌ ERROR: Expected exception from throw after catch");
|
||||
} catch (e) {
|
||||
console.log("✅ SUCCESS: Caught exception after catch:", e.message);
|
||||
}
|
||||
|
||||
console.log("\n=== Node.js Verification Complete ===");
|
||||
console.log("If both tests show SUCCESS, then Node.js handles this correctly");
|
||||
console.log("and the issue is Bun-specific.");
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <napi.h>
|
||||
#include <iostream>
|
||||
|
||||
// Clean test to verify Node.js handles "throw after catch" correctly
|
||||
// This should work fine in Node.js but crash in Bun
|
||||
|
||||
Napi::Value ThrowAfterCatchClean(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "[C++] Starting throw after catch test..." << std::endl;
|
||||
|
||||
try {
|
||||
std::cout << "[C++] Throwing first exception..." << std::endl;
|
||||
Napi::Error::New(env, "First exception").ThrowAsJavaScriptException();
|
||||
std::cout << "[C++] ERROR: Should not reach here after first throw!" << std::endl;
|
||||
} catch (const Napi::Error& e) {
|
||||
std::cout << "[C++] Caught first Napi::Error: " << e.Message() << std::endl;
|
||||
} catch (...) {
|
||||
std::cout << "[C++] Caught first unknown exception" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "[C++] Now throwing second exception..." << std::endl;
|
||||
|
||||
// This second throw should work in Node.js but causes assertion failure in Bun
|
||||
Napi::Error::New(env, "Second exception after catch").ThrowAsJavaScriptException();
|
||||
|
||||
std::cout << "[C++] ERROR: Should not reach here after second throw!" << std::endl;
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value SimpleThrow(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
std::cout << "[C++] Simple throw test..." << std::endl;
|
||||
Napi::Error::New(env, "Simple exception").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set(Napi::String::New(env, "throwAfterCatchClean"),
|
||||
Napi::Function::New(env, ThrowAfterCatchClean));
|
||||
exports.Set(Napi::String::New(env, "simpleThrow"),
|
||||
Napi::Function::New(env, SimpleThrow));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(verify_node_behavior, Init)
|
||||
119
test/regression/issue/napi-exception-after-catch.test.ts
Normal file
119
test/regression/issue/napi-exception-after-catch.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunExe } from "harness";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
test("NAPI exception after catch should not cause assertion failure", async () => {
|
||||
// Create a temporary directory for our test
|
||||
const testDir = join(tmpdir(), "napi-exception-after-catch-test");
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
|
||||
// Create package.json
|
||||
writeFileSync(join(testDir, "package.json"), JSON.stringify({
|
||||
name: "napi-exception-after-catch-test",
|
||||
version: "1.0.0",
|
||||
gypfile: true
|
||||
}));
|
||||
|
||||
// Create C++ source file
|
||||
const cppSource = `
|
||||
#include <napi.h>
|
||||
|
||||
Napi::Value ThrowAfterCatch(const Napi::CallbackInfo& info) {
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
try {
|
||||
Napi::Error::New(env, "First throw").ThrowAsJavaScriptException();
|
||||
} catch (...) {
|
||||
// Caught the first exception in C++
|
||||
}
|
||||
|
||||
// This second throw causes assertion failure in Bun
|
||||
Napi::Error::New(env, "Second throw").ThrowAsJavaScriptException();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set(Napi::String::New(env, "throwAfterCatch"),
|
||||
Napi::Function::New(env, ThrowAfterCatch));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(test_module, Init)
|
||||
`;
|
||||
|
||||
writeFileSync(join(testDir, "test.cpp"), cppSource);
|
||||
|
||||
// Create binding.gyp
|
||||
const bindingGyp = `
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_module",
|
||||
"sources": ["test.cpp"],
|
||||
"include_dirs": ["<!@(node -p \\"require('node-addon-api').include\\")"],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(join(testDir, "binding.gyp"), bindingGyp);
|
||||
|
||||
// Create test JavaScript file
|
||||
const jsTest = `
|
||||
const testModule = require('./build/Release/test_module.node');
|
||||
|
||||
try {
|
||||
testModule.throwAfterCatch();
|
||||
console.log("ERROR: Expected exception to be thrown");
|
||||
process.exit(1);
|
||||
} catch (e) {
|
||||
console.log("SUCCESS: Caught exception:", e.message);
|
||||
process.exit(0);
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(join(testDir, "test.js"), jsTest);
|
||||
|
||||
// Install node-addon-api and build the module
|
||||
await using proc1 = Bun.spawn({
|
||||
cmd: ["npm", "install", "node-addon-api"],
|
||||
cwd: testDir,
|
||||
});
|
||||
await proc1.exited;
|
||||
|
||||
await using proc2 = Bun.spawn({
|
||||
cmd: ["node-gyp", "configure", "build"],
|
||||
cwd: testDir,
|
||||
});
|
||||
await proc2.exited;
|
||||
|
||||
// Test with Bun - this should not crash
|
||||
await using proc3 = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
cwd: testDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe"
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc3.stdout.text(),
|
||||
proc3.stderr.text(),
|
||||
proc3.exited
|
||||
]);
|
||||
|
||||
// The bug causes an assertion failure (abort), so we expect exit code 134 or similar
|
||||
// When the bug is fixed, we expect exit code 0 and "SUCCESS" message
|
||||
console.log("stdout:", stdout);
|
||||
console.log("stderr:", stderr);
|
||||
console.log("exitCode:", exitCode);
|
||||
|
||||
// For now, we expect this to fail with assertion error until the bug is fixed
|
||||
// When this test starts passing, the bug has been fixed!
|
||||
expect(exitCode).toBe(0); // This will fail until the bug is fixed
|
||||
expect(stdout).toContain("SUCCESS: Caught exception: Second throw");
|
||||
}, 30000);
|
||||
Reference in New Issue
Block a user