diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js
index 508fd6d..23cefef 100644
--- a/crates/obscura-js/js/bootstrap.js
+++ b/crates/obscura-js/js/bootstrap.js
@@ -7398,21 +7398,28 @@ if (typeof Image === 'undefined') {
// `.src` flips `complete` and fires `load` on a microtask-later tick. Lazy
// loaders and preloaders that create `new Image()`, set `.src`, and wait for
// `onload` (or addEventListener('load')) would hang forever otherwise.
- Object.defineProperty(img, 'src', {
- configurable: true, enumerable: true,
- get() { return _imgSrcDesc.get.call(img); },
- set(v) {
- _imgSrcDesc.set.call(img, v);
- if (!img.getAttribute('src')) return;
- img.complete = false;
- setTimeout(function () {
- img.complete = true;
- img.naturalWidth = img.naturalWidth || img.width || 0;
- img.naturalHeight = img.naturalHeight || img.height || 0;
- try { img.dispatchEvent(new Event('load')); } catch (e) {}
- }, 0);
- },
- });
+ // Anti-bot scripts (Booking.com, issue #394) pre-define a non-configurable
+ // own `src` on elements; redefining it throws "Cannot redefine
+ // property: src" and kills the constructor. Skip the load emulation then:
+ // a page that owns `src` is instrumenting loads itself.
+ const ownSrc = Object.getOwnPropertyDescriptor(img, 'src');
+ if (!ownSrc || ownSrc.configurable) {
+ Object.defineProperty(img, 'src', {
+ configurable: true, enumerable: true,
+ get() { return _imgSrcDesc.get.call(img); },
+ set(v) {
+ _imgSrcDesc.set.call(img, v);
+ if (!img.getAttribute('src')) return;
+ img.complete = false;
+ setTimeout(function () {
+ img.complete = true;
+ img.naturalWidth = img.naturalWidth || img.width || 0;
+ img.naturalHeight = img.naturalHeight || img.height || 0;
+ try { img.dispatchEvent(new Event('load')); } catch (e) {}
+ }, 0);
+ },
+ });
+ }
return img;
};
globalThis.Image.prototype = globalThis.HTMLImageElement.prototype;
diff --git a/crates/obscura/tests/image_shim.rs b/crates/obscura/tests/image_shim.rs
new file mode 100644
index 0000000..5c7b14e
--- /dev/null
+++ b/crates/obscura/tests/image_shim.rs
@@ -0,0 +1,102 @@
+//! Regression test for issue #394 (crash half): `new Image()` must survive a
+//! page that pre-defines a non-configurable own `src` on `
` elements, the
+//! way Booking.com's anti-bot instrumentation does. The Image shim used to
+//! unconditionally redefine `src` on the element it just created, throwing
+//! `TypeError: Cannot redefine property: src`.
+
+use std::io::{Read, Write};
+
+use obscura::Browser;
+
+/// Minimal HTTP/1.1 server returning HTML that wraps document.createElement to
+/// plant a non-configurable `src` on every
, then calls `new Image()`.
+fn spawn_server(html: &'static str) -> String {
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+ std::thread::spawn(move || {
+ for incoming in listener.incoming() {
+ let mut s = match incoming {
+ Ok(s) => s,
+ Err(_) => continue,
+ };
+ let mut buf = [0u8; 2048];
+ let _ = s.read(&mut buf);
+ let resp = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ html.len(),
+ html
+ );
+ let _ = s.write_all(resp.as_bytes());
+ let _ = s.shutdown(std::net::Shutdown::Both);
+ }
+ });
+ format!("http://{}", addr)
+}
+
+#[tokio::test]
+async fn new_image_survives_non_configurable_src() {
+ std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
+ let base = spawn_server(
+ r#"