diff --git a/test/src/d1.rs b/test/src/d1.rs index 519deeb91..950276d11 100644 --- a/test/src/d1.rs +++ b/test/src/d1.rs @@ -174,6 +174,32 @@ pub async fn session_bookmark_roundtrip( Response::ok("ok") } +#[worker::send] +pub async fn blob_roundtrip(_req: Request, env: Env, _data: SomeSharedData) -> Result { + let db = env.d1("DB")?; + + db.exec("CREATE TABLE IF NOT EXISTS blobs (id INTEGER PRIMARY KEY, data BLOB NOT NULL);") + .await?; + + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0xfd, 0xfe, 0xff]; + let insert = worker::query!(&db, "INSERT OR REPLACE INTO blobs (id, data) VALUES (1, ?)"); + insert.bind_refs(&D1Type::Blob(bytes))?.run().await?; + + // Verify the bound value arrived in SQLite byte-for-byte. + let stmt = worker::query!(&db, "SELECT hex(data) AS h FROM blobs WHERE id = 1"); + let hex = stmt.first::(Some("h")).await?.unwrap(); + assert_eq!(hex, "000102FDFEFF"); + + let insert = worker::query!(&db, "INSERT OR REPLACE INTO blobs (id, data) VALUES (2, ?)"); + insert.bind_refs(&D1Type::Blob(&[]))?.run().await?; + + let stmt = worker::query!(&db, "SELECT length(data) AS l FROM blobs WHERE id = 2"); + let len = stmt.first::(Some("l")).await?.unwrap(); + assert_eq!(len, 0); + + Response::ok("ok") +} + #[worker::send] pub async fn exec(mut req: Request, env: Env, _data: SomeSharedData) -> Result { let db = env.d1("DB")?; diff --git a/test/src/router.rs b/test/src/router.rs index b5a4b56b2..36e203181 100644 --- a/test/src/router.rs +++ b/test/src/router.rs @@ -198,6 +198,7 @@ macro_rules! add_routes ( add_route!($obj, get, "/d1/retrieve_optional_none", d1::retrieve_optional_none); add_route!($obj, get, "/d1/retrieve_optional_some", d1::retrieve_optional_some); add_route!($obj, get, "/d1/retrive_first_none", d1::retrive_first_none); + add_route!($obj, get, "/d1/blob_roundtrip", d1::blob_roundtrip); add_route!($obj, get, "/kv/get", kv::get); add_route!($obj, get, "/kv/get-not-found", kv::get_not_found); add_route!($obj, get, "/kv/list-keys", kv::list_keys); diff --git a/test/tests/d1.spec.ts b/test/tests/d1.spec.ts index a6b23ddf2..36305a6c4 100644 --- a/test/tests/d1.spec.ts +++ b/test/tests/d1.spec.ts @@ -90,6 +90,12 @@ describe("d1", () => { expect(await exec(query)).toBe(1); }); + test("blob roundtrip", async () => { + const resp = await mf.dispatchFetch(`${mfUrl}d1/blob_roundtrip`); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + test("jsvalue_null_is_null", async () => { const resp = await mf.dispatchFetch(`${mfUrl}d1/jsvalue_null_is_null`); expect(await resp.text()).toBe("ok"); diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs index 47799065f..ae360f48b 100644 --- a/worker/src/d1/mod.rs +++ b/worker/src/d1/mod.rs @@ -286,7 +286,10 @@ impl<'a> From<&'a D1Type<'a>> for JsValue { D1Type::Integer(i) => JsValue::from_f64(i as f64), D1Type::Text(s) => JsValue::from_str(s), D1Type::Boolean(b) => JsValue::from_bool(b), - D1Type::Blob(a) => serde_wasm_bindgen::to_value(a).unwrap(), + // D1's documented BLOB representation is an `ArrayBuffer`; copying + // through a `Uint8Array` crosses the JS boundary once instead of + // serializing element-by-element into a number array. + D1Type::Blob(a) => js_sys::Uint8Array::from(a).buffer().into(), } } } diff --git a/worker/src/http/body.rs b/worker/src/http/body.rs index 2c102096d..148f03194 100644 --- a/worker/src/http/body.rs +++ b/worker/src/http/body.rs @@ -74,7 +74,7 @@ impl HttpBody for Body { stream .poll_next_unpin(cx) .map_ok(|buf| { - let bytes = Bytes::copy_from_slice(&js_sys::Uint8Array::from(buf).to_vec()); + let bytes = Bytes::from(js_sys::Uint8Array::from(buf).to_vec()); Frame::data(bytes) }) .map_err(Error::Internal) diff --git a/worker/src/response.rs b/worker/src/response.rs index 4ffac586b..7a5e7fb05 100644 --- a/worker/src/response.rs +++ b/worker/src/response.rs @@ -175,7 +175,10 @@ impl Response { /// Access this response's body encoded as JSON. pub async fn json(&mut self) -> Result { - serde_json::from_str(&self.text().await?).map_err(Error::from) + if let ResponseBody::Body(bytes) = &self.body { + return serde_json::from_slice(bytes).map_err(Error::from); + } + serde_json::from_slice(&self.bytes().await?).map_err(Error::from) } /// Access this response's body encoded as raw bytes. @@ -304,6 +307,15 @@ impl Response { return Err(Error::RustError("WebSockets cannot be cloned".into())); } + // Only stream bodies need the JS `Response.clone()` to tee the stream; + // fixed and empty bodies can be duplicated on the Rust side. + if !matches!(self.body, ResponseBody::Stream(_)) { + return Ok(Self { + body: self.body.clone(), + init: self.init.clone(), + }); + } + let edge = web_sys::Response::from(&*self); let cloned = edge.clone()?; diff --git a/worker/src/sql.rs b/worker/src/sql.rs index 50c875a9e..c71605f0c 100644 --- a/worker/src/sql.rs +++ b/worker/src/sql.rs @@ -265,20 +265,12 @@ where type Item = Result; fn next(&mut self) -> Option { - let result = self.cursor.inner.next(); + let result: js_sys::IteratorNext = self.cursor.inner.next().unchecked_into(); - let done = js_sys::Reflect::get(&result, &JsValue::from("done")) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - if done { + if result.done() { None } else { - let value = js_sys::Reflect::get(&result, &JsValue::from("value")) - .map_err(Error::from) - .and_then(|js_val| swb::from_value(js_val).map_err(Error::from)); - Some(value) + Some(swb::from_value(result.value()).map_err(Error::from)) } } } @@ -313,7 +305,9 @@ impl Iterator for SqlCursorRawIterator { } fn js_array_to_sql_storage_values(js_val: JsValue) -> Result> { - let array = js_sys::Array::from(&js_val); + let array: js_sys::Array = js_val + .dyn_into() + .map_err(|_| Error::from("Expected an array of SQL values"))?; let mut values = Vec::with_capacity(array.length() as usize); for i in 0..array.length() { @@ -396,20 +390,12 @@ impl Iterator for SqlCursor { type Item = Result; fn next(&mut self) -> Option { - let result = self.inner.next(); - - // Extract 'done' property from iterator result - let done = js_sys::Reflect::get(&result, &JsValue::from("done")) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(true); + let result: js_sys::IteratorNext = self.inner.next().unchecked_into(); - if done { + if result.done() { None } else { - // Extract 'value' property from iterator result - let value = js_sys::Reflect::get(&result, &JsValue::from("value")).map_err(Error::from); - Some(value) + Some(Ok(result.value())) } } }