Repository: sugyan/atrium (language: rust)

Issue:
wasm32 support?
If we try to use ATrium in an environment such as Cloudflare Workers (not sure if there is such a use case), it will be built with `--target wasm32-...`.

However, the current atrium-api cannot be built with that target. Perhaps we encount the following error.
```
future created by async block is not `Send`.
```
This is because `async_trait` converts async functions to require `dyn Future + Send`, which seems to cause the build to fail on the wasm32 target.
It seems that we can write `#[async_trait(?Send)]` to work around this limitation.
https://docs.rs/async-trait/latest/async_trait/#non-threadsafe-futures

Many projects seem to use `target_arch = "wasm32"` to switch whether to add `?Send` or not.
ATrium seems to be able to solve this problem in the same way.
https://github.com/search?q=%22%2C+async_trait%28%3FSend%29%29%5D%22+language%3ARust&type=code

By the way, if we can change MSRV to 1.75 in the future, we won't need to use `async_trait`, but we will probably use `trait_variant::make` then.
https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#async-fn-in-public-traits


Discussion / hints:
As far as I can tell, `atrium-api` and `atrium-xrpc` both build fine with `--target wasm32-wasi`:
```
$ cargo build -p atrium-api --target wasm32-wasi
[..]
    Finished dev [unoptimized + debuginfo] target(s) in [..]s
$ cargo build -p atrium-xrpc --target wasm32-wasi
   Compiling proc-macro2 v1.0.78
   Compiling serde v1.0.196
   Compiling quote v1.0.35
   Compiling syn v2.0.48
   Compiling serde_derive v1.0.196
   Compiling thiserror-impl v1.0.57
   Compiling async-trait v0.1.77
   Compiling thiserror v1.0.57
   Compiling serde_qs v0.12.0
   Compiling serde_json v1.0.113
   Compiling atrium-xrpc v0.9.0 (/path/to/atrium/atrium-xrpc)
    Finished dev [unoptimized + debuginfo] target(s) in 5.58s
```
What fails is `atrium-xrpc-client`, specifically the `atrium_xrpc_client::reqwest` module (failing with the error you describe above). But if you compile with `--no-default-features` (disabling the `reqwest-native` feature), it also compiles fine (which of course means there's no client enabled; fixing an individual client in the way you describe would be the next step).
I've added a CI job in #117 that pins the working `wasm32-wasi` build configuration (default features for `atrium-api` and `atrium-xrpc`, `--no-default-features` for `atrium-xrpc-client`).
Thanks for the information.
As you pointed out, the main cause of the build failure seems to be only the xrpc-client part and not the atrium-api.

As far as I tried in my local environment, `reqwest` client seemed to work by modifying the `async_trait` part.
It seems that we will need to support only the `reqwest` client when using the wasm32 target.

Fix patch:
diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml
index 6c18d570..c5e894bb 100644
--- a/.github/workflows/wasm.yml
+++ b/.github/workflows/wasm.yml
@@ -45,7 +45,6 @@ jobs:
         run: >
           cargo add
           --path ../crates/atrium-xrpc-client
-          --no-default-features
       - name: Copy pinned dependencies into synthetic crate
         run: cp crates/Cargo.lock ci-build/
       - name: Add target
@@ -54,3 +53,16 @@ jobs:
       - name: Build for target
         working-directory: ./ci-build
         run: cargo build --verbose --target ${{ matrix.target }}
+
+  tests:
+    name: Testing with wasm-bindgen-test
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Add target
+        run: rustup target add wasm32-unknown-unknown
+      - name: Install wasm-pack
+        run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+      - run: wasm-pack test --node atrium-api
+      - run: wasm-pack test --node atrium-xrpc
+      - run: wasm-pack test --node atrium-xrpc-client
diff --git a/.github/workflows/xrpc-client.yml b/.github/workflows/xrpc-client.yml
index c79f02c1..6d9cf741 100644
--- a/.github/workflows/xrpc-client.yml
+++ b/.github/workflows/xrpc-client.yml
@@ -19,8 +19,7 @@ jobs:
       - name: Run tests
         run: |
           cargo test -p atrium-xrpc-client --lib
-          cargo test -p atrium-xrpc-client --lib --no-default-features --features=reqwest-native
-          cargo test -p atrium-xrpc-client --lib --no-default-features --features=reqwest-rustls
+          cargo test -p atrium-xrpc-client --lib --no-default-features --features=reqwest
           cargo test -p atrium-xrpc-client --lib --no-default-features --features=isahc
           cargo test -p atrium-xrpc-client --lib --no-default-features --features=surf
           cargo test -p atrium-xrpc-client --lib --all-features
diff --git a/Cargo.lock b/Cargo.lock
index 0c9f3518..85efc0b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -97,9 +97,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.11"
+version = "0.6.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
+checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -145,9 +145,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.79"
+version = "1.0.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
 
 [[package]]
 name = "arrayref"
@@ -195,7 +195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
 dependencies = [
  "concurrent-queue",
- "event-listener 5.0.0",
+ "event-listener 5.1.0",
  "event-listener-strategy 0.5.0",
  "futures-core",
  "pin-project-lite",
@@ -289,7 +289,7 @@ dependencies = [
  "futures-io",
  "futures-lite 2.2.0",
  "parking",
- "polling 3.4.0",
+ "polling 3.5.0",
  "rustix 0.38.31",
  "slab",
  "tracing",
@@ -369,7 +369,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -399,7 +399,6 @@ version = "0.17.2"
 dependencies = [
  "async-trait",
  "atrium-xrpc 0.9.0",
- "atrium-xrpc-client",
  "chrono",
  "cid 0.10.1",
  "futures",
@@ -412,6 +411,7 @@ dependencies = [
  "serde_ipld_dagcbor 0.4.2",
  "serde_json",
  "tokio",
+ "wasm-bindgen-test",
 ]
 
 [[package]]
@@ -454,6 +454,7 @@ dependencies = [
  "serde_qs 0.12.0",
  "thiserror",
  "tokio",
+ "wasm-bindgen-test",
 ]
 
 [[package]]
@@ -471,6 +472,7 @@ dependencies = [
  "serde",
  "surf",
  "tokio",
+ "wasm-bindgen-test",
 ]
 
 [[package]]
@@ -638,12 +640,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.83"
+version = "1.0.88"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
-dependencies = [
- "libc",
-]
+checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc"
 
 [[package]]
 name = "cfg-if"
@@ -663,7 +662,7 @@ dependencies = [
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets 0.52.0",
+ "windows-targets 0.52.3",
 ]
 
 [[package]]
@@ -734,7 +733,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -751,10 +750,11 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
 [[package]]
 name = "colored"
-version = "2.1.0"
+version = "2.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
+checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
 dependencies = [
+ "is-terminal",
  "lazy_static",
  "windows-sys 0.48.0",
 ]
@@ -779,6 +779,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "const_fn"
 version = "0.4.9"
@@ -903,7 +913,7 @@ dependencies = [
  "openssl-probe",
  "openssl-sys",
  "schannel",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "windows-sys 0.52.0",
 ]
 
@@ -1066,9 +1076,9 @@ dependencies = [
 
 [[package]]
 name = "event-listener"
-version = "5.0.0"
+version = "5.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b72557800024fabbaa2449dd4bf24e37b93702d457a4d4f2b0dd1f0f039f20c1"
+checksum = "b7ad6fd685ce13acd6d9541a30f6db6567a7a24c9ffd4ba2955d29e3f22c8b27"
 dependencies = [
  "concurrent-queue",
  "parking",
@@ -1091,7 +1101,7 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
 dependencies = [
- "event-listener 5.0.0",
+ "event-listener 5.1.0",
  "pin-project-lite",
 ]
 
@@ -1224,7 +1234,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -1350,9 +1360,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "hermit-abi"
-version = "0.3.6"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
+checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60"
 
 [[package]]
 name = "hkdf"
@@ -1466,7 +1476,7 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "tokio",
  "tower-service",
  "tracing",
@@ -1575,6 +1585,17 @@ version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
+[[package]]
+name = "is-terminal"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "isahc"
 version = "1.7.2"
@@ -1792,9 +1813,9 @@ dependencies = [
 
 [[package]]
 name = "mockito"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8d3038e23466858569c2d30a537f691fa0d53b51626630ae08262943e3bbb8b"
+checksum = "a8c84fe1f1d8c56dc157f79942056fad4b9efceebba374a01b222428b553facb"
 dependencies = [
  "assert-json-diff",
  "colored",
@@ -1937,9 +1958,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "openssl"
-version = "0.10.63"
+version = "0.10.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
 dependencies = [
  "bitflags 2.4.2",
  "cfg-if",
@@ -1958,7 +1979,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -1969,9 +1990,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.99"
+version = "0.9.101"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
+checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
 dependencies = [
  "cc",
  "libc",
@@ -2037,7 +2058,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -2087,9 +2108,9 @@ dependencies = [
 
 [[package]]
 name = "polling"
-version = "3.4.0"
+version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30054e72317ab98eddd8561db0f6524df3367636884b7b21b703e4b280a84a14"
+checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
@@ -2355,16 +2376,17 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.17.7"
+version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
+ "cfg-if",
  "getrandom 0.2.12",
  "libc",
  "spin 0.9.8",
  "untrusted 0.9.0",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2429,7 +2451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
 dependencies = [
  "log",
- "ring 0.17.7",
+ "ring 0.17.8",
  "rustls-webpki",
  "sct 0.7.1",
 ]
@@ -2449,15 +2471,15 @@ version = "0.101.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
 dependencies = [
- "ring 0.17.7",
+ "ring 0.17.8",
  "untrusted 0.9.0",
 ]
 
 [[package]]
 name = "ryu"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
 
 [[package]]
 name = "schannel"
@@ -2468,6 +2490,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -2490,7 +2518,7 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
 dependencies = [
- "ring 0.17.7",
+ "ring 0.17.8",
  "untrusted 0.9.0",
 ]
 
@@ -2534,9 +2562,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
-version = "1.0.196"
+version = "1.0.197"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
 dependencies = [
  "serde_derive",
 ]
@@ -2561,13 +2589,13 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.196"
+version = "1.0.197"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -2595,9 +2623,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.113"
+version = "1.0.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
 dependencies = [
  "itoa",
  "ryu",
@@ -2740,12 +2768,12 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.5"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
 dependencies = [
  "libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2871,9 +2899,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.48"
+version = "2.0.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2948,7 +2976,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -3018,7 +3046,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "tokio-macros",
  "windows-sys 0.48.0",
 ]
@@ -3031,7 +3059,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -3103,7 +3131,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
 ]
 
 [[package]]
@@ -3160,9 +3188,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.22"
+version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
 dependencies = [
  "tinyvec",
 ]
@@ -3285,7 +3313,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
  "wasm-bindgen-shared",
 ]
 
@@ -3319,7 +3347,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.48",
+ "syn 2.0.50",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3330,6 +3358,31 @@ version = "0.2.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
 
+[[package]]
+name = "wasm-bindgen-test"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "143ddeb4f833e2ed0d252e618986e18bfc7b0e52f2d28d77d05b2f045dd8eb61"
+dependencies = [
+ "console_error_panic_hook",
+ "js-sys",
+ "scoped-tls",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-test-macro"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
 [[package]]
 name = "web-sys"
 version = "0.3.68"
@@ -3393,7 +3446,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.0",
+ "windows-targets 0.52.3",
 ]
 
 [[package]]
@@ -3411,7 +3464,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.0",
+ "windows-targets 0.52.3",
 ]
 
 [[package]]
@@ -3431,17 +3484,17 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.0",
- "windows_aarch64_msvc 0.52.0",
- "windows_i686_gnu 0.52.0",
- "windows_i686_msvc 0.52.0",
- "windows_x86_64_gnu 0.52.0",
- "windows_x86_64_gnullvm 0.52.0",
- "windows_x86_64_msvc 0.52.0",
+ "windows_aarch64_gnullvm 0.52.3",
+ "windows_aarch64_msvc 0.52.3",
+ "windows_i686_gnu 0.52.3",
+ "windows_i686_msvc 0.52.3",
+ "windows_x86_64_gnu 0.52.3",
+ "windows_x86_64_gnullvm 0.52.3",
+ "windows_x86_64_msvc 0.52.3",
 ]
 
 [[package]]
@@ -3452,9 +3505,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -3464,9 +3517,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -3476,9 +3529,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -3488,9 +3541,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -3500,9 +3553,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -3512,9 +3565,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -3524,9 +3577,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.0"
+version = "0.52.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
 
 [[package]]
 name = "winreg"
diff --git a/Cargo.toml b/Cargo.toml
index 72d1297e..c12b8045 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,7 +52,7 @@ tokio = { version = "1.33", default-features = false }
 
 # HTTP client integrations
 isahc = "1.7.2"
-reqwest = { version = "0.11.22", default-features = false }
+reqwest = { version = "0.11.24", default-features = false }
 surf = { version = "2.3.2", default-features = false }
 
 # Errors
@@ -66,3 +66,6 @@ dirs = "5.0.1"
 # Testing
 http-client = { version = "6.5.3", default-features = false }
 mockito = "1.2"
+
+# WebAssembly
+wasm-bindgen-test = "0.3.41"
diff --git a/atrium-api/CHANGELOG.md b/atrium-api/CHANGELOG.md
index b121f6ac..f2587337 100644
--- a/atrium-api/CHANGELOG.md
+++ b/atrium-api/CHANGELOG.md
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Changed
+- For traits defined using `async_trait`, the `Send` bound is now optional with `wasm32-*` targets.
+
 ### Fixed
 - `atrium_api::types::string::{Cid, Datetime}` can now be deserialized with `serde`.
 
diff --git a/atrium-api/Cargo.toml b/atrium-api/Cargo.toml
index 4f487e76..db264106 100644
--- a/atrium-api/Cargo.toml
+++ b/atrium-api/Cargo.toml
@@ -30,12 +30,17 @@ agent = ["tokio/sync"]
 dag-cbor = ["cid/serde-codec", "libipld-core/serde-codec"]
 
 [dev-dependencies]
-atrium-xrpc-client.workspace = true
 futures.workspace = true
 serde_json.workspace = true
-tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
 serde_ipld_dagcbor = { git = "https://github.com/sugyan/serde_ipld_dagcbor.git", rev = "345b240" }
 
+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+tokio = { workspace = true, features = ["macros", "rt"] }
+wasm-bindgen-test.workspace = true
+
 [package.metadata.docs.rs]
 all-features = true
 rustdoc-args = ["--cfg", "docsrs"]
diff --git a/atrium-api/src/agent.rs b/atrium-api/src/agent.rs
index f8cad61f..29509869 100644
--- a/atrium-api/src/agent.rs
+++ b/atrium-api/src/agent.rs
@@ -92,10 +92,11 @@ mod tests {
     use crate::did_doc::{DidDocument, Service, VerificationMethod};
     use async_trait::async_trait;
     use atrium_xrpc::HttpClient;
-    use futures::future::join_all;
     use http::{Request, Response};
     use std::collections::HashMap;
     use tokio::sync::RwLock;
+    #[cfg(target_arch = "wasm32")]
+    use wasm_bindgen_test::wasm_bindgen_test;
 
     #[derive(Default)]
     struct DummyResponses {
@@ -109,13 +110,16 @@ mod tests {
         counts: Arc<RwLock<HashMap<String, usize>>>,
     }
 
-    #[async_trait]
+    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
     impl HttpClient for DummyClient {
         async fn send_http(
             &self,
             request: Request<Vec<u8>>,
         ) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
+            #[cfg(not(target_arch = "wasm32"))]
             tokio::time::sleep(std::time::Duration::from_micros(10)).await;
+
             let builder =
                 Response::builder().header(http::header::CONTENT_TYPE, "application/json");
             let token = request
@@ -197,12 +201,14 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_new() {
         let agent = AtpAgent::new(DummyClient::default(), MemorySessionStore::default());
         assert_eq!(agent.store.get_session().await, None);
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_login() {
         let session = session();
         // success
@@ -241,6 +247,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_xrpc_get_session() {
         let session = session();
         let client = DummyClient {
@@ -270,6 +277,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_xrpc_get_session_with_refresh() {
         let mut session = session();
         session.access_jwt = String::from("expired");
@@ -307,6 +315,7 @@ mod tests {
         );
     }
 
+    #[cfg(not(target_arch = "wasm32"))]
     #[tokio::test]
     async fn test_xrpc_get_session_with_duplicated_refresh() {
         let mut session = session();
@@ -331,7 +340,7 @@ mod tests {
             let agent = Arc::clone(&agent);
             tokio::spawn(async move { agent.api.com.atproto.server.get_session().await })
         });
-        let results = join_all(handles).await;
+        let results = futures::future::join_all(handles).await;
         for result in &results {
             let output = result
                 .as_ref()
@@ -358,6 +367,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_resume_session() {
         let session = session();
         // success
@@ -405,6 +415,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_resume_session_with_refresh() {
         let session = session();
         let client = DummyClient {
@@ -432,6 +443,7 @@ mod tests {
     }
 
     #[tokio::test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     async fn test_login_with_diddoc() {
         let session = session();
         let did_doc = DidDocument {
diff --git a/atrium-api/src/agent/inner.rs b/atrium-api/src/agent/inner.rs
index f590ec54..33518524 100644
--- a/atrium-api/src/agent/inner.rs
+++ b/atrium-api/src/agent/inner.rs
@@ -15,7 +15,8 @@ struct SessionStoreClient<S, T> {
     inner: T,
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<S, T> HttpClient for SessionStoreClient<S, T>
 where
     S: Send + Sync,
@@ -29,7 +30,8 @@ where
     }
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<S, T> XrpcClient for SessionStoreClient<S, T>
 where
     S: SessionStore + Send + Sync,
@@ -144,7 +146,8 @@ where
     }
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<S, T> HttpClient for Client<S, T>
 where
     S: Send + Sync,
@@ -158,7 +161,8 @@ where
     }
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<S, T> XrpcClient for Client<S, T>
 where
     S: SessionStore + Send + Sync,
@@ -235,7 +239,8 @@ impl<S> Store<S> {
     }
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<S> SessionStore for Store<S>
 where
     S: SessionStore + Send + Sync,
diff --git a/atrium-api/src/agent/store.rs b/atrium-api/src/agent/store.rs
index 732b7262..3d2b3397 100644
--- a/atrium-api/src/agent/store.rs
+++ b/atrium-api/src/agent/store.rs
@@ -4,7 +4,8 @@ pub use self::memory::MemorySessionStore;
 pub(crate) use super::Session;
 use async_trait::async_trait;
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 pub trait SessionStore {
     #[must_use]
     async fn get_session(&self) -> Option<Session>;
diff --git a/atrium-api/src/agent/store/memory.rs b/atrium-api/src/agent/store/memory.rs
index 642d861b..a60d2d97 100644
--- a/atrium-api/src/agent/store/memory.rs
+++ b/atrium-api/src/agent/store/memory.rs
@@ -8,7 +8,8 @@ pub struct MemorySessionStore {
     session: Arc<RwLock<Option<Session>>>,
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl SessionStore for MemorySessionStore {
     async fn get_session(&self) -> Option<Session> {
         self.session.read().await.clone()
diff --git a/atrium-xrpc-client/CHANGELOG.md b/atrium-xrpc-client/CHANGELOG.md
index 0aa610cb..e4352abc 100644
--- a/atrium-xrpc-client/CHANGELOG.md
+++ b/atrium-xrpc-client/CHANGELOG.md
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+- WASM support with `reqwest::ReqwestClient`
+
+### Changed
+- `reqwest-native` feature was renamed to `reqwest-default-tls`
+- `reqwest-rustls` feature was removed. Use `reqwest` feature and `reqwest` crate to configure yourself.
+
 ## [0.3.0](https://github.com/sugyan/atrium/compare/atrium-xrpc-client-v0.2.0...atrium-xrpc-client-v0.3.0) - 2024-02-20
 
 ### Added
diff --git a/atrium-xrpc-client/Cargo.toml b/atrium-xrpc-client/Cargo.toml
index 449bdc62..296f22a7 100644
--- a/atrium-xrpc-client/Cargo.toml
+++ b/atrium-xrpc-client/Cargo.toml
@@ -20,13 +20,17 @@ reqwest = { workspace = true, optional = true }
 surf = { workspace = true, optional = true }
 
 [features]
-default = ["reqwest-native"]
+default = ["reqwest-default-tls"]
 isahc = ["dep:isahc"]
-reqwest-native = ["reqwest/native-tls"]
-reqwest-rustls = ["reqwest/rustls-tls"]
+reqwest = ["dep:reqwest"]
+reqwest-default-tls = ["reqwest/default-tls"]
 surf = ["dep:surf"]
 
-[dev-dependencies]
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+reqwest.workspace = true
+
+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+reqwest = { workspace = true, features = ["native-tls", "rustls-tls"] }
 surf = { workspace = true, features = ["h1-client-rustls"] }
 http-client = { workspace = true, features = ["h1_client", "rustls"] }
 mockito.workspace = true
@@ -34,6 +38,9 @@ tokio = { workspace = true, features = ["macros"] }
 serde = { workspace = true, features = ["derive"] }
 futures.workspace = true
 
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+wasm-bindgen-test.workspace = true
+
 [package.metadata.docs.rs]
 all-features = true
 rustdoc-args = ["--cfg", "docsrs"]
diff --git a/atrium-xrpc-client/README.md b/atrium-xrpc-client/README.md
index 1b712ebe..227e7112 100644
--- a/atrium-xrpc-client/README.md
+++ b/atrium-xrpc-client/README.md
@@ -9,31 +9,22 @@ This library provides clients that implement the [`XrpcClient`](https://docs.rs/
 
 ## Features
 
-- `reqwest-native` (default)
-- `reqwest-rustls`
+- `reqwest-default-tls` (default)
+- `reqwest`
 - `isahc`
 - `surf`
 
 Usage examples are provided below.
 
-### `reqwest-native` and `reqwest-rustls`
+### `reqwest`
 
-If you are using [`tokio`](https://crates.io/crates/tokio) as your asynchronous runtime, you may find it convenient to utilize the [`reqwest`](https://crates.io/crates/reqwest) backend with this feature, which is a high-level asynchronous HTTP Client. Within this crate, you have the choice of configuring `reqwest` with either `reqwest/native-tls` or `reqwest/rustls-tls`.
+If you are using [`tokio`](https://crates.io/crates/tokio) as your asynchronous runtime, you may find it convenient to utilize the [`reqwest`](https://crates.io/crates/reqwest) backend with this feature, which is a high-level asynchronous HTTP Client. By default, transport layer security (TLS) with `reqwest`'s `default-tls` feature is used.
 
 ```toml
 [dependencies]
 atrium-xrpc-client = "*"
 ```
 
-To use the `reqwest::Client` with the `rustls` TLS backend, specify the feature as follows:
-
-```toml
-[dependencies]
-atrium-xrpc-client = { version = "*", default-features = false, features = ["reqwest-rustls"]}
-```
-
-In either case, you can use the `ReqwestClient`:
-
 ```rust
 use atrium_xrpc_client::reqwest::ReqwestClient;
 
@@ -43,12 +34,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 }
 ```
 
-You can also directly specify a `reqwest::Client` with your own configuration:
+
+If you want to use the `rustls` TLS backend, or use `reqwest::Client` with your own configuration, you can directly specify with the `ReqwestClientBuilder`:
 
 ```toml
 [dependencies]
-atrium-xrpc-client = { version = "*", default-features = false }
-reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls"] }
+atrium-xrpc-client = { version = "*", default-features = false, features = ["reqwest"] }
+reqwest = { version = "0.11.24", default-features = false, features = ["rustls-tls"] }
 ```
 
 ```rust
@@ -160,3 +152,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 ```
 
 For more details, refer to the [`surf` documentation](https://docs.rs/surf).
+
+## WASM support
+
+When the target_arch is wasm32, only `reqwest::*` will be enabled, and its
+client implementation automatically switches to the WASM one .
diff --git a/atrium-xrpc-client/src/lib.rs b/atrium-xrpc-client/src/lib.rs
index 409abdf6..eadeb55d 100644
--- a/atrium-xrpc-client/src/lib.rs
+++ b/atrium-xrpc-client/src/lib.rs
@@ -4,15 +4,12 @@
 #[cfg_attr(docsrs, doc(cfg(feature = "isahc")))]
 #[cfg(feature = "isahc")]
 pub mod isahc;
-#[cfg_attr(
-    docsrs,
-    doc(cfg(any(feature = "reqwest-native", feature = "reqwest-rustls")))
-)]
-#[cfg(any(feature = "reqwest-native", feature = "reqwest-rustls"))]
+#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
+#[cfg(any(feature = "reqwest", target_arch = "wasm32"))]
 pub mod reqwest;
 #[cfg_attr(docsrs, doc(cfg(feature = "surf")))]
 #[cfg(feature = "surf")]
 pub mod surf;
 
-#[cfg(test)]
+#[cfg(all(test, not(target_arch = "wasm32")))]
 mod tests;
diff --git a/atrium-xrpc-client/src/reqwest.rs b/atrium-xrpc-client/src/reqwest.rs
index cd612e57..f251d423 100644
--- a/atrium-xrpc-client/src/reqwest.rs
+++ b/atrium-xrpc-client/src/reqwest.rs
@@ -54,7 +54,8 @@ impl ReqwestClientBuilder {
     }
 }
 
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl HttpClient for ReqwestClient {
     async fn send_http(
         &self,
@@ -80,9 +81,13 @@ impl XrpcClient for ReqwestClient {
 #[cfg(test)]
 mod tests {
     use super::*;
+    #[cfg(not(target_arch = "wasm32"))]
     use std::time::Duration;
+    #[cfg(target_arch = "wasm32")]
+    use wasm_bindgen_test::*;
 
     #[test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     fn new() -> Result<(), Box<dyn std::error::Error>> {
         let client = ReqwestClient::new("http://localhost:8080");
         assert_eq!(client.base_uri(), "http://localhost:8080");
@@ -90,12 +95,16 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
     fn builder_without_client() -> Result<(), Box<dyn std::error::Error>> {
         let client = ReqwestClientBuilder::new("http://localhost:8080").build();
         assert_eq!(client.base_uri(), "http://localhost:8080");
         Ok(())
     }
 
+    // TODO: Reqwest::Client doesn't have a `timeout` in wasm module
+    // https://github.com/seanmonstar/reqwest/pull/1760
+    #[cfg(not(target_arch = "wasm32"))]
     #[test]
     fn builder_with_client() -> Result<(), Box<dyn std::error::Error>> {
         let client = ReqwestClientBuilder::new("http://localhost:8080")
diff --git a/atrium-xrpc/CHANGELOG.md b/atrium-xrpc/CHANGELOG.md
index d1d89b9d..491a144c 100644
--- a/atrium-xrpc/CHANGELOG.md
+++ b/atrium-xrpc/CHANGELOG.md
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Changed
+- For traits defined using `async_trait`, the `Send` bound is now optional with `wasm32-*` targets.
+
 ## [0.9.0](https://github.com/sugyan/atrium/compare/atrium-xrpc-v0.8.0...atrium-xrpc-v0.9.0) - 2024-02-20
 
 ### Other
diff --git a/atrium-xrpc/Cargo.toml b/atrium-xrpc/Cargo.toml
index 07980d12..f6bfaff9 100644
--- a/atrium-xrpc/Cargo.toml
+++ b/atrium-xrpc/Cargo.toml
@@ -21,3 +21,6 @@ thiserror.workspace = true
 
 [dev-dependencies]
 tokio = { workspace = true, features = ["macros", "rt"] }
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+wasm-bindgen-test.workspace = true
diff --git a/atrium-xrpc/src/lib.rs b/atrium-xrpc/src/lib.rs
index 9e9ee1bd..78b6ec2d 100644
--- a/atrium-xrpc/src/lib.rs
+++ b/atrium-xrpc/src/lib.rs
@@ -29,7 +29,8 @@ where
 }
 
 /// An abstract HTTP client.
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 pub trait HttpClient {
     async fn send_http(
         &self,
@@ -55,7 +56,8 @@ pub type XrpcResult<O, E> = Result<OutputDataOrBytes<O>, self::Error<E>>;
 ///
 /// [`send_xrpc()`](XrpcClient::send_xrpc) method has a default implementation,
 /// which wraps the [`HttpClient::send_http()`]` method to handle input and output as an XRPC Request.
-#[async_trait]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 pub trait XrpcClient: HttpClient {
     fn base_uri(&self) -> String;
     #[allow(unused_variables)]
@@ -127,6 +129,8 @@ pub trait XrpcClient: HttpClient {
 #[cfg(test)]
 mod tests {
     use super::*;
+    #[cfg(target_arch = "wasm32")]
+    use wasm_bindgen_test::*;
 
     struct DummyClient {
         status: http::StatusCode,
@@ -134,7 +138,8 @@ mod tests {
         body: Vec<u8>,
     }
 
-    #[async_trait]
+    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
     impl HttpClient for DummyClient {
         async fn send_http(
             &self,
@@ -148,7 +153,6 @@ mod tests {
         }
     }
 
-    #[async_trait]
     impl XrpcClient for DummyClient {
         fn base_uri(&self) -> String {
             "https://example.com".into()
@@ -195,6 +199,7 @@ mod tests {
         }
 
         #[test]
+        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
         fn deserialize_xrpc_error() {
             {
                 let body = r#"{"error":"InvalidToken","message":"Invalid token"}"#;
@@ -223,6 +228,7 @@ mod tests {
         }
 
         #[tokio::test]
+        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
         async fn response_ok() {
             let client = DummyClient {
                 status: http::StatusCode::OK,
@@ -236,6 +242,7 @@ mod tests {
         }
 
         #[tokio::test]
+        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
         async fn response_custom_error() {
             let client = DummyClient {
                 status: http::StatusCode::BAD_REQUEST,
@@ -261,6 +268,7 @@ mod tests {
         }
 
         #[tokio::test]
+        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
         async fn response_undefined_error() {
             let client = DummyClient {
                 status: http::StatusCode::INTERNAL_SERVER_ERROR,
@@ -330,6 +338,7 @@ mod tests {
             enum Error {}
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_ok() {
                 let body = r"data".as_bytes().to_vec();
                 let client = DummyClient {
@@ -349,6 +358,7 @@ mod tests {
             }
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_unexpected() {
                 let client = DummyClient {
                     status: http::StatusCode::OK,
@@ -407,6 +417,7 @@ mod tests {
             enum Error {}
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_ok() {
                 let client = DummyClient {
                     status: http::StatusCode::OK,
@@ -419,6 +430,7 @@ mod tests {
             }
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_unexpected() {
                 let client = DummyClient {
                     status: http::StatusCode::OK,
@@ -467,6 +479,7 @@ mod tests {
             enum Error {}
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_ok() {
                 let client = DummyClient {
                     status: http::StatusCode::OK,
@@ -479,6 +492,7 @@ mod tests {
             }
 
             #[tokio::test]
+            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
             async fn response_unexpected() {
                 let client = DummyClient {
                     status: http::StatusCode::OK,


Test patch:
diff --git a/atrium-xrpc-client/src/tests.rs b/atrium-xrpc-client/src/tests.rs
index 0755feba..73db1ff2 100644
--- a/atrium-xrpc-client/src/tests.rs
+++ b/atrium-xrpc-client/src/tests.rs
@@ -107,7 +107,7 @@ async fn send_query() -> Result<(), Box<dyn std::error::Error>> {
                     .build(),
                 path.to_string(),
             )),
-            #[cfg(feature = "reqwest-native")]
+            #[cfg(feature = "reqwest")]
             tokio::spawn(run_query(
                 crate::reqwest::ReqwestClientBuilder::new(base_uri)
                     .client(
@@ -119,7 +119,7 @@ async fn send_query() -> Result<(), Box<dyn std::error::Error>> {
                     .build(),
                 path.to_string(),
             )),
-            #[cfg(feature = "reqwest-rustls")]
+            #[cfg(feature = "reqwest")]
             tokio::spawn(run_query(
                 crate::reqwest::ReqwestClientBuilder::new(base_uri)
                     .client(
@@ -232,7 +232,7 @@ async fn send_procedure() -> Result<(), Box<dyn std::error::Error>> {
                     .build(),
                 path.to_string(),
             )),
-            #[cfg(feature = "reqwest-native")]
+            #[cfg(feature = "reqwest")]
             tokio::spawn(run_procedure(
                 crate::reqwest::ReqwestClientBuilder::new(base_uri)
                     .client(
@@ -244,7 +244,7 @@ async fn send_procedure() -> Result<(), Box<dyn std::error::Error>> {
                     .build(),
                 path.to_string(),
             )),
-            #[cfg(feature = "reqwest-rustls")]
+            #[cfg(feature = "reqwest")]
             tokio::spawn(run_procedure(
                 crate::reqwest::ReqwestClientBuilder::new(base_uri)
                     .client(