pub struct Client { /* private fields */ }Expand description
HTTP client for making typed requests against a Target server.
A Client is created in one of three modes depending on which send variants you need:
| Constructor | send() (async) | send_sync() (blocking) | Safe in async context |
|---|---|---|---|
Client::new_async | ✓ | ✗ | ✓ |
Client::new_sync | ✗ | ✓ | ✓ |
Client::new | ✓ | ✓ | ✗ — panics if called inside a tokio runtime |
reqwest::blocking::Client internally creates its own single-threaded tokio runtime. If
you call Client::new() (or Client::new_sync()) from within an existing async context
(e.g. inside #[tokio::main]) it will panic. Use Client::new_async() when your program
is async-first and only call Client::new_sync() / Client::new() before entering any
async runtime.
§Example
// Async-only client — safe inside #[tokio::main]
let client = Client::new_async(Target::Localhost(8080));
let items: Vec<Item> = client.get("/items").send().await?;
let created: Item = client.post("/items").json(NewItem { name: "w".into() }).send().await?;Implementations§
Source§impl Client
impl Client
Sourcepub fn new_async(target: Target) -> Self
pub fn new_async(target: Target) -> Self
Creates an async-only client. Safe to call from any context, including inside
#[tokio::main]. Calling .send_sync() on builders from this client will panic.
Sourcepub fn new_sync(target: Target) -> Self
pub fn new_sync(target: Target) -> Self
Creates a sync-only client. Must not be called from within an async context
(inside #[tokio::main] or similar) — doing so panics. Calling .send() on builders
from this client will panic with a message pointing to Client::new_async.
§Panics
Panics at construction time if called inside a tokio runtime (same restriction as
reqwest::blocking::Client). Prefer Client::new_async for async contexts.
Sourcepub fn new(target: Target) -> Self
pub fn new(target: Target) -> Self
Creates a client supporting both async and blocking sends.
§Panics
Panics immediately if called from within an async context (e.g. inside
#[tokio::main], tokio::spawn, or any .await call chain). This happens because
reqwest::blocking::Client creates its own internal tokio runtime, and Rust/tokio
forbids nesting two runtimes in the same thread.
If you are in an async context, use Client::new_async instead.
If you only need blocking calls, use Client::new_sync before entering any runtime.
§Example
// Correct — called from synchronous main before any async runtime starts
fn main() {
let client = Client::new(Target::Localhost(8080));
// ... use client.send_sync() and client.send() via manual runtime
}
// WRONG — will panic at runtime:
// #[tokio::main]
// async fn main() { let client = Client::new(...); } // panics!Sourcepub fn get(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn get(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a GET request builder for endpoint.
Examples found in repository?
62async fn main() -> Result<(), Box<dyn std::error::Error>> {
63 // ── Build routes ──────────────────────────────────────────────────────────
64 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
65
66 let mut server = Server::default();
67
68 // Plain GET — no body, no state.
69 server.mechanism(
70 ServerMechanism::get("/health")
71 .onconnect(|| async { reply!(json => Health { ok: true }) })
72 );
73
74 // POST with a JSON body — echoes back the created item.
75 server.mechanism(
76 ServerMechanism::post("/items")
77 .json::<NewItem>()
78 .onconnect(|body: NewItem| async move {
79 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
80 })
81 );
82
83 // GET with query parameters — filters the stored items by prefix.
84 server.mechanism({
85 let store = store.clone();
86 ServerMechanism::get("/items/search")
87 .state(store)
88 .query::<Filter>()
89 .onconnect(|state: Arc<Mutex<Vec<Item>>>, f: Filter| async move {
90 let items = state.lock().unwrap();
91 let matches: Vec<Item> = items
92 .iter()
93 .filter(|i| i.name.starts_with(&f.prefix))
94 .cloned()
95 .collect();
96 reply!(json => SearchResult { matches })
97 })
98 });
99
100 // POST with shared state — stores the item and returns it.
101 server.mechanism({
102 let store = store.clone();
103 ServerMechanism::post("/items/store")
104 .state(store)
105 .json::<NewItem>()
106 .onconnect(|state: Arc<Mutex<Vec<Item>>>, body: NewItem| async move {
107 let mut s = state.lock().unwrap();
108 let id = s.len() as u32 + 1;
109 let item = Item { id, name: body.name };
110 s.push(item.clone());
111 reply!(json => item, status => Status::Created)
112 })
113 });
114
115 // POST with authenticated-encrypted body (ChaCha20-Poly1305 via SerializationKey).
116 // The body is decrypted before the handler is called; a wrong key returns 403.
117 server.mechanism(
118 ServerMechanism::post("/items/secure")
119 .encryption::<NewItem>(SerializationKey::Default)
120 .onconnect(|body: NewItem| async move {
121 let item = Item { id: 99, name: body.name };
122 // The response must also be sealed so the client can open it.
123 reply!(sealed => item, key => SerializationKey::Default)
124 })
125 );
126
127 // ── Serve with graceful shutdown ──────────────────────────────────────────
128 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
129 let server_handle = tokio::spawn(async move {
130 server.serve_with_graceful_shutdown(
131 ([127, 0, 0, 1], PORT),
132 async { rx.await.ok(); },
133 ).await;
134 });
135
136 // Give the server time to bind before firing requests.
137 tokio::time::sleep(Duration::from_millis(200)).await;
138 println!("Server started on port {PORT}");
139
140 // ── Client requests ───────────────────────────────────────────────────────
141 let client = ClientBuilder::new(Target::Localhost(PORT))
142 .timeout(Duration::from_secs(5))
143 .build_async();
144
145 // GET /health
146 let health: Health = client.get("/health").send().await?;
147 assert!(health.ok);
148 println!("GET /health → ok={}", health.ok);
149
150 // POST /items (JSON body)
151 let created: Item = client
152 .post("/items")
153 .json(NewItem { name: "widget".into() })
154 .send()
155 .await?;
156 assert_eq!(created.name, "widget");
157 println!("POST /items → {:?}", created);
158
159 // POST /items/store (shared state — populates the store)
160 let stored: Item = client
161 .post("/items/store")
162 .json(NewItem { name: "gadget".into() })
163 .send()
164 .await?;
165 println!("POST /items/store → {:?}", stored);
166
167 let stored2: Item = client
168 .post("/items/store")
169 .json(NewItem { name: "gizmo".into() })
170 .send()
171 .await?;
172 println!("POST /items/store → {:?}", stored2);
173
174 // GET /items/search?prefix=ga (query params)
175 let result: SearchResult = client
176 .get("/items/search")
177 .query(Filter { prefix: "ga".into() })
178 .send()
179 .await?;
180 assert!(result.matches.iter().all(|i| i.name.starts_with("ga")));
181 println!("GET /items/search?prefix=ga → {} match(es): {:?}", result.matches.len(), result.matches);
182
183 // POST /items/secure (authenticated-encrypted body)
184 let secure_item = client
185 .post("/items/secure")
186 .encryption(NewItem { name: "secret".into() }, SerializationKey::Default)
187 .send::<Item>()
188 .await?;
189 assert_eq!(secure_item.name, "secret");
190 println!("POST /items/secure → {:?}", secure_item);
191
192 // ── Shutdown ──────────────────────────────────────────────────────────────
193 tx.send(()).ok();
194 server_handle.await?;
195 println!("\nAll requests successful ✓");
196 Ok(())
197}Sourcepub fn post(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn post(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a POST request builder for endpoint.
Examples found in repository?
62async fn main() -> Result<(), Box<dyn std::error::Error>> {
63 // ── Build routes ──────────────────────────────────────────────────────────
64 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
65
66 let mut server = Server::default();
67
68 // Plain GET — no body, no state.
69 server.mechanism(
70 ServerMechanism::get("/health")
71 .onconnect(|| async { reply!(json => Health { ok: true }) })
72 );
73
74 // POST with a JSON body — echoes back the created item.
75 server.mechanism(
76 ServerMechanism::post("/items")
77 .json::<NewItem>()
78 .onconnect(|body: NewItem| async move {
79 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
80 })
81 );
82
83 // GET with query parameters — filters the stored items by prefix.
84 server.mechanism({
85 let store = store.clone();
86 ServerMechanism::get("/items/search")
87 .state(store)
88 .query::<Filter>()
89 .onconnect(|state: Arc<Mutex<Vec<Item>>>, f: Filter| async move {
90 let items = state.lock().unwrap();
91 let matches: Vec<Item> = items
92 .iter()
93 .filter(|i| i.name.starts_with(&f.prefix))
94 .cloned()
95 .collect();
96 reply!(json => SearchResult { matches })
97 })
98 });
99
100 // POST with shared state — stores the item and returns it.
101 server.mechanism({
102 let store = store.clone();
103 ServerMechanism::post("/items/store")
104 .state(store)
105 .json::<NewItem>()
106 .onconnect(|state: Arc<Mutex<Vec<Item>>>, body: NewItem| async move {
107 let mut s = state.lock().unwrap();
108 let id = s.len() as u32 + 1;
109 let item = Item { id, name: body.name };
110 s.push(item.clone());
111 reply!(json => item, status => Status::Created)
112 })
113 });
114
115 // POST with authenticated-encrypted body (ChaCha20-Poly1305 via SerializationKey).
116 // The body is decrypted before the handler is called; a wrong key returns 403.
117 server.mechanism(
118 ServerMechanism::post("/items/secure")
119 .encryption::<NewItem>(SerializationKey::Default)
120 .onconnect(|body: NewItem| async move {
121 let item = Item { id: 99, name: body.name };
122 // The response must also be sealed so the client can open it.
123 reply!(sealed => item, key => SerializationKey::Default)
124 })
125 );
126
127 // ── Serve with graceful shutdown ──────────────────────────────────────────
128 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
129 let server_handle = tokio::spawn(async move {
130 server.serve_with_graceful_shutdown(
131 ([127, 0, 0, 1], PORT),
132 async { rx.await.ok(); },
133 ).await;
134 });
135
136 // Give the server time to bind before firing requests.
137 tokio::time::sleep(Duration::from_millis(200)).await;
138 println!("Server started on port {PORT}");
139
140 // ── Client requests ───────────────────────────────────────────────────────
141 let client = ClientBuilder::new(Target::Localhost(PORT))
142 .timeout(Duration::from_secs(5))
143 .build_async();
144
145 // GET /health
146 let health: Health = client.get("/health").send().await?;
147 assert!(health.ok);
148 println!("GET /health → ok={}", health.ok);
149
150 // POST /items (JSON body)
151 let created: Item = client
152 .post("/items")
153 .json(NewItem { name: "widget".into() })
154 .send()
155 .await?;
156 assert_eq!(created.name, "widget");
157 println!("POST /items → {:?}", created);
158
159 // POST /items/store (shared state — populates the store)
160 let stored: Item = client
161 .post("/items/store")
162 .json(NewItem { name: "gadget".into() })
163 .send()
164 .await?;
165 println!("POST /items/store → {:?}", stored);
166
167 let stored2: Item = client
168 .post("/items/store")
169 .json(NewItem { name: "gizmo".into() })
170 .send()
171 .await?;
172 println!("POST /items/store → {:?}", stored2);
173
174 // GET /items/search?prefix=ga (query params)
175 let result: SearchResult = client
176 .get("/items/search")
177 .query(Filter { prefix: "ga".into() })
178 .send()
179 .await?;
180 assert!(result.matches.iter().all(|i| i.name.starts_with("ga")));
181 println!("GET /items/search?prefix=ga → {} match(es): {:?}", result.matches.len(), result.matches);
182
183 // POST /items/secure (authenticated-encrypted body)
184 let secure_item = client
185 .post("/items/secure")
186 .encryption(NewItem { name: "secret".into() }, SerializationKey::Default)
187 .send::<Item>()
188 .await?;
189 assert_eq!(secure_item.name, "secret");
190 println!("POST /items/secure → {:?}", secure_item);
191
192 // ── Shutdown ──────────────────────────────────────────────────────────────
193 tx.send(()).ok();
194 server_handle.await?;
195 println!("\nAll requests successful ✓");
196 Ok(())
197}Sourcepub fn put(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn put(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a PUT request builder for endpoint.
Sourcepub fn delete(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn delete(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a DELETE request builder for endpoint.
Sourcepub fn patch(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn patch(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a PATCH request builder for endpoint.
Sourcepub fn head(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn head(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts a HEAD request builder for endpoint.
Sourcepub fn options(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
pub fn options(&self, endpoint: impl Into<String>) -> RequestBuilder<'_>
Starts an OPTIONS request builder for endpoint.