Skip to main content

shape_runtime/stdlib/
http.rs

1//! Native `http` module for making HTTP requests.
2//!
3//! Exports: http.get, http.delete (Stage C); http.post_text,
4//! http.post_bytes, http.put_text, http.put_bytes (Stage D).
5//!
6//! All functions are async. Uses reqwest under the hood.
7//! Policy gated: requires NetConnect permission.
8//!
9//! Stage C HashMap-marshal P1(b) migration (2026-05-07):
10//! - Outer response shape `{status, headers, body, ok}` returns via
11//!   `TypedReturn::OkObjectPairs` (Cluster #4 β shape, mirrors
12//!   `arrow.metadata` precedent at `arrow_module.rs:127`).
13//! - Inner `headers` field carries `HashMap<string, string>` payload via
14//!   `ConcreteReturn::HashMapStringString` (insertion-order preserved).
15//! - Options arg parsing uses `Vec<(Arc<String>, Arc<HeapValue>)>`
16//!   FromSlot impl from Step 1 P1(b) infrastructure
17//!   (`crates/shape-runtime/src/marshal.rs`, Stage C commit `36519f6`).
18//!
19//! Stage D N4 partial sign-off (2026-05-07; supervisor relay):
20//! - `http.post`/`http.put` legacy shape (single fn with `body: any`)
21//!   replaced by typed overloads via Shape API split
22//!   (`stdlib-src/core/http.shape`):
23//!     - `post_text(url, body: string, options)` — sets
24//!       `Content-Type: text/plain; charset=utf-8`
25//!     - `post_bytes(url, body: Array<int>, options)` — sets
26//!       `Content-Type: application/octet-stream`
27//!     - `put_text(url, body: string, options)` — same content-type as
28//!       post_text
29//!     - `put_bytes(url, body: Array<int>, options)` — same as post_bytes
30//! - Body types map directly to existing `FromSlot` impls
31//!   (`Arc<String>` at `marshal.rs:129`, `Vec<u8>` at `marshal.rs:330`)
32//!   per supervisor's "mechanical typed marshal" framing.
33//! - `http.post_json(url, body: object, options)` and `http.put_json`
34//!   remain DEFERRED pending architectural sub-decision **N7 —
35//!   HeapValue→JSON serializer for HTTP / object-output marshal
36//!   contexts.** The `body: object` shape requires walking the
37//!   polymorphic `Vec<(Arc<String>, Arc<HeapValue>)>` tree and producing
38//!   a JSON string; per-variant serialization choices for Decimal,
39//!   DataTable, Content, Temporal, TableView each represent a
40//!   user-visible behavioral commitment that needs supervisor sign-off
41//!   (architectural-adjacent helper, refused as bundled with Step 2 per
42//!   the "no bundling architectural decisions" watchlist refusal).
43//!   Surfaced via team-lead's relay batch.
44//!
45//! Tests deleted along with the legacy ValueWord-based fixtures, mirroring
46//! the csv_module migration (commit `9f6b1d3`). New typed-marshal test
47//! harness arrives with the shape-vm cleanup workstream.
48
49use crate::marshal::register_typed_async_fn_3_full;
50use crate::marshal::register_typed_async_fn_2_full;
51use crate::module_exports::{ModuleExports, ModuleParam};
52use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
53use shape_value::heap_value::HeapValue;
54use std::sync::Arc;
55
56/// Build the schemaful HttpResponse pair-list returned by every http.*
57/// function. Schema: `{status: int, headers: HashMap<string, string>,
58/// body: string, ok: bool}`. Insertion order preserved per `ObjectPairs`
59/// contract (`crates/shape-runtime/src/typed_module_exports.rs:117`).
60fn build_response_pairs(
61    status: u16,
62    headers: Vec<(String, String)>,
63    body: String,
64) -> Vec<(String, ConcreteReturn)> {
65    vec![
66        ("status".to_string(), ConcreteReturn::I64(status as i64)),
67        (
68            "headers".to_string(),
69            ConcreteReturn::HashMapStringString(headers),
70        ),
71        ("body".to_string(), ConcreteReturn::String(body)),
72        (
73            "ok".to_string(),
74            ConcreteReturn::Bool((200..300).contains(&status)),
75        ),
76    ]
77}
78
79/// Extract optional headers from an `options: HashMap<string, *>` arg.
80/// The options HashMap may contain a `"headers"` key whose value is itself
81/// a `HashMap<string, string>` (`HeapValue::HashMap` variant). Walks the
82/// outer pair list linearly looking for `"headers"`, then reads the
83/// nested HashMap's keys/values buffers.
84fn extract_headers(options: &[(Arc<String>, Arc<HeapValue>)]) -> Vec<(String, String)> {
85    for (k, v) in options.iter() {
86        if k.as_str() == "headers" {
87            if let HeapValue::HashMap(kref) = &**v {
88                // Wave 2 Round 3b C2-joint ckpt-4 (2026-05-14): per-V walk
89                // for HashMap<string, string> (the canonical headers shape).
90                // Other V variants return empty (caller's contract is
91                // string headers; non-string Vs are a producer-side bug).
92                use shape_value::heap_value::HashMapKindedRef;
93                if let HashMapKindedRef::String(arc) = kref {
94                    let n = arc.len();
95                    let mut out = Vec::with_capacity(n);
96                    for i in 0..n {
97                        let key: String = unsafe {
98                            let ptr = shape_value::v2::typed_array::TypedArray::get_unchecked(
99                                arc.keys, i as u32,
100                            );
101                            shape_value::v2::string_obj::StringObj::as_str(ptr).to_owned()
102                        };
103                        let val: String = unsafe {
104                            let v_ptr: *const shape_value::v2::string_obj::StringObj =
105                                *(*arc.values).data.add(i);
106                            shape_value::v2::string_obj::StringObj::as_str(v_ptr).to_owned()
107                        };
108                        out.push((key, val));
109                    }
110                    return out;
111                }
112                return Vec::new();
113            }
114        }
115    }
116    Vec::new()
117}
118
119/// Extract optional `timeout` (milliseconds) from the options HashMap.
120/// Walks linearly for `"timeout"`; if present and integer, converts to a
121/// `Duration`.
122///
123/// Currently accepts `HeapValue::BigInt` (i64-typed integer) values only.
124/// `number`-typed (f64) timeout values surface as raw scalar slots in
125/// post-bulldozer Shape and don't reach `HeapValue` — supporting them
126/// would require either Shape user code passing an int (`5000` not
127/// `5000.0`) OR a future `HeapValue::NativeScalar`-aware branch here.
128/// Documented for follow-on if a consumer surfaces.
129fn extract_timeout(
130    options: &[(Arc<String>, Arc<HeapValue>)],
131) -> Option<std::time::Duration> {
132    for (k, v) in options.iter() {
133        if k.as_str() == "timeout" {
134            if let HeapValue::BigInt(ms) = &**v {
135                let n = **ms;
136                if n > 0 {
137                    return Some(std::time::Duration::from_millis(n as u64));
138                }
139            }
140        }
141    }
142    None
143}
144
145/// Create the `http` module with async HTTP request functions.
146pub fn create_http_module() -> ModuleExports {
147    let mut module = ModuleExports::new("std::core::http");
148    module.description = "HTTP client for making web requests".to_string();
149
150    let url_param = ModuleParam {
151        name: "url".to_string(),
152        type_name: "string".to_string(),
153        required: true,
154        description: "URL to request".to_string(),
155        ..Default::default()
156    };
157
158    let options_param = ModuleParam {
159        name: "options".to_string(),
160        type_name: "HashMap<string, any>".to_string(),
161        required: false,
162        description: "Request options: { headers?: HashMap, timeout?: int }"
163            .to_string(),
164        default_snippet: Some("{}".to_string()),
165        ..Default::default()
166    };
167
168    let response_ty =
169        ConcreteType::Result(Box::new(ConcreteType::Named("HttpResponse".to_string())));
170
171    // http.get(url: string, options?: HashMap) -> Result<HttpResponse>
172    register_typed_async_fn_2_full::<_, _, Arc<String>, Vec<(Arc<String>, Arc<HeapValue>)>>(
173        &mut module,
174        "get",
175        "Perform an HTTP GET request",
176        [url_param.clone(), options_param.clone()],
177        response_ty.clone(),
178        |url: Arc<String>, options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
179            let mut builder = reqwest::Client::new().get(url.as_str());
180
181            for (k, v) in extract_headers(&options) {
182                builder = builder.header(&k, &v);
183            }
184            if let Some(timeout) = extract_timeout(&options) {
185                builder = builder.timeout(timeout);
186            }
187
188            let resp = builder
189                .send()
190                .await
191                .map_err(|e| format!("http.get() failed: {}", e))?;
192
193            let status = resp.status().as_u16();
194            let headers: Vec<(String, String)> = resp
195                .headers()
196                .iter()
197                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
198                .collect();
199            let body = resp
200                .text()
201                .await
202                .map_err(|e| format!("http.get() body read failed: {}", e))?;
203
204            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
205                status, headers, body,
206            )))
207        },
208    );
209
210    // http.delete(url: string, options?: HashMap) -> Result<HttpResponse>
211    register_typed_async_fn_2_full::<_, _, Arc<String>, Vec<(Arc<String>, Arc<HeapValue>)>>(
212        &mut module,
213        "delete",
214        "Perform an HTTP DELETE request",
215        [url_param, options_param],
216        response_ty,
217        |url: Arc<String>, options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
218            let mut builder = reqwest::Client::new().delete(url.as_str());
219
220            for (k, v) in extract_headers(&options) {
221                builder = builder.header(&k, &v);
222            }
223            if let Some(timeout) = extract_timeout(&options) {
224                builder = builder.timeout(timeout);
225            }
226
227            let resp = builder
228                .send()
229                .await
230                .map_err(|e| format!("http.delete() failed: {}", e))?;
231
232            let status = resp.status().as_u16();
233            let headers: Vec<(String, String)> = resp
234                .headers()
235                .iter()
236                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
237                .collect();
238            let body = resp
239                .text()
240                .await
241                .map_err(|e| format!("http.delete() body read failed: {}", e))?;
242
243            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
244                status, headers, body,
245            )))
246        },
247    );
248
249    // Stage D N4 partial sign-off: 4 typed overloads via Shape API
250    // split. Each body type is a fixed-arity register_typed_async_fn_3
251    // with one specific body type per overload, per supervisor's
252    // "mechanical typed marshal" framing. Reuses build_response_pairs +
253    // extract_headers + extract_timeout from the get/delete path.
254
255    let url_param_3 = ModuleParam {
256        name: "url".to_string(),
257        type_name: "string".to_string(),
258        required: true,
259        description: "URL to request".to_string(),
260        ..Default::default()
261    };
262    let options_param_3 = ModuleParam {
263        name: "options".to_string(),
264        type_name: "HashMap<string, any>".to_string(),
265        required: false,
266        description: "Request options: { headers?: HashMap, timeout?: int }"
267            .to_string(),
268        default_snippet: Some("{}".to_string()),
269        ..Default::default()
270    };
271    let body_text_param = ModuleParam {
272        name: "body".to_string(),
273        type_name: "string".to_string(),
274        required: true,
275        description: "Request body as a string (sent verbatim)".to_string(),
276        ..Default::default()
277    };
278    let body_bytes_param = ModuleParam {
279        name: "body".to_string(),
280        type_name: "Array<int>".to_string(),
281        required: true,
282        description: "Request body as a byte array".to_string(),
283        ..Default::default()
284    };
285    let response_ty_3 =
286        ConcreteType::Result(Box::new(ConcreteType::Named("HttpResponse".to_string())));
287
288    // http.post_text(url: string, body: string, options?: HashMap) -> Result<HttpResponse>
289    register_typed_async_fn_3_full::<
290        _,
291        _,
292        Arc<String>,
293        Arc<String>,
294        Vec<(Arc<String>, Arc<HeapValue>)>,
295    >(
296        &mut module,
297        "post_text",
298        "Perform an HTTP POST request with a text body",
299        [
300            url_param_3.clone(),
301            body_text_param.clone(),
302            options_param_3.clone(),
303        ],
304        response_ty_3.clone(),
305        |url: Arc<String>,
306         body: Arc<String>,
307         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
308            let mut builder = reqwest::Client::new()
309                .post(url.as_str())
310                .header(
311                    reqwest::header::CONTENT_TYPE,
312                    "text/plain; charset=utf-8",
313                )
314                .body(body.as_str().to_string());
315
316            for (k, v) in extract_headers(&options) {
317                builder = builder.header(&k, &v);
318            }
319            if let Some(timeout) = extract_timeout(&options) {
320                builder = builder.timeout(timeout);
321            }
322
323            let resp = builder
324                .send()
325                .await
326                .map_err(|e| format!("http.post_text() failed: {}", e))?;
327
328            let status = resp.status().as_u16();
329            let headers: Vec<(String, String)> = resp
330                .headers()
331                .iter()
332                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
333                .collect();
334            let body_out = resp
335                .text()
336                .await
337                .map_err(|e| format!("http.post_text() body read failed: {}", e))?;
338
339            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
340                status, headers, body_out,
341            )))
342        },
343    );
344
345    // http.post_bytes(url: string, body: Array<int>, options?: HashMap) -> Result<HttpResponse>
346    register_typed_async_fn_3_full::<
347        _,
348        _,
349        Arc<String>,
350        Vec<u8>,
351        Vec<(Arc<String>, Arc<HeapValue>)>,
352    >(
353        &mut module,
354        "post_bytes",
355        "Perform an HTTP POST request with a binary body",
356        [
357            url_param_3.clone(),
358            body_bytes_param.clone(),
359            options_param_3.clone(),
360        ],
361        response_ty_3.clone(),
362        |url: Arc<String>,
363         body: Vec<u8>,
364         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
365            let mut builder = reqwest::Client::new()
366                .post(url.as_str())
367                .header(
368                    reqwest::header::CONTENT_TYPE,
369                    "application/octet-stream",
370                )
371                .body(body);
372
373            for (k, v) in extract_headers(&options) {
374                builder = builder.header(&k, &v);
375            }
376            if let Some(timeout) = extract_timeout(&options) {
377                builder = builder.timeout(timeout);
378            }
379
380            let resp = builder
381                .send()
382                .await
383                .map_err(|e| format!("http.post_bytes() failed: {}", e))?;
384
385            let status = resp.status().as_u16();
386            let headers: Vec<(String, String)> = resp
387                .headers()
388                .iter()
389                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
390                .collect();
391            let body_out = resp
392                .text()
393                .await
394                .map_err(|e| format!("http.post_bytes() body read failed: {}", e))?;
395
396            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
397                status, headers, body_out,
398            )))
399        },
400    );
401
402    // http.put_text(url: string, body: string, options?: HashMap) -> Result<HttpResponse>
403    register_typed_async_fn_3_full::<
404        _,
405        _,
406        Arc<String>,
407        Arc<String>,
408        Vec<(Arc<String>, Arc<HeapValue>)>,
409    >(
410        &mut module,
411        "put_text",
412        "Perform an HTTP PUT request with a text body",
413        [
414            url_param_3.clone(),
415            body_text_param,
416            options_param_3.clone(),
417        ],
418        response_ty_3.clone(),
419        |url: Arc<String>,
420         body: Arc<String>,
421         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
422            let mut builder = reqwest::Client::new()
423                .put(url.as_str())
424                .header(
425                    reqwest::header::CONTENT_TYPE,
426                    "text/plain; charset=utf-8",
427                )
428                .body(body.as_str().to_string());
429
430            for (k, v) in extract_headers(&options) {
431                builder = builder.header(&k, &v);
432            }
433            if let Some(timeout) = extract_timeout(&options) {
434                builder = builder.timeout(timeout);
435            }
436
437            let resp = builder
438                .send()
439                .await
440                .map_err(|e| format!("http.put_text() failed: {}", e))?;
441
442            let status = resp.status().as_u16();
443            let headers: Vec<(String, String)> = resp
444                .headers()
445                .iter()
446                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
447                .collect();
448            let body_out = resp
449                .text()
450                .await
451                .map_err(|e| format!("http.put_text() body read failed: {}", e))?;
452
453            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
454                status, headers, body_out,
455            )))
456        },
457    );
458
459    // http.put_bytes(url: string, body: Array<int>, options?: HashMap) -> Result<HttpResponse>
460    register_typed_async_fn_3_full::<
461        _,
462        _,
463        Arc<String>,
464        Vec<u8>,
465        Vec<(Arc<String>, Arc<HeapValue>)>,
466    >(
467        &mut module,
468        "put_bytes",
469        "Perform an HTTP PUT request with a binary body",
470        [url_param_3, body_bytes_param, options_param_3],
471        response_ty_3,
472        |url: Arc<String>,
473         body: Vec<u8>,
474         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
475            let mut builder = reqwest::Client::new()
476                .put(url.as_str())
477                .header(
478                    reqwest::header::CONTENT_TYPE,
479                    "application/octet-stream",
480                )
481                .body(body);
482
483            for (k, v) in extract_headers(&options) {
484                builder = builder.header(&k, &v);
485            }
486            if let Some(timeout) = extract_timeout(&options) {
487                builder = builder.timeout(timeout);
488            }
489
490            let resp = builder
491                .send()
492                .await
493                .map_err(|e| format!("http.put_bytes() failed: {}", e))?;
494
495            let status = resp.status().as_u16();
496            let headers: Vec<(String, String)> = resp
497                .headers()
498                .iter()
499                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
500                .collect();
501            let body_out = resp
502                .text()
503                .await
504                .map_err(|e| format!("http.put_bytes() body read failed: {}", e))?;
505
506            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
507                status, headers, body_out,
508            )))
509        },
510    );
511
512    // N7 ε disposition (REFINEMENT-3A γ + a, 2026-05-07): post_json /
513    // put_json take `body: object` which lands in this body as
514    // `Vec<(Arc<String>, Arc<HeapValue>)>` via the existing FromSlot
515    // impl at `crates/shape-runtime/src/marshal.rs:624`
516    // (`NATIVE_KIND = NativeKind::Ptr(HeapKind::HashMap)` — supervisor's
517    // load-bearing finding: the HashMap container anchors the slot kind
518    // structurally, side-stepping the N4-α single-any wildcard refusal
519    // that blocks the 5 single-any consumers C7/C10-C13 deferred to the
520    // n7-single-any-input-resolution follow-on workstream).
521    //
522    // Body algorithm: build `JsonValue::Object` by walking each HashMap
523    // pair via `heap_to_json_value(&v)?` (C2) → `json_value_to_serde_json`
524    // (C3) → `serde_json::to_string(&serde_json_v)?` → reqwest body with
525    // `Content-Type: application/json`. Insertion order preserved per
526    // ObjectPairs contract.
527
528    let url_param_post_json = ModuleParam {
529        name: "url".to_string(),
530        type_name: "string".to_string(),
531        required: true,
532        description: "URL to request".to_string(),
533        ..Default::default()
534    };
535    let body_object_param_post = ModuleParam {
536        name: "body".to_string(),
537        type_name: "object".to_string(),
538        required: true,
539        description: "Request body as an object (sent as JSON)".to_string(),
540        ..Default::default()
541    };
542    let options_param_post_json = ModuleParam {
543        name: "options".to_string(),
544        type_name: "HashMap<string, any>".to_string(),
545        required: false,
546        description: "Request options: { headers?: HashMap, timeout?: int }"
547            .to_string(),
548        default_snippet: Some("{}".to_string()),
549        ..Default::default()
550    };
551    let response_ty_post_json =
552        ConcreteType::Result(Box::new(ConcreteType::Named("HttpResponse".to_string())));
553
554    // http.post_json(url: string, body: object, options?: HashMap) -> Result<HttpResponse>
555    register_typed_async_fn_3_full::<
556        _,
557        _,
558        Arc<String>,
559        Vec<(Arc<String>, Arc<HeapValue>)>,
560        Vec<(Arc<String>, Arc<HeapValue>)>,
561    >(
562        &mut module,
563        "post_json",
564        "Perform an HTTP POST request with a JSON body",
565        [
566            url_param_post_json.clone(),
567            body_object_param_post,
568            options_param_post_json.clone(),
569        ],
570        response_ty_post_json.clone(),
571        |url: Arc<String>,
572         body: Vec<(Arc<String>, Arc<HeapValue>)>,
573         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
574            let mut json_pairs: Vec<(String, crate::json_value::JsonValue)> =
575                Vec::with_capacity(body.len());
576            for (k, v) in body.iter() {
577                json_pairs.push(((**k).clone(), crate::json_value::heap_to_json_value(v)?));
578            }
579            let json_value = crate::json_value::JsonValue::Object(json_pairs);
580            let serde_json_v = crate::json_value::json_value_to_serde_json(&json_value);
581            let body_str = serde_json::to_string(&serde_json_v)
582                .map_err(|e| format!("http.post_json() body serialization failed: {}", e))?;
583
584            let mut builder = reqwest::Client::new()
585                .post(url.as_str())
586                .header(reqwest::header::CONTENT_TYPE, "application/json")
587                .body(body_str);
588
589            for (k, v) in extract_headers(&options) {
590                builder = builder.header(&k, &v);
591            }
592            if let Some(timeout) = extract_timeout(&options) {
593                builder = builder.timeout(timeout);
594            }
595
596            let resp = builder
597                .send()
598                .await
599                .map_err(|e| format!("http.post_json() failed: {}", e))?;
600
601            let status = resp.status().as_u16();
602            let headers: Vec<(String, String)> = resp
603                .headers()
604                .iter()
605                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
606                .collect();
607            let body_out = resp
608                .text()
609                .await
610                .map_err(|e| format!("http.post_json() body read failed: {}", e))?;
611
612            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
613                status, headers, body_out,
614            )))
615        },
616    );
617
618    // http.put_json(url: string, body: object, options?: HashMap) -> Result<HttpResponse>
619    let body_object_param_put = ModuleParam {
620        name: "body".to_string(),
621        type_name: "object".to_string(),
622        required: true,
623        description: "Request body as an object (sent as JSON)".to_string(),
624        ..Default::default()
625    };
626    register_typed_async_fn_3_full::<
627        _,
628        _,
629        Arc<String>,
630        Vec<(Arc<String>, Arc<HeapValue>)>,
631        Vec<(Arc<String>, Arc<HeapValue>)>,
632    >(
633        &mut module,
634        "put_json",
635        "Perform an HTTP PUT request with a JSON body",
636        [url_param_post_json, body_object_param_put, options_param_post_json],
637        response_ty_post_json,
638        |url: Arc<String>,
639         body: Vec<(Arc<String>, Arc<HeapValue>)>,
640         options: Vec<(Arc<String>, Arc<HeapValue>)>| async move {
641            let mut json_pairs: Vec<(String, crate::json_value::JsonValue)> =
642                Vec::with_capacity(body.len());
643            for (k, v) in body.iter() {
644                json_pairs.push(((**k).clone(), crate::json_value::heap_to_json_value(v)?));
645            }
646            let json_value = crate::json_value::JsonValue::Object(json_pairs);
647            let serde_json_v = crate::json_value::json_value_to_serde_json(&json_value);
648            let body_str = serde_json::to_string(&serde_json_v)
649                .map_err(|e| format!("http.put_json() body serialization failed: {}", e))?;
650
651            let mut builder = reqwest::Client::new()
652                .put(url.as_str())
653                .header(reqwest::header::CONTENT_TYPE, "application/json")
654                .body(body_str);
655
656            for (k, v) in extract_headers(&options) {
657                builder = builder.header(&k, &v);
658            }
659            if let Some(timeout) = extract_timeout(&options) {
660                builder = builder.timeout(timeout);
661            }
662
663            let resp = builder
664                .send()
665                .await
666                .map_err(|e| format!("http.put_json() failed: {}", e))?;
667
668            let status = resp.status().as_u16();
669            let headers: Vec<(String, String)> = resp
670                .headers()
671                .iter()
672                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
673                .collect();
674            let body_out = resp
675                .text()
676                .await
677                .map_err(|e| format!("http.put_json() body read failed: {}", e))?;
678
679            Ok(TypedReturn::OkObjectPairs(build_response_pairs(
680                status, headers, body_out,
681            )))
682        },
683    );
684
685    module
686}