Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/javy/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- `to_string_lossy` no longer leaks the engine-allocated copy of the string it
converts, and no longer constructs a slice from a null pointer when that
allocation fails.
- `JSON.stringify` with `simd_json_builtins` enabled now propagates an
exception thrown while looking up the top-level value's `toJSON` property
(an accessor or proxy trap that throws) instead of dropping it and
continuing.

## [8.1.0] - 2026-07-30

Expand Down
26 changes: 9 additions & 17 deletions crates/javy/src/apis/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use crate::{
atom::PredefinedAtom,
function::This,
prelude::{MutFn, Rest},
qjs::JS_GetProperty,
},
to_js_error, val_to_string,
};
Expand Down Expand Up @@ -137,7 +136,7 @@ fn call_json_stringify(args: Args<'_>) -> Result<Value<'_>> {
1 => {
let arg = args[0].clone();
let val: Value = if arg.is_object() {
if let Some(f) = get_to_json(&arg) {
if let Some(f) = get_to_json(&arg)? {
f.call((
This(arg.clone()),
JSString::from_str(arg.ctx().clone(), "")?.into_value(),
Expand Down Expand Up @@ -172,20 +171,13 @@ fn call_json_stringify(args: Args<'_>) -> Result<Value<'_>> {
}
}

fn get_to_json<'a>(value: &Value<'a>) -> Option<Function<'a>> {
let f = unsafe {
JS_GetProperty(
value.ctx().as_raw().as_ptr(),
value.as_raw(),
PredefinedAtom::ToJSON as u32,
)
/// Looks up the `toJSON` property of an object, propagating any exception
/// thrown by a getter or proxy trap during the lookup.
fn get_to_json<'a>(value: &Value<'a>) -> Result<Option<Function<'a>>> {
let Some(object) = value.as_object() else {
return Ok(None);
};
let f = unsafe { Value::from_raw(value.ctx().clone(), f) };
if f.is_function()
&& let Some(f) = f.into_function()
{
Some(f)
} else {
None
}
Ok(object
.get::<_, Value<'a>>(PredefinedAtom::ToJSON)?
.into_function())
}
19 changes: 19 additions & 0 deletions crates/javy/tests/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,22 @@ fn to_string_lossy_frees_engine_cstring() -> anyhow::Result<()> {
);
Ok(())
}

#[cfg(feature = "json")]
#[test]
fn json_stringify_propagates_to_json_lookup_errors() -> Result<()> {
let mut config = Config::default();
config.simd_json_builtins(true);

let source = include_bytes!("stringify_tojson_throws.js");
let rt = Runtime::new(config)?;

rt.context().with(|this| {
let _: () = this
.eval_with_options(*source, EvalOptions::default())
.inspect_err(|e| println!("{e}"))
.expect("source evaluation to succeed");
});

Ok(())
}
33 changes: 33 additions & 0 deletions crates/javy/tests/stringify_tojson_throws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// An exception thrown while looking up `toJSON` must propagate out of
// `JSON.stringify` instead of being dropped.
const boom = new Error("boom");
const values = [
{
get toJSON() {
throw boom;
},
},
new Proxy(
{ a: 1 },
{
get(target, key) {
if (key === "toJSON") {
throw boom;
}
return Reflect.get(target, key);
},
}
),
];

for (const value of values) {
let thrown;
try {
JSON.stringify(value);
} catch (e) {
thrown = e;
}
if (thrown !== boom) {
throw new Error(`expected JSON.stringify to throw, got ${thrown}`);
}
}
Loading