How to send `FormData` via `Fetch`? I

How to send FormData via Fetch? I don't see Into<JsValue> implementation on the FormData struct.
4 Replies
alex35mil
alex35milOP12mo ago
Apparently, there's no way to use exposed FormData for a request body, so I just build a string manually (I don't send any files yet, so it supports only text fields):
pub trait Body: fmt::Debug {
fn content_type(&self) -> String;
fn try_into(&self) -> Result<JsValue, worker::Error>;
}

#[derive(Debug)]
pub struct FormDataBody(HashMap<String, String>);

impl FormDataBody {
const BOUNDARY: &'static str = "X-INSERT-YOUR-BOUNDARY";

pub fn new() -> Self {
Self(HashMap::new())
}

pub fn append(&mut self, key: &str, value: &str) {
self.0.insert(key.to_string(), value.to_string());
}
}

impl Body for FormDataBody {
fn content_type(&self) -> String {
format!("multipart/form-data; boundary={}", Self::BOUNDARY)
}

fn try_into(&self) -> Result<JsValue, worker::Error> {
let mut body = String::new();

for (key, value) in &self.0 {
body.push_str(&format!("--{}\r\n", Self::BOUNDARY));
body.push_str(&format!("Content-Disposition: form-data; name=\"{key}\""));
body.push_str(&format!("\r\n\r\n{value}\r\n"));
}

body.push_str(&format!("--{}--\r\n", Self::BOUNDARY));

Ok(body.into())
}
}
pub trait Body: fmt::Debug {
fn content_type(&self) -> String;
fn try_into(&self) -> Result<JsValue, worker::Error>;
}

#[derive(Debug)]
pub struct FormDataBody(HashMap<String, String>);

impl FormDataBody {
const BOUNDARY: &'static str = "X-INSERT-YOUR-BOUNDARY";

pub fn new() -> Self {
Self(HashMap::new())
}

pub fn append(&mut self, key: &str, value: &str) {
self.0.insert(key.to_string(), value.to_string());
}
}

impl Body for FormDataBody {
fn content_type(&self) -> String {
format!("multipart/form-data; boundary={}", Self::BOUNDARY)
}

fn try_into(&self) -> Result<JsValue, worker::Error> {
let mut body = String::new();

for (key, value) in &self.0 {
body.push_str(&format!("--{}\r\n", Self::BOUNDARY));
body.push_str(&format!("Content-Disposition: form-data; name=\"{key}\""));
body.push_str(&format!("\r\n\r\n{value}\r\n"));
}

body.push_str(&format!("--{}--\r\n", Self::BOUNDARY));

Ok(body.into())
}
}
radix
radix12mo ago
FWIW, you can also use reqwest which has a nicer API than the workers-provided Fetch e.g., this is how I interact with the CloudFlare "images" API, which also uses form data:
let api_url =
format!("https://api.cloudflare.com/client/v4/accounts/{}/images/v1", self.account_id);
let metadata = serde_json::to_string(&json!({"purpose": purpose.to_string()}))?;
let form = reqwest::multipart::Form::new()
.text("metadata", metadata)
.text("id", self.gen_custom_id())
.text("url", url.to_owned());
let client = reqwest::Client::new();
let response = client
.post(api_url)
.multipart(form)
.header("Authorization", format!("Bearer {}", &self.images_token))
.send()
.await?;
let response = response.error_for_status()?;
let api_url =
format!("https://api.cloudflare.com/client/v4/accounts/{}/images/v1", self.account_id);
let metadata = serde_json::to_string(&json!({"purpose": purpose.to_string()}))?;
let form = reqwest::multipart::Form::new()
.text("metadata", metadata)
.text("id", self.gen_custom_id())
.text("url", url.to_owned());
let client = reqwest::Client::new();
let response = client
.post(api_url)
.multipart(form)
.header("Authorization", format!("Bearer {}", &self.images_token))
.send()
.await?;
let response = response.error_for_status()?;
natsumesou
natsumesou12mo ago
@alex35mil How did you put the manually constructed string into the request body? Attempting to assign it with RequestInit.with_body results in an error because it's not of type JsValue.
alex35mil
alex35milOP12mo ago
@natsumesou you can call .into() on a string to turn it into JsValue since the latter implements From<String>
Want results from more Discord servers?
Add your server