Browse Source

fix(core): fix raw invoke body for isolation pattern (#10167)

* fix(core): fix raw invoke body for isolation pattern

The `isolation` pattern requests are made using JSON but the payload could be raw bytes, so we send the original `Content-Type`  from frontend and make sure to deserialize the payload using that one instead of `Content-Type` from request headers

* clippy

* disable plist embed in generate_context in tests

* change file

* docs [skip ci]

* move unused_variables [skip ci]

* last commit regression [skip ci]

* fix test

* add example, do not text encode raw request

* check type instead of contenttype

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Amr Bashir 1 năm trước cách đây
mục cha
commit
4c239729c3

+ 6 - 0
.changes/generate_context-test.md

@@ -0,0 +1,6 @@
+---
+"tauri-macros": "patch"
+"tauri-codegen": "patch"
+---
+
+Add support for `test = true` in `generate_context!` macro to skip some code generation that could affect some tests, for now it only skips empedding a plist on macOS.

+ 5 - 0
.changes/isolation-raw-request.md

@@ -0,0 +1,5 @@
+---
+"tauri": "patch:bug"
+---
+
+Fix deserialization of raw invoke requests when using `isolation` pattern.

+ 5 - 0
.changes/utils-raw-isolation-payload.md

@@ -0,0 +1,5 @@
+---
+"tauri-utils": "patch:feat"
+---
+
+Add `RawIsolationPayload::content_type` method.

+ 1 - 0
core/tauri-build/src/codegen/context.rs

@@ -129,6 +129,7 @@ impl CodegenContext {
       root: quote::quote!(::tauri),
       capabilities: self.capabilities,
       assets: None,
+      test: false,
     })?;
 
     // get the full output file path

+ 7 - 1
core/tauri-codegen/src/context.rs

@@ -43,6 +43,8 @@ pub struct ContextData {
   pub capabilities: Option<Vec<PathBuf>>,
   /// The custom assets implementation
   pub assets: Option<Expr>,
+  /// Skip runtime-only types generation for tests (e.g. embed-plist usage).
+  pub test: bool,
 }
 
 fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
@@ -140,8 +142,12 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
     root,
     capabilities: additional_capabilities,
     assets,
+    test,
   } = data;
 
+  #[allow(unused_variables)]
+  let running_tests = test;
+
   let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
     .as_deref()
     .map(Target::from_triple)
@@ -291,7 +297,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
   };
 
   #[cfg(target_os = "macos")]
-  let info_plist = if target == Target::MacOS && dev {
+  let info_plist = if target == Target::MacOS && dev && !running_tests {
     let info_plist_path = config_parent.join("Info.plist");
     let mut info_plist = if info_plist_path.exists() {
       plist::Value::from_file(&info_plist_path)

+ 18 - 1
core/tauri-macros/src/context.rs

@@ -8,7 +8,7 @@ use std::path::PathBuf;
 use syn::{
   parse::{Parse, ParseBuffer},
   punctuated::Punctuated,
-  Expr, ExprLit, Lit, LitStr, Meta, PathArguments, PathSegment, Token,
+  Expr, ExprLit, Lit, LitBool, LitStr, Meta, PathArguments, PathSegment, Token,
 };
 use tauri_codegen::{context_codegen, get_config, ContextData};
 use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Target};
@@ -18,6 +18,7 @@ pub(crate) struct ContextItems {
   root: syn::Path,
   capabilities: Option<Vec<PathBuf>>,
   assets: Option<Expr>,
+  test: bool,
 }
 
 impl Parse for ContextItems {
@@ -31,6 +32,7 @@ impl Parse for ContextItems {
     let mut root = None;
     let mut capabilities = None;
     let mut assets = None;
+    let mut test = false;
     let config_file = input.parse::<LitStr>().ok().map(|raw| {
       let _ = input.parse::<Token![,]>();
       let path = PathBuf::from(raw.value());
@@ -93,6 +95,17 @@ impl Parse for ContextItems {
             "assets" => {
               assets.replace(v.value);
             }
+            "test" => {
+              if let Expr::Lit(ExprLit {
+                lit: Lit::Bool(LitBool { value, .. }),
+                ..
+              }) = v.value
+              {
+                test = value;
+              } else {
+                return Err(syn::Error::new(input.span(), "unexpected value for test"));
+              }
+            }
             name => {
               return Err(syn::Error::new(
                 input.span(),
@@ -105,6 +118,8 @@ impl Parse for ContextItems {
           return Err(syn::Error::new(input.span(), "unexpected list input"));
         }
       }
+
+      let _ = input.parse::<Token![,]>();
     }
 
     Ok(Self {
@@ -128,6 +143,7 @@ impl Parse for ContextItems {
       }),
       capabilities,
       assets,
+      test,
     })
   }
 }
@@ -142,6 +158,7 @@ pub(crate) fn generate_context(context: ContextItems) -> TokenStream {
       root: context.root.to_token_stream(),
       capabilities: context.capabilities,
       assets: context.assets,
+      test: context.test,
     })
     .and_then(|data| context_codegen(data).map_err(|e| e.to_string()));
 

+ 1 - 1
core/tauri-plugin/src/build/mod.rs

@@ -124,7 +124,7 @@ impl<'a> Builder<'a> {
       acl::build::generate_docs(
         &permissions,
         &autogenerated,
-        &name.strip_prefix("tauri-plugin-").unwrap_or(&name),
+        name.strip_prefix("tauri-plugin-").unwrap_or(&name),
       )?;
     }
 

+ 14 - 5
core/tauri-utils/src/pattern/isolation.js

@@ -37,20 +37,27 @@
    * @param {object} data
    * @return {Promise<{nonce: number[], payload: number[]}>}
    */
-  async function encrypt(data) {
+  async function encrypt(payload) {
     const algorithm = Object.create(null)
     algorithm.name = 'AES-GCM'
     algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
 
-    const encoder = new TextEncoder()
-    const encoded = encoder.encode(__RAW_process_ipc_message_fn__(data).data)
+    const { contentType, data } = __RAW_process_ipc_message_fn__(payload)
+
+    const message =
+      typeof data === 'string'
+        ? new TextEncoder().encode(data)
+        : ArrayBuffer.isView(data) || data instanceof ArrayBuffer
+        ? data
+        : new Uint8Array(data)
 
     return window.crypto.subtle
-      .encrypt(algorithm, aesGcmKey, encoded)
+      .encrypt(algorithm, aesGcmKey, message)
       .then((payload) => {
         const result = Object.create(null)
         result.nonce = Array.from(new Uint8Array(algorithm.iv))
         result.payload = Array.from(new Uint8Array(payload))
+        result.contentType = contentType
         return result
       })
   }
@@ -66,7 +73,9 @@
       const keys = data.payload ? Object.keys(data.payload) : []
       return (
         keys.length > 0 &&
-        keys.every((key) => key === 'nonce' || key === 'payload')
+        keys.every(
+          (key) => key === 'nonce' || key === 'payload' || key === 'contentType'
+        )
       )
     }
     return false

+ 18 - 1
core/tauri-utils/src/pattern/isolation.rs

@@ -73,6 +73,14 @@ impl AesGcmPair {
   pub fn key(&self) -> &Aes256Gcm {
     &self.key
   }
+
+  #[doc(hidden)]
+  pub fn encrypt(&self, nonce: &[u8; 12], payload: &[u8]) -> Result<Vec<u8>, Error> {
+    self
+      .key
+      .encrypt(nonce.into(), payload)
+      .map_err(|_| self::Error::Aes)
+  }
 }
 
 /// All cryptographic keys required for Isolation encryption
@@ -97,7 +105,7 @@ impl Keys {
 
   /// Decrypts a message using the generated keys.
   pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
-    let RawIsolationPayload { nonce, payload } = raw;
+    let RawIsolationPayload { nonce, payload, .. } = raw;
     let nonce: [u8; 12] = nonce.as_ref().try_into()?;
     self
       .aes_gcm
@@ -109,9 +117,18 @@ impl Keys {
 
 /// Raw representation of
 #[derive(Debug, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
 pub struct RawIsolationPayload<'a> {
   nonce: Cow<'a, [u8]>,
   payload: Cow<'a, [u8]>,
+  content_type: Cow<'a, str>,
+}
+
+impl<'a> RawIsolationPayload<'a> {
+  /// Content type of this payload.
+  pub fn content_type(&self) -> &Cow<'a, str> {
+    &self.content_type
+  }
 }
 
 impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {

+ 3 - 1
core/tauri/scripts/ipc.js

@@ -39,7 +39,9 @@
       const keys = Object.keys(event.data.payload || {})
       return (
         keys.length > 0 &&
-        keys.every((key) => key === 'nonce' || key === 'payload')
+        keys.every(
+          (key) => key === 'contentType' || key === 'nonce' || key === 'payload'
+        )
       )
     }
     return false

+ 1 - 0
core/tauri/src/ipc/mod.rs

@@ -46,6 +46,7 @@ pub type OwnedInvokeResponder<R> =
 
 /// Possible values of an IPC payload.
 #[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
 pub enum InvokeBody {
   /// Json payload.
   Json(JsonValue),

+ 212 - 10
core/tauri/src/ipc/protocol.rs

@@ -388,6 +388,15 @@ fn parse_invoke_request<R: Runtime>(
   // so we must ignore it because some commands use the IPC for faster response
   let has_payload = !body.is_empty();
 
+  #[allow(unused_mut)]
+  let mut content_type = parts
+    .headers
+    .get(http::header::CONTENT_TYPE)
+    .and_then(|h| h.to_str().ok())
+    .map(|mime| mime.parse())
+    .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
+    .map_err(|_| "unknown content type")?;
+
   #[cfg(feature = "isolation")]
   if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
     // if the platform does not support request body, we ignore it
@@ -395,8 +404,18 @@ fn parse_invoke_request<R: Runtime>(
       #[cfg(feature = "tracing")]
       let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered();
 
-      body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
-        .and_then(|raw| crypto_keys.decrypt(raw))
+      (body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
+        .and_then(|raw| {
+          let content_type = raw.content_type().clone();
+          crypto_keys.decrypt(raw).map(|decrypted| {
+            (
+              decrypted,
+              content_type
+                .parse()
+                .unwrap_or(mime::APPLICATION_OCTET_STREAM),
+            )
+          })
+        })
         .map_err(|e| e.to_string())?;
     }
   }
@@ -440,14 +459,6 @@ fn parse_invoke_request<R: Runtime>(
       .map_err(|_| "Tauri error header value must be a numeric string")?,
   );
 
-  let content_type = parts
-    .headers
-    .get(http::header::CONTENT_TYPE)
-    .and_then(|h| h.to_str().ok())
-    .map(|mime| mime.parse())
-    .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
-    .map_err(|_| "unknown content type")?;
-
   #[cfg(feature = "tracing")]
   let span = tracing::trace_span!("ipc::request::deserialize").entered();
 
@@ -481,3 +492,194 @@ fn parse_invoke_request<R: Runtime>(
 
   Ok(payload)
 }
+
+#[cfg(test)]
+mod tests {
+  use std::str::FromStr;
+
+  use super::*;
+  use crate::{manager::AppManager, plugin::PluginStore, StateManager, Wry};
+  use http::header::*;
+  use serde_json::json;
+  use tauri_macros::generate_context;
+
+  #[test]
+  fn parse_invoke_request() {
+    let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
+    let manager: AppManager<Wry> = AppManager::with_handlers(
+      context,
+      PluginStore::default(),
+      Box::new(|_| false),
+      None,
+      Default::default(),
+      StateManager::new(),
+      Default::default(),
+      Default::default(),
+      Default::default(),
+      (None, "".into()),
+      crate::generate_invoke_key().unwrap(),
+    );
+
+    let cmd = "write_something";
+    let url = "tauri://localhost";
+    let invoke_key = "1234ahdsjkl123";
+    let callback = 12378123;
+    let error = 6243;
+    let headers = HeaderMap::from_iter(vec![
+      (
+        CONTENT_TYPE,
+        HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(invoke_key).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(&callback.to_string()).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(&error.to_string()).unwrap(),
+      ),
+      (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
+    ]);
+
+    let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
+    *request.headers_mut().unwrap() = headers.clone();
+
+    let body = vec![123, 31, 45];
+    let request = request.body(body.clone()).unwrap();
+    let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
+
+    assert_eq!(invoke_request.cmd, cmd);
+    assert_eq!(invoke_request.callback.0, callback);
+    assert_eq!(invoke_request.error.0, error);
+    assert_eq!(invoke_request.invoke_key, invoke_key);
+    assert_eq!(invoke_request.url, url.parse().unwrap());
+    assert_eq!(invoke_request.headers, headers);
+    assert_eq!(invoke_request.body, InvokeBody::Raw(body));
+
+    let body = json!({
+      "key": 1,
+      "anotherKey": "asda",
+    });
+
+    let mut headers = headers.clone();
+    headers.insert(
+      CONTENT_TYPE,
+      HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
+    );
+
+    let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
+    *request.headers_mut().unwrap() = headers.clone();
+
+    let request = request.body(serde_json::to_vec(&body).unwrap()).unwrap();
+    let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
+
+    assert_eq!(invoke_request.headers, headers);
+    assert_eq!(invoke_request.body, InvokeBody::Json(body));
+  }
+
+  #[test]
+  #[cfg(feature = "isolation")]
+  fn parse_invoke_request_isolation() {
+    let context = generate_context!(
+      "test/fixture/isolation/src-tauri/tauri.conf.json",
+      crate,
+      test = false
+    );
+
+    let crate::pattern::Pattern::Isolation { crypto_keys, .. } = &context.pattern else {
+      unreachable!()
+    };
+
+    let mut nonce = [0u8; 12];
+    getrandom::getrandom(&mut nonce).unwrap();
+
+    let body_raw = vec![1, 41, 65, 12, 78];
+    let body_bytes = crypto_keys.aes_gcm().encrypt(&nonce, &body_raw).unwrap();
+    let isolation_payload_raw = json!({
+      "nonce": nonce,
+      "payload": body_bytes,
+      "contentType":  mime::APPLICATION_OCTET_STREAM.to_string(),
+    });
+
+    let body_json = json!({
+      "key": 1,
+      "anotherKey": "string"
+    });
+    let body_bytes = crypto_keys
+      .aes_gcm()
+      .encrypt(&nonce, &serde_json::to_vec(&body_json).unwrap())
+      .unwrap();
+    let isolation_payload_json = json!({
+      "nonce": nonce,
+      "payload": body_bytes,
+      "contentType":  mime::APPLICATION_JSON.to_string(),
+    });
+
+    let manager: AppManager<Wry> = AppManager::with_handlers(
+      context,
+      PluginStore::default(),
+      Box::new(|_| false),
+      None,
+      Default::default(),
+      StateManager::new(),
+      Default::default(),
+      Default::default(),
+      Default::default(),
+      (None, "".into()),
+      crate::generate_invoke_key().unwrap(),
+    );
+
+    let cmd = "write_something";
+    let url = "tauri://localhost";
+    let invoke_key = "1234ahdsjkl123";
+    let callback = 12378123;
+    let error = 6243;
+
+    let headers = HeaderMap::from_iter(vec![
+      (
+        CONTENT_TYPE,
+        HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(invoke_key).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(&callback.to_string()).unwrap(),
+      ),
+      (
+        HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
+        HeaderValue::from_str(&error.to_string()).unwrap(),
+      ),
+      (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
+    ]);
+
+    let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
+    *request.headers_mut().unwrap() = headers.clone();
+    let body = serde_json::to_vec(&isolation_payload_raw).unwrap();
+    let request = request.body(body).unwrap();
+    let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
+
+    assert_eq!(invoke_request.cmd, cmd);
+    assert_eq!(invoke_request.callback.0, callback);
+    assert_eq!(invoke_request.error.0, error);
+    assert_eq!(invoke_request.invoke_key, invoke_key);
+    assert_eq!(invoke_request.url, url.parse().unwrap());
+    assert_eq!(invoke_request.headers, headers);
+    assert_eq!(invoke_request.body, InvokeBody::Raw(body_raw));
+
+    let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
+    *request.headers_mut().unwrap() = headers.clone();
+    let body = serde_json::to_vec(&isolation_payload_json).unwrap();
+    let request = request.body(body).unwrap();
+    let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
+
+    assert_eq!(invoke_request.headers, headers);
+    assert_eq!(invoke_request.body, InvokeBody::Json(body_json));
+  }
+}

+ 1 - 1
core/tauri/src/manager/mod.rs

@@ -692,7 +692,7 @@ mod test {
 
   #[test]
   fn check_get_url() {
-    let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
+    let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
     let manager: AppManager<Wry> = AppManager::with_handlers(
       context,
       PluginStore::default(),

+ 0 - 1
core/tauri/src/vibrancy/macos.rs

@@ -2,7 +2,6 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-#![cfg(target_os = "macos")]
 #![allow(deprecated)]
 
 use crate::utils::config::WindowEffectsConfig;

+ 6 - 0
core/tauri/test/fixture/isolation/dist/index.html

@@ -0,0 +1,6 @@
+<html>
+  <head></head>
+  <body>
+    <iframe id="mainframe"></iframe>
+  </body>
+</html>

+ 10 - 0
core/tauri/test/fixture/isolation/isolation-dist/index.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Isolation Secure Script</title>
+  </head>
+  <body>
+    <script src="index.js"></script>
+  </body>
+</html>

+ 8 - 0
core/tauri/test/fixture/isolation/isolation-dist/index.js

@@ -0,0 +1,8 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
+  console.log('hook', payload, options)
+  return payload
+}

BIN
core/tauri/test/fixture/isolation/src-tauri/icons/icon.ico


BIN
core/tauri/test/fixture/isolation/src-tauri/icons/icon.ico~dev


BIN
core/tauri/test/fixture/isolation/src-tauri/icons/icon.png


+ 27 - 0
core/tauri/test/fixture/isolation/src-tauri/tauri.conf.json

@@ -0,0 +1,27 @@
+{
+  "$schema": "../../../../../../core/tauri-config-schema/schema.json",
+  "identifier": "isolation.tauri.example",
+  "build": {
+    "frontendDist": "../dist",
+    "devUrl": "http://localhost:4000"
+  },
+  "app": {
+    "windows": [
+      {
+        "title": "Isolation Tauri App"
+      }
+    ],
+    "security": {
+      "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: http://ipc.localhost",
+      "pattern": {
+        "use": "isolation",
+        "options": {
+          "dir": "../isolation-dist"
+        }
+      }
+    }
+  },
+  "bundle": {
+    "active": true
+  }
+}

+ 5 - 3
examples/api/src-tauri/build.rs

@@ -10,9 +10,11 @@ fn main() {
         "app-menu",
         tauri_build::InlinedPlugin::new().commands(&["toggle", "popup"]),
       )
-      .app_manifest(
-        tauri_build::AppManifest::new().commands(&["log_operation", "perform_request"]),
-      ),
+      .app_manifest(tauri_build::AppManifest::new().commands(&[
+        "log_operation",
+        "perform_request",
+        "echo",
+      ])),
   )
   .expect("failed to run tauri-build");
 }

+ 2 - 1
examples/api/src-tauri/capabilities/run-app.json

@@ -16,6 +16,7 @@
       ]
     },
     "allow-perform-request",
+    "allow-echo",
     "app-menu:default",
     "sample:allow-ping-scoped",
     "sample:global-scope",
@@ -68,4 +69,4 @@
     "webview:allow-create-webview-window",
     "webview:allow-print"
   ]
-}
+}

+ 11 - 0
examples/api/src-tauri/permissions/autogenerated/echo.toml

@@ -0,0 +1,11 @@
+# Automatically generated - DO NOT EDIT!
+
+[[permission]]
+identifier = "allow-echo"
+description = "Enables the echo command without any pre-configured scope."
+commands.allow = ["echo"]
+
+[[permission]]
+identifier = "deny-echo"
+description = "Denies the echo command without any pre-configured scope."
+commands.deny = ["echo"]

+ 5 - 0
examples/api/src-tauri/src/cmd.rs

@@ -45,3 +45,8 @@ pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
     message: "message response".into(),
   }
 }
+
+#[command]
+pub fn echo(request: tauri::ipc::Request<'_>) -> tauri::ipc::Response {
+  tauri::ipc::Response::new(request.body().clone())
+}

+ 1 - 0
examples/api/src-tauri/src/lib.rs

@@ -141,6 +141,7 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
     .invoke_handler(tauri::generate_handler![
       cmd::log_operation,
       cmd::perform_request,
+      cmd::echo
     ])
     .build(tauri::tauri_build_context!())
     .expect("error while building tauri application");

+ 11 - 1
examples/api/src/App.svelte

@@ -83,12 +83,22 @@
   let messages = writable([])
   let consoleTextEl
   async function onMessage(value) {
+    const valueStr =
+      typeof value === 'string'
+        ? value
+        : JSON.stringify(
+            value instanceof ArrayBuffer
+              ? Array.from(new Uint8Array(value))
+              : value,
+            null,
+            1
+          )
     messages.update((r) => [
       ...r,
       {
         html:
           `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
-          (typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
+          valueStr +
           '</pre>'
       }
     ])

+ 11 - 0
examples/api/src/views/Communication.svelte

@@ -36,6 +36,16 @@
       .catch(onMessage)
   }
 
+  function echo() {
+    invoke('echo', {
+      message: 'Tauri JSON request!'
+    })
+      .then(onMessage)
+      .catch(onMessage)
+
+    invoke('echo', [1, 2, 3]).then(onMessage).catch(onMessage)
+  }
+
   function emitEvent() {
     webviewWindow.emit('js-event', 'this is the payload string')
   }
@@ -49,4 +59,5 @@
   <button class="btn" id="event" on:click={emitEvent}>
     Send event to Rust
   </button>
+  <button class="btn" id="request" on:click={echo}> Echo </button>
 </div>