diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5dfcdd35..4e79a270 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2497,6 +2497,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3512,6 +3522,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -5104,6 +5115,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check 0.9.4", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index db5f58a3..bcb49d19 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ chrono = { version = "0.4.23", features = ["serde"] } futures = "0.3.26" http = "0.2.8" rand = "0.8.5" -reqwest = { version = "0.11.14", features = ["json"] } +reqwest = { version = "0.11.14", features = ["json", "multipart"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } diff --git a/src-tauri/src/send.rs b/src-tauri/src/send.rs index dfd40704..7addf34e 100644 --- a/src-tauri/src/send.rs +++ b/src-tauri/src/send.rs @@ -1,3 +1,4 @@ +use std::fs; use std::fs::{create_dir_all, File}; use std::io::Write; @@ -5,6 +6,7 @@ use base64::Engine; use http::{HeaderMap, HeaderName, HeaderValue, Method}; use http::header::{ACCEPT, USER_AGENT}; use log::warn; +use reqwest::multipart; use reqwest::redirect::Policy; use sqlx::{Pool, Sqlite}; use sqlx::types::Json; @@ -38,6 +40,10 @@ pub async fn actually_send_request( .build() .expect("Failed to build client"); + let m = Method::from_bytes(request.method.to_uppercase().as_bytes()) + .expect("Failed to create method"); + let mut request_builder = client.request(m, url_string.to_string()); + let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("yaak")); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); @@ -106,11 +112,6 @@ pub async fn actually_send_request( } } - let m = Method::from_bytes(request.method.to_uppercase().as_bytes()) - .expect("Failed to create method"); - - let mut request_builder = client.request(m, url_string.to_string()).headers(headers); - let mut query_params = Vec::new(); for p in request.url_parameters.0 { if !p.enabled || p.name.is_empty() { continue; } @@ -121,25 +122,24 @@ pub async fn actually_send_request( } request_builder = request_builder.query(&query_params); - - if let Some(t) = &request.body_type { + if let Some(body_type) = &request.body_type { let empty_string = &serde_json::to_value("").unwrap(); let empty_bool = &serde_json::to_value(false).unwrap(); - let b = request.body.0; + let request_body = request.body.0; - if b.contains_key("text") { - let raw_text = b.get("text").unwrap_or(empty_string).as_str().unwrap_or(""); + if request_body.contains_key("text") { + let raw_text = request_body.get("text").unwrap_or(empty_string).as_str().unwrap_or(""); let body = render::render(raw_text, &workspace, environment_ref); request_builder = request_builder.body(body); - } else if b.contains_key("form") { + } else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") { let mut form_params = Vec::new(); - let form = b.get("form"); + let form = request_body.get("form"); if let Some(f) = form { for p in f.as_array().unwrap_or(&Vec::new()) { let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false); let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default(); - let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default(); if !enabled || name.is_empty() { continue; } + let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default(); form_params.push(( render::render(name, &workspace, environment_ref), render::render(value, &workspace, environment_ref), @@ -147,11 +147,35 @@ pub async fn actually_send_request( } } request_builder = request_builder.form(&form_params); + } else if body_type == "multipart/form-data" && request_body.contains_key("form") { + let mut multipart_form = multipart::Form::new(); + if let Some(form_definition) = request_body.get("form") { + for p in form_definition.as_array().unwrap_or(&Vec::new()) { + let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false); + let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default(); + if !enabled || name.is_empty() { continue; } + + let file = p.get("file").unwrap_or(empty_string).as_str().unwrap_or_default(); + let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default(); + multipart_form = multipart_form.part( + render::render(name, &workspace, environment_ref), + match !file.is_empty() { + true => multipart::Part::bytes(fs::read(file).expect("Failed to read file")), + false => multipart::Part::text(render::render(value, &workspace, environment_ref)), + }, + ); + } + } + headers.remove("Content-Type"); // reqwest will add this automatically + request_builder = request_builder.multipart(multipart_form); } else { - warn!("Unsupported body type: {}", t); + warn!("Unsupported body type: {}", body_type); } } + // Add headers last, because previous steps may modify them + request_builder = request_builder.headers(headers); + let sendable_req = match request_builder.build() { Ok(r) => r, Err(e) => { diff --git a/src-web/components/FormMultipartEditor.tsx b/src-web/components/FormMultipartEditor.tsx new file mode 100644 index 00000000..5a66d354 --- /dev/null +++ b/src-web/components/FormMultipartEditor.tsx @@ -0,0 +1,37 @@ +import { useCallback, useMemo } from 'react'; +import type { HttpRequest } from '../lib/models'; +import type { PairEditorProps } from './core/PairEditor'; +import { PairEditor } from './core/PairEditor'; + +type Props = { + forceUpdateKey: string; + body: HttpRequest['body']; + onChange: (headers: HttpRequest['body']) => void; +}; + +export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) { + const pairs = useMemo( + () => + (Array.isArray(body.form) ? body.form : []).map((p) => ({ + enabled: p.enabled, + name: p.name, + value: p.value, + })), + [body.form], + ); + + const handleChange = useCallback( + (pairs) => onChange({ form: pairs }), + [onChange], + ); + + return ( + + ); +} diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 424277f3..28a1e855 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -29,6 +29,7 @@ import { Editor } from './core/Editor'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; import { EmptyStateText } from './EmptyStateText'; +import { FormMultipartEditor } from './FormMultipartEditor'; import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { GraphQLEditor } from './GraphQLEditor'; import { HeadersEditor } from './HeadersEditor'; @@ -64,7 +65,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN items: [ { type: 'separator', label: 'Form Data' }, { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, - // { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, + { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, { type: 'separator', label: 'Text Content' }, { label: 'JSON', value: BODY_TYPE_JSON }, { label: 'XML', value: BODY_TYPE_XML }, @@ -282,6 +283,12 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN body={activeRequest.body} onChange={handleBodyChange} /> + ) : activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART ? ( + ) : ( No Body )}