script/dom/bindings/
error.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Utilities to throw exceptions from Rust bindings.
6
7use std::ptr::NonNull;
8use std::slice::from_raw_parts;
9
10#[cfg(feature = "js_backtrace")]
11use backtrace::Backtrace;
12use embedder_traits::JavaScriptErrorInfo;
13use js::conversions::jsstr_to_string;
14use js::error::{throw_range_error, throw_type_error};
15#[cfg(feature = "js_backtrace")]
16use js::jsapi::StackFormat as JSStackFormat;
17use js::jsapi::{
18    ExceptionStackBehavior, JS_ClearPendingException, JS_GetProperty, JS_IsExceptionPending,
19};
20use js::jsval::UndefinedValue;
21use js::rust::wrappers::{JS_ErrorFromException, JS_GetPendingException, JS_SetPendingException};
22use js::rust::{HandleObject, HandleValue, MutableHandleValue, describe_scripted_caller};
23use libc::c_uint;
24use script_bindings::conversions::SafeToJSValConvertible;
25pub(crate) use script_bindings::error::*;
26use script_bindings::root::DomRoot;
27use script_bindings::str::DOMString;
28
29#[cfg(feature = "js_backtrace")]
30use crate::dom::bindings::cell::DomRefCell;
31use crate::dom::bindings::conversions::{
32    ConversionResult, SafeFromJSValConvertible, root_from_object,
33};
34use crate::dom::bindings::str::USVString;
35use crate::dom::domexception::{DOMErrorName, DOMException};
36use crate::dom::globalscope::GlobalScope;
37use crate::dom::types::QuotaExceededError;
38use crate::realms::InRealm;
39use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
40
41#[cfg(feature = "js_backtrace")]
42thread_local! {
43    /// An optional stringified JS backtrace and stringified native backtrace from the
44    /// the last DOM exception that was reported.
45    static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None);
46}
47
48/// Error values that have no equivalent DOMException representation.
49pub(crate) enum JsEngineError {
50    /// An EMCAScript TypeError.
51    Type(String),
52    /// An ECMAScript RangeError.
53    Range(String),
54    /// The JS engine reported a thrown exception.
55    JSFailed,
56}
57
58/// Set a pending exception for the given `result` on `cx`.
59pub(crate) fn throw_dom_exception(
60    cx: SafeJSContext,
61    global: &GlobalScope,
62    result: Error,
63    can_gc: CanGc,
64) {
65    #[cfg(feature = "js_backtrace")]
66    unsafe {
67        capture_stack!(in(*cx) let stack);
68        let js_stack = stack.and_then(|s| s.as_string(None, JSStackFormat::Default));
69        let rust_stack = Backtrace::new();
70        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
71            *backtrace.borrow_mut() = Some((js_stack, format!("{:?}", rust_stack)));
72        });
73    }
74
75    match create_dom_exception(global, result, can_gc) {
76        Ok(exception) => unsafe {
77            assert!(!JS_IsExceptionPending(*cx));
78            rooted!(in(*cx) let mut thrown = UndefinedValue());
79            exception.safe_to_jsval(cx, thrown.handle_mut(), can_gc);
80            JS_SetPendingException(*cx, thrown.handle(), ExceptionStackBehavior::Capture);
81        },
82
83        Err(JsEngineError::Type(message)) => unsafe {
84            assert!(!JS_IsExceptionPending(*cx));
85            throw_type_error(*cx, &message);
86        },
87
88        Err(JsEngineError::Range(message)) => unsafe {
89            assert!(!JS_IsExceptionPending(*cx));
90            throw_range_error(*cx, &message);
91        },
92
93        Err(JsEngineError::JSFailed) => unsafe {
94            assert!(JS_IsExceptionPending(*cx));
95        },
96    }
97}
98
99/// If possible, create a new DOMException representing the provided error.
100/// If no such DOMException exists, return a subset of the original error values
101/// that may need additional handling.
102pub(crate) fn create_dom_exception(
103    global: &GlobalScope,
104    result: Error,
105    can_gc: CanGc,
106) -> Result<DomRoot<DOMException>, JsEngineError> {
107    let new_custom_exception = |error_name, message| {
108        Ok(DOMException::new_with_custom_message(
109            global, error_name, message, can_gc,
110        ))
111    };
112
113    let code = match result {
114        Error::IndexSize(Some(custom_message)) => {
115            return new_custom_exception(DOMErrorName::IndexSizeError, custom_message);
116        },
117        Error::IndexSize(None) => DOMErrorName::IndexSizeError,
118        Error::NotFound(Some(custom_message)) => {
119            return new_custom_exception(DOMErrorName::NotFoundError, custom_message);
120        },
121        Error::NotFound(None) => DOMErrorName::NotFoundError,
122        Error::HierarchyRequest(Some(custom_message)) => {
123            return new_custom_exception(DOMErrorName::HierarchyRequestError, custom_message);
124        },
125        Error::HierarchyRequest(None) => DOMErrorName::HierarchyRequestError,
126        Error::WrongDocument(Some(doc_err_custom_message)) => {
127            return new_custom_exception(DOMErrorName::WrongDocumentError, doc_err_custom_message);
128        },
129        Error::WrongDocument(None) => DOMErrorName::WrongDocumentError,
130        Error::InvalidCharacter(Some(custom_message)) => {
131            return new_custom_exception(DOMErrorName::InvalidCharacterError, custom_message);
132        },
133        Error::InvalidCharacter(None) => DOMErrorName::InvalidCharacterError,
134        Error::NotSupported(Some(custom_message)) => {
135            return new_custom_exception(DOMErrorName::NotSupportedError, custom_message);
136        },
137        Error::NotSupported(None) => DOMErrorName::NotSupportedError,
138        Error::InUseAttribute(Some(custom_message)) => {
139            return new_custom_exception(DOMErrorName::InUseAttributeError, custom_message);
140        },
141        Error::InUseAttribute(None) => DOMErrorName::InUseAttributeError,
142        Error::InvalidState(Some(custom_message)) => {
143            return new_custom_exception(DOMErrorName::InvalidStateError, custom_message);
144        },
145        Error::InvalidState(None) => DOMErrorName::InvalidStateError,
146        Error::Syntax(Some(custom_message)) => {
147            return new_custom_exception(DOMErrorName::SyntaxError, custom_message);
148        },
149        Error::Syntax(None) => DOMErrorName::SyntaxError,
150        Error::Namespace(Some(custom_message)) => {
151            return new_custom_exception(DOMErrorName::NamespaceError, custom_message);
152        },
153        Error::Namespace(None) => DOMErrorName::NamespaceError,
154        Error::InvalidAccess(Some(custom_message)) => {
155            return new_custom_exception(DOMErrorName::InvalidAccessError, custom_message);
156        },
157        Error::InvalidAccess(None) => DOMErrorName::InvalidAccessError,
158        Error::Security(Some(custom_message)) => {
159            return new_custom_exception(DOMErrorName::SecurityError, custom_message);
160        },
161        Error::Security(None) => DOMErrorName::SecurityError,
162        Error::Network(Some(custom_message)) => {
163            return new_custom_exception(DOMErrorName::NetworkError, custom_message);
164        },
165        Error::Network(None) => DOMErrorName::NetworkError,
166        Error::Abort(Some(custom_message)) => {
167            return new_custom_exception(DOMErrorName::AbortError, custom_message);
168        },
169        Error::Abort(None) => DOMErrorName::AbortError,
170        Error::Timeout(Some(custom_message)) => {
171            return new_custom_exception(DOMErrorName::TimeoutError, custom_message);
172        },
173        Error::Timeout(None) => DOMErrorName::TimeoutError,
174        Error::InvalidNodeType(Some(custom_message)) => {
175            return new_custom_exception(DOMErrorName::InvalidNodeTypeError, custom_message);
176        },
177        Error::InvalidNodeType(None) => DOMErrorName::InvalidNodeTypeError,
178        Error::DataClone(Some(custom_message)) => {
179            return new_custom_exception(DOMErrorName::DataCloneError, custom_message);
180        },
181        Error::DataClone(None) => DOMErrorName::DataCloneError,
182        Error::Data(Some(custom_message)) => {
183            return new_custom_exception(DOMErrorName::DataError, custom_message);
184        },
185        Error::Data(None) => DOMErrorName::DataError,
186        Error::TransactionInactive(Some(custom_message)) => {
187            return new_custom_exception(DOMErrorName::TransactionInactiveError, custom_message);
188        },
189        Error::TransactionInactive(None) => DOMErrorName::TransactionInactiveError,
190        Error::ReadOnly(Some(custom_message)) => {
191            return new_custom_exception(DOMErrorName::ReadOnlyError, custom_message);
192        },
193        Error::ReadOnly(None) => DOMErrorName::ReadOnlyError,
194        Error::Version(Some(custom_message)) => {
195            return new_custom_exception(DOMErrorName::VersionError, custom_message);
196        },
197        Error::Version(None) => DOMErrorName::VersionError,
198        Error::NoModificationAllowed(Some(custom_message)) => {
199            return new_custom_exception(DOMErrorName::NoModificationAllowedError, custom_message);
200        },
201        Error::NoModificationAllowed(None) => DOMErrorName::NoModificationAllowedError,
202        Error::QuotaExceeded { quota, requested } => {
203            return Ok(DomRoot::upcast(QuotaExceededError::new(
204                global,
205                DOMString::new(),
206                quota,
207                requested,
208                can_gc,
209            )));
210        },
211        Error::TypeMismatch(Some(custom_message)) => {
212            return new_custom_exception(DOMErrorName::TypeMismatchError, custom_message);
213        },
214        Error::TypeMismatch(None) => DOMErrorName::TypeMismatchError,
215        Error::InvalidModification(Some(custom_message)) => {
216            return new_custom_exception(DOMErrorName::InvalidModificationError, custom_message);
217        },
218        Error::InvalidModification(None) => DOMErrorName::InvalidModificationError,
219        Error::NotReadable(Some(custom_message)) => {
220            return new_custom_exception(DOMErrorName::NotReadableError, custom_message);
221        },
222        Error::NotReadable(None) => DOMErrorName::NotReadableError,
223        Error::Operation(Some(custom_message)) => {
224            return new_custom_exception(DOMErrorName::OperationError, custom_message);
225        },
226        Error::Operation(None) => DOMErrorName::OperationError,
227        Error::NotAllowed(Some(custom_message)) => {
228            return new_custom_exception(DOMErrorName::NotAllowedError, custom_message);
229        },
230        Error::NotAllowed(None) => DOMErrorName::NotAllowedError,
231        Error::Encoding(Some(custom_message)) => {
232            return new_custom_exception(DOMErrorName::EncodingError, custom_message);
233        },
234        Error::Encoding(None) => DOMErrorName::EncodingError,
235        Error::Constraint(Some(custom_message)) => {
236            return new_custom_exception(DOMErrorName::ConstraintError, custom_message);
237        },
238        Error::Constraint(None) => DOMErrorName::ConstraintError,
239        Error::Type(message) => return Err(JsEngineError::Type(message)),
240        Error::Range(message) => return Err(JsEngineError::Range(message)),
241        Error::JSFailed => return Err(JsEngineError::JSFailed),
242    };
243    Ok(DOMException::new(global, code, can_gc))
244}
245
246/// A struct encapsulating information about a runtime script error.
247#[derive(Default)]
248pub(crate) struct ErrorInfo {
249    /// The error message.
250    pub(crate) message: String,
251    /// The file name.
252    pub(crate) filename: String,
253    /// The line number.
254    pub(crate) lineno: c_uint,
255    /// The column number.
256    pub(crate) column: c_uint,
257}
258
259impl ErrorInfo {
260    fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
261        let report = unsafe { JS_ErrorFromException(*cx, object) };
262        if report.is_null() {
263            return None;
264        }
265
266        let filename = {
267            let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
268            if !filename.is_null() {
269                let filename = unsafe {
270                    let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
271                    from_raw_parts(filename, length as usize)
272                };
273                String::from_utf8_lossy(filename).into_owned()
274            } else {
275                "none".to_string()
276            }
277        };
278
279        let lineno = unsafe { (*report)._base.lineno };
280        let column = unsafe { (*report)._base.column._base };
281
282        let message = {
283            let message = unsafe { (*report)._base.message_.data_ as *const u8 };
284            let message = unsafe {
285                let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
286                from_raw_parts(message, length as usize)
287            };
288            String::from_utf8_lossy(message).into_owned()
289        };
290
291        Some(ErrorInfo {
292            filename,
293            message,
294            lineno,
295            column,
296        })
297    }
298
299    fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
300        let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? };
301        let scripted_caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
302        Some(ErrorInfo {
303            message: exception.stringifier().into(),
304            filename: scripted_caller.filename,
305            lineno: scripted_caller.line,
306            column: scripted_caller.col + 1,
307        })
308    }
309
310    fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
311        if let Some(info) = ErrorInfo::from_native_error(object, cx) {
312            return Some(info);
313        }
314        if let Some(info) = ErrorInfo::from_dom_exception(object, cx) {
315            return Some(info);
316        }
317        None
318    }
319
320    /// <https://html.spec.whatwg.org/multipage/#extract-error>
321    pub(crate) fn from_value(value: HandleValue, cx: SafeJSContext, can_gc: CanGc) -> ErrorInfo {
322        if value.is_object() {
323            rooted!(in(*cx) let object = value.to_object());
324            if let Some(info) = ErrorInfo::from_object(object.handle(), cx) {
325                return info;
326            }
327        }
328
329        match USVString::safe_from_jsval(cx, value, (), can_gc) {
330            Ok(ConversionResult::Success(USVString(string))) => {
331                let scripted_caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
332                ErrorInfo {
333                    message: format!("uncaught exception: {}", string),
334                    filename: scripted_caller.filename,
335                    lineno: scripted_caller.line,
336                    column: scripted_caller.col + 1,
337                }
338            },
339            _ => {
340                panic!("uncaught exception: failed to stringify primitive");
341            },
342        }
343    }
344}
345
346/// Report a pending exception, thereby clearing it.
347///
348/// The `dispatch_event` argument is temporary and non-standard; passing false
349/// prevents dispatching the `error` event.
350pub(crate) fn report_pending_exception(
351    cx: SafeJSContext,
352    dispatch_event: bool,
353    realm: InRealm,
354    can_gc: CanGc,
355) {
356    rooted!(in(*cx) let mut value = UndefinedValue());
357    if take_pending_exception(cx, value.handle_mut()) {
358        let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
359        report_error(
360            error_info,
361            value.handle(),
362            cx,
363            dispatch_event,
364            realm,
365            can_gc,
366        );
367    }
368}
369
370fn take_pending_exception(cx: SafeJSContext, value: MutableHandleValue) -> bool {
371    unsafe {
372        if !JS_IsExceptionPending(*cx) {
373            return false;
374        }
375    }
376
377    unsafe {
378        if !JS_GetPendingException(*cx, value) {
379            JS_ClearPendingException(*cx);
380            error!("Uncaught exception: JS_GetPendingException failed");
381            return false;
382        }
383
384        JS_ClearPendingException(*cx);
385    }
386    true
387}
388
389fn report_error(
390    error_info: ErrorInfo,
391    value: HandleValue,
392    cx: SafeJSContext,
393    dispatch_event: bool,
394    realm: InRealm,
395    can_gc: CanGc,
396) {
397    error!(
398        "Error at {}:{}:{} {}",
399        error_info.filename, error_info.lineno, error_info.column, error_info.message
400    );
401
402    #[cfg(feature = "js_backtrace")]
403    {
404        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
405            if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
406                if let Some(stack) = js_backtrace {
407                    error!("JS backtrace:\n{}", stack);
408                }
409                error!("Rust backtrace:\n{}", rust_backtrace);
410            }
411        });
412    }
413
414    if dispatch_event {
415        GlobalScope::from_safe_context(cx, realm).report_an_error(error_info, value, can_gc);
416    }
417}
418
419pub(crate) fn javascript_error_info_from_error_info(
420    cx: SafeJSContext,
421    error_info: &ErrorInfo,
422    value: HandleValue,
423    _: CanGc,
424) -> JavaScriptErrorInfo {
425    let stack = || {
426        if !value.is_object() {
427            return None;
428        }
429
430        rooted!(in(*cx) let object = value.to_object());
431        rooted!(in(*cx) let mut stack_value = UndefinedValue());
432        if unsafe {
433            !JS_GetProperty(
434                *cx,
435                object.handle().into(),
436                c"stack".as_ptr(),
437                stack_value.handle_mut().into(),
438            )
439        } {
440            return None;
441        }
442        if !stack_value.is_string() {
443            return None;
444        }
445        let stack_string = NonNull::new(stack_value.to_string())?;
446        Some(unsafe { jsstr_to_string(*cx, stack_string) })
447    };
448
449    JavaScriptErrorInfo {
450        message: error_info.message.clone(),
451        filename: error_info.filename.clone(),
452        line_number: error_info.lineno as u64,
453        column: error_info.column as u64,
454        stack: stack(),
455    }
456}
457
458pub(crate) fn take_and_report_pending_exception_for_api(
459    cx: SafeJSContext,
460    realm: InRealm,
461    can_gc: CanGc,
462) -> Option<JavaScriptErrorInfo> {
463    rooted!(in(*cx) let mut value = UndefinedValue());
464    if !take_pending_exception(cx, value.handle_mut()) {
465        return None;
466    }
467
468    let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
469    let return_value =
470        javascript_error_info_from_error_info(cx, &error_info, value.handle(), can_gc);
471    report_error(
472        error_info,
473        value.handle(),
474        cx,
475        true, /* dispatch_event */
476        realm,
477        can_gc,
478    );
479    Some(return_value)
480}
481
482pub(crate) trait ErrorToJsval {
483    fn to_jsval(
484        self,
485        cx: SafeJSContext,
486        global: &GlobalScope,
487        rval: MutableHandleValue,
488        can_gc: CanGc,
489    );
490}
491
492impl ErrorToJsval for Error {
493    /// Convert this error value to a JS value, consuming it in the process.
494    fn to_jsval(
495        self,
496        cx: SafeJSContext,
497        global: &GlobalScope,
498        rval: MutableHandleValue,
499        can_gc: CanGc,
500    ) {
501        match self {
502            Error::JSFailed => (),
503            _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) },
504        }
505        throw_dom_exception(cx, global, self, can_gc);
506        unsafe {
507            assert!(JS_IsExceptionPending(*cx));
508            assert!(JS_GetPendingException(*cx, rval));
509            JS_ClearPendingException(*cx);
510        }
511    }
512}