From 0f6b2b214abd78de40f5386b312c8db69db22888 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 3 Aug 2024 22:41:08 +0200 Subject: [PATCH 1/3] Add multipart/form-data uploads This adds support for `multipart/form-data` (RFC 7578) uploads to nushell. Binary data is uploaded as files (`application/octet-stream`), everything else is uploaded as plain text. --- Cargo.lock | 97 +++++++++++++++++++ Cargo.toml | 2 + crates/nu-command/Cargo.toml | 3 +- crates/nu-command/src/network/http/client.rs | 33 +++++++ crates/nu-command/src/network/http/post.rs | 5 + .../tests/commands/network/http/post.rs | 32 +++++- 6 files changed, 170 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4495d6bfa..bbb1c6c5a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2908,6 +2908,7 @@ dependencies = [ "tango-bench", "tempfile", "time", + "ureq_multipart", "winresource", ] @@ -3121,6 +3122,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "ureq", + "ureq_multipart", "url", "uu_cp", "uu_mkdir", @@ -5104,6 +5106,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.44" @@ -5314,6 +5331,38 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.15" @@ -5737,6 +5786,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "sqlparser" version = "0.47.0" @@ -5871,6 +5926,12 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "2.1.0" @@ -6482,6 +6543,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.10.0" @@ -6494,9 +6561,24 @@ dependencies = [ "log", "native-tls", "once_cell", + "rustls", + "rustls-pki-types", "serde", "serde_json", "url", + "webpki-roots", +] + +[[package]] +name = "ureq_multipart" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf2d124865fc4d505f5942222a57f6a3eae8a133819d7fd6194423e3f6e91" +dependencies = [ + "mime", + "mime_guess", + "rand", + "ureq", ] [[package]] @@ -6902,6 +6984,15 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "6.0.1" @@ -7357,6 +7448,12 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index e60acd2548..92c3341658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,6 +166,7 @@ umask = "2.1" unicode-segmentation = "1.11" unicode-width = "0.1" ureq = { version = "2.10", default-features = false } +ureq_multipart = "1.1.1" url = "2.2" uu_cp = "0.0.27" uu_mkdir = "0.0.27" @@ -210,6 +211,7 @@ mimalloc = { version = "0.1.42", default-features = false, optional = true } serde_json = { workspace = true } simplelog = "0.12" time = "0.3" +ureq_multipart = { workspace = true } [target.'cfg(not(target_os = "windows"))'.dependencies] # Our dependencies don't use OpenSSL on Windows diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index d1defd4327..77219d075c 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -90,6 +90,7 @@ titlecase = { workspace = true } toml = { workspace = true, features = ["preserve_order"]} unicode-segmentation = { workspace = true } ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"] } +ureq_multipart = { workspace = true } url = { workspace = true } uu_cp = { workspace = true } uu_mkdir = { workspace = true } @@ -146,4 +147,4 @@ quickcheck = { workspace = true } quickcheck_macros = { workspace = true } rstest = { workspace = true, default-features = false } pretty_assertions = { workspace = true } -tempfile = { workspace = true } \ No newline at end of file +tempfile = { workspace = true } diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index e348bedc25..2158b86430 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -8,18 +8,21 @@ use nu_engine::command_prelude::*; use nu_protocol::{ByteStream, Signals}; use std::{ collections::HashMap, + io::Cursor, path::PathBuf, str::FromStr, sync::mpsc::{self, RecvTimeoutError}, time::Duration, }; use ureq::{Error, ErrorKind, Request, Response}; +use ureq_multipart::MultipartBuilder; use url::Url; #[derive(PartialEq, Eq)] pub enum BodyType { Json, Form, + Multipart, Unknown, } @@ -210,6 +213,7 @@ pub fn send_request( let (body_type, req) = match content_type { Some(it) if it == "application/json" => (BodyType::Json, request), Some(it) if it == "application/x-www-form-urlencoded" => (BodyType::Form, request), + Some(it) if it == "multipart/form-data" => (BodyType::Multipart, request), Some(it) => { let r = request.clone().set("Content-Type", &it); (BodyType::Unknown, r) @@ -265,6 +269,35 @@ pub fn send_request( }; send_cancellable_request(&request_url, Box::new(request_fn), span, signals) } + // multipart form upload + Value::Record { val, .. } if body_type == BodyType::Multipart => { + let mut builder = MultipartBuilder::new(); + + let err = |e| { + ShellErrorOrRequestError::ShellError(ShellError::IOError { + msg: format!("failed to build multipart data: {}", e), + }) + }; + + for (col, val) in val.into_owned() { + if let Value::Binary { val, .. } = val { + builder = builder + .add_stream(&mut Cursor::new(val), &col, Some(&col), None) + .map_err(err)?; + } else { + builder = builder + .add_text(&col, &val.coerce_into_string()?) + .map_err(err)?; + } + } + + let (content_type, data) = builder.finish().map_err(err)?; + + let request_fn = + move || req.set("Content-Type", &content_type).send_bytes(&data); + + send_cancellable_request(&request_url, Box::new(request_fn), span, signals) + } Value::List { vals, .. } if body_type == BodyType::Form => { if vals.len() % 2 != 0 { return Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { diff --git a/crates/nu-command/src/network/http/post.rs b/crates/nu-command/src/network/http/post.rs index ea4ec093f3..a593c5489c 100644 --- a/crates/nu-command/src/network/http/post.rs +++ b/crates/nu-command/src/network/http/post.rs @@ -127,6 +127,11 @@ impl Command for SubCommand { example: "open foo.json | http post https://www.example.com", result: None, }, + Example { + description: "Upload a file to example.com", + example: "http post --content-type multipart/form-data https://www.example.com { audio: (open -r file.mp3) }", + result: None, + }, ] } } diff --git a/crates/nu-command/tests/commands/network/http/post.rs b/crates/nu-command/tests/commands/network/http/post.rs index 02aa8df23f..66ac2f029c 100644 --- a/crates/nu-command/tests/commands/network/http/post.rs +++ b/crates/nu-command/tests/commands/network/http/post.rs @@ -1,4 +1,4 @@ -use mockito::Server; +use mockito::{Matcher, Server, ServerOpts}; use nu_test_support::{nu, pipeline}; #[test] @@ -197,3 +197,33 @@ fn http_post_redirect_mode_error() { "Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)" )); } +#[test] +fn http_post_multipart_is_success() { + let mut server = Server::new_with_opts(ServerOpts { + assert_on_drop: true, + ..Default::default() + }); + let _mock = server + .mock("POST", "/") + .match_header( + "content-type", + Matcher::Regex("multipart/form-data; boundary=.*".to_string()), + ) + .match_body(Matcher::AllOf(vec![ + Matcher::Regex(r#"Content-Disposition: form-data; name="foo""#.to_string()), + Matcher::Regex(r#"Content-Type: application/octet-stream"#.to_string()), + Matcher::Regex(r#"bar"#.to_string()), + ])) + .with_status(200) + .create(); + + let actual = nu!(pipeline( + format!( + "http post --content-type multipart/form-data {url} {{foo: ('bar' | into binary) }}", + url = server.url() + ) + .as_str() + )); + + assert!(actual.out.is_empty()) +} From fc964074d3850def208dba4c23ddc67994ade6ec Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 4 Aug 2024 02:03:44 +0200 Subject: [PATCH 2/3] Replaced `ureq_multipart` with `multipart-rs` The latter seems maintained and does provide a license, unlike the former. --- Cargo.lock | 113 +++---------------- Cargo.toml | 4 +- crates/nu-command/Cargo.toml | 2 +- crates/nu-command/src/network/http/client.rs | 25 ++-- 4 files changed, 36 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbb1c6c5a4..a12c9ddb30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2748,6 +2748,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "multipart-rs" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ea34e5c0fa65ba84707cfaf5bf43d500f1c5a4c6c36327bf5541c5bcd17e98" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "memchr", + "mime", + "uuid", +] + [[package]] name = "multiversion" version = "0.7.4" @@ -2877,6 +2891,7 @@ dependencies = [ "log", "miette", "mimalloc", + "multipart-rs", "nix", "nu-cli", "nu-cmd-base", @@ -2908,7 +2923,6 @@ dependencies = [ "tango-bench", "tempfile", "time", - "ureq_multipart", "winresource", ] @@ -3065,6 +3079,7 @@ dependencies = [ "mime", "mime_guess", "mockito", + "multipart-rs", "native-tls", "nix", "notify-debouncer-full", @@ -3122,7 +3137,6 @@ dependencies = [ "unicode-segmentation", "unicode-width", "ureq", - "ureq_multipart", "url", "uu_cp", "uu_mkdir", @@ -5106,21 +5120,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rkyv" version = "0.7.44" @@ -5331,38 +5330,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.23.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" - -[[package]] -name = "rustls-webpki" -version = "0.102.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.15" @@ -5786,12 +5753,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "sqlparser" version = "0.47.0" @@ -5926,12 +5887,6 @@ dependencies = [ "syn 2.0.60", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "supports-color" version = "2.1.0" @@ -6543,12 +6498,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "ureq" version = "2.10.0" @@ -6561,24 +6510,9 @@ dependencies = [ "log", "native-tls", "once_cell", - "rustls", - "rustls-pki-types", "serde", "serde_json", "url", - "webpki-roots", -] - -[[package]] -name = "ureq_multipart" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22baf2d124865fc4d505f5942222a57f6a3eae8a133819d7fd6194423e3f6e91" -dependencies = [ - "mime", - "mime_guess", - "rand", - "ureq", ] [[package]] @@ -6984,15 +6918,6 @@ dependencies = [ "url", ] -[[package]] -name = "webpki-roots" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "6.0.1" @@ -7448,12 +7373,6 @@ dependencies = [ "syn 2.0.60", ] -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 92c3341658..db67c6f99e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,7 @@ miette = "7.2" mime = "0.3" mime_guess = "2.0" mockito = { version = "1.4", default-features = false } +multipart-rs = "0.1.11" native-tls = "0.2" nix = { version = "0.28", default-features = false } notify-debouncer-full = { version = "0.3", default-features = false } @@ -166,7 +167,6 @@ umask = "2.1" unicode-segmentation = "1.11" unicode-width = "0.1" ureq = { version = "2.10", default-features = false } -ureq_multipart = "1.1.1" url = "2.2" uu_cp = "0.0.27" uu_mkdir = "0.0.27" @@ -208,10 +208,10 @@ dirs = { workspace = true } log = { workspace = true } miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] } mimalloc = { version = "0.1.42", default-features = false, optional = true } +multipart-rs = { workspace = true } serde_json = { workspace = true } simplelog = "0.12" time = "0.3" -ureq_multipart = { workspace = true } [target.'cfg(not(target_os = "windows"))'.dependencies] # Our dependencies don't use OpenSSL on Windows diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 77219d075c..2459d74ad9 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -60,6 +60,7 @@ lscolors = { workspace = true, default-features = false, features = ["nu-ansi-te md5 = { workspace = true } mime = { workspace = true } mime_guess = { workspace = true } +multipart-rs = { workspace = true } native-tls = { workspace = true } notify-debouncer-full = { workspace = true, default-features = false } num-format = { workspace = true } @@ -90,7 +91,6 @@ titlecase = { workspace = true } toml = { workspace = true, features = ["preserve_order"]} unicode-segmentation = { workspace = true } ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"] } -ureq_multipart = { workspace = true } url = { workspace = true } uu_cp = { workspace = true } uu_mkdir = { workspace = true } diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index 2158b86430..4de40c92d6 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -4,6 +4,7 @@ use base64::{ engine::{general_purpose::PAD, GeneralPurpose}, Engine, }; +use multipart_rs::MultipartWriter; use nu_engine::command_prelude::*; use nu_protocol::{ByteStream, Signals}; use std::{ @@ -15,7 +16,6 @@ use std::{ time::Duration, }; use ureq::{Error, ErrorKind, Request, Response}; -use ureq_multipart::MultipartBuilder; use url::Url; #[derive(PartialEq, Eq)] @@ -271,7 +271,7 @@ pub fn send_request( } // multipart form upload Value::Record { val, .. } if body_type == BodyType::Multipart => { - let mut builder = MultipartBuilder::new(); + let mut builder = MultipartWriter::new(); let err = |e| { ShellErrorOrRequestError::ShellError(ShellError::IOError { @@ -281,17 +281,26 @@ pub fn send_request( for (col, val) in val.into_owned() { if let Value::Binary { val, .. } = val { - builder = builder - .add_stream(&mut Cursor::new(val), &col, Some(&col), None) - .map_err(err)?; + let headers = format!( + r#"Content-Type: application/octet-stream + Content-Disposition: form-data; name="{}"; filename="{}" + Content-Transfer-Encoding: binary + "#, + col, col, + ); + builder.add(&mut Cursor::new(val), &headers).map_err(err)?; } else { - builder = builder - .add_text(&col, &val.coerce_into_string()?) + let headers = + format!(r#"Content-Disposition: form-data; name="{}""#, col,); + builder + .add(val.coerce_into_string()?.as_bytes(), &headers) .map_err(err)?; } } + builder.finish(); - let (content_type, data) = builder.finish().map_err(err)?; + let (boundary, data) = (builder.boundary, builder.data); + let content_type = format!("multipart/form-data; boundary={}", boundary); let request_fn = move || req.set("Content-Type", &content_type).send_bytes(&data); From efd784353e515fe5afcc32eac7730381c2f7146c Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 4 Aug 2024 14:37:51 +0200 Subject: [PATCH 3/3] Fixed part headers formatting and added `Content-Length` --- crates/nu-command/src/network/http/client.rs | 22 +++++++++++-------- .../tests/commands/network/http/post.rs | 7 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index 4de40c92d6..7bb63a8fbd 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -281,17 +281,21 @@ pub fn send_request( for (col, val) in val.into_owned() { if let Value::Binary { val, .. } = val { - let headers = format!( - r#"Content-Type: application/octet-stream - Content-Disposition: form-data; name="{}"; filename="{}" - Content-Transfer-Encoding: binary - "#, - col, col, - ); - builder.add(&mut Cursor::new(val), &headers).map_err(err)?; + let headers = [ + "Content-Type: application/octet-stream".to_string(), + "Content-Transfer-Encoding: binary".to_string(), + format!( + "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"", + col, col + ), + format!("Content-Length: {}", val.len()), + ]; + builder + .add(&mut Cursor::new(val), &headers.join("\n")) + .map_err(err)?; } else { let headers = - format!(r#"Content-Disposition: form-data; name="{}""#, col,); + format!(r#"Content-Disposition: form-data; name="{}""#, col); builder .add(val.coerce_into_string()?.as_bytes(), &headers) .map_err(err)?; diff --git a/crates/nu-command/tests/commands/network/http/post.rs b/crates/nu-command/tests/commands/network/http/post.rs index 66ac2f029c..11db87168e 100644 --- a/crates/nu-command/tests/commands/network/http/post.rs +++ b/crates/nu-command/tests/commands/network/http/post.rs @@ -210,9 +210,10 @@ fn http_post_multipart_is_success() { Matcher::Regex("multipart/form-data; boundary=.*".to_string()), ) .match_body(Matcher::AllOf(vec![ - Matcher::Regex(r#"Content-Disposition: form-data; name="foo""#.to_string()), - Matcher::Regex(r#"Content-Type: application/octet-stream"#.to_string()), - Matcher::Regex(r#"bar"#.to_string()), + Matcher::Regex(r#"(?m)^Content-Disposition: form-data; name="foo""#.to_string()), + Matcher::Regex(r#"(?m)^Content-Type: application/octet-stream"#.to_string()), + Matcher::Regex(r#"(?m)^Content-Length: 3"#.to_string()), + Matcher::Regex(r#"(?m)^bar"#.to_string()), ])) .with_status(200) .create();