Skip to content

Commit 6e68336

Browse files
committed
feat: HTTP body support
1 parent f28d004 commit 6e68336

File tree

2 files changed

+105
-21
lines changed
  • guests/python/src/python_modules
  • host/tests/integration_tests/python/runtime

2 files changed

+105
-21
lines changed

guests/python/src/python_modules/mod.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ mod wit_world {
112112
use self::{
113113
error::Error,
114114
poll::Pollable,
115-
streams::InputStream,
115+
streams::{InputStream, OutputStream},
116116
types::{ErrorCode, FutureIncomingResponse, OutgoingRequest, RequestOptions},
117117
};
118118
use super::*;
@@ -286,6 +286,43 @@ mod wit_world {
286286
}
287287
}
288288

289+
#[pyclass]
290+
pub(crate) struct OutputStream {
291+
pub(crate) inner: Option<wasip2::http::types::OutputStream>,
292+
}
293+
294+
impl OutputStream {
295+
fn inner(&self) -> Result<&wasip2::http::types::OutputStream, ResourceMoved> {
296+
self.inner.as_ref().require_resource()
297+
}
298+
}
299+
300+
#[pymethods]
301+
impl OutputStream {
302+
fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
303+
slf
304+
}
305+
306+
fn __exit__(
307+
&mut self,
308+
_exc_type: &Bound<'_, PyAny>,
309+
_exc_value: &Bound<'_, PyAny>,
310+
_traceback: &Bound<'_, PyAny>,
311+
) {
312+
self.inner.take();
313+
}
314+
315+
fn blocking_write_and_flush(&self, contents: Vec<u8>) -> PyResult<()> {
316+
match self.inner()?.blocking_write_and_flush(&contents) {
317+
Ok(()) => Ok(()),
318+
Err(e) => {
319+
let e = Python::attach(|py| StreamError::new(py, e))?;
320+
Err(e).to_pyres()
321+
}
322+
}
323+
}
324+
}
325+
289326
#[pyclass]
290327
#[derive(Debug)]
291328
#[pyo3(frozen, get_all, str)]
@@ -1350,8 +1387,21 @@ mod wit_world {
13501387
inner: Option<wasip2::http::types::OutgoingBody>,
13511388
}
13521389

1390+
impl OutgoingBody {
1391+
fn inner(&self) -> Result<&wasip2::http::types::OutgoingBody, ResourceMoved> {
1392+
self.inner.as_ref().require_resource()
1393+
}
1394+
}
1395+
13531396
#[pymethods]
13541397
impl OutgoingBody {
1398+
fn write(&self) -> PyResult<OutputStream> {
1399+
let stream = self.inner()?.write().to_pyres()?;
1400+
Ok(OutputStream {
1401+
inner: Some(stream),
1402+
})
1403+
}
1404+
13551405
fn finish(&mut self, trailers: Option<&'_ mut Fields>) -> PyResult<()> {
13561406
let body = self.inner.take().require_resource()?;
13571407
let trailers = trailers

host/tests/integration_tests/python/runtime/http.rs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -152,43 +152,44 @@ def _headers_dict_to_str(headers: dict[str, str]) -> str:
152152
else:
153153
return headers
154154
155-
def perform_request(method: str, url: str, headers: str | None) -> str:
155+
def perform_request(method: str, url: str, headers: str | None, body: str | None) -> str:
156156
try:
157157
resp = urllib3.request(
158158
method=method,
159159
url=url,
160160
headers=_headers_str_to_dict(headers),
161+
body=body,
161162
)
162163
except Exception as e:
163164
return f"ERR: {e}"
164165
165166
resp_status = resp.status
166-
resp_body = resp.data.decode("utf-8")
167+
resp_body = f"'{resp.data.decode("utf-8")}'" if resp.data else "n/a"
167168
resp_headers = _headers_dict_to_str(resp.headers)
168169
169-
return f"OK: status={resp_status} headers={resp_headers} body='{resp_body}'"
170+
return f"OK: status={resp_status} headers={resp_headers} body={resp_body}"
170171
"#;
171172

172173
let mut cases = vec![
173174
TestCase {
174175
resp: Ok(TestResponse {
175-
body: "case_1",
176+
body: Some("case_1"),
176177
..Default::default()
177178
}),
178179
..Default::default()
179180
},
180181
TestCase {
181182
method: "POST",
182183
resp: Ok(TestResponse {
183-
body: "case_2",
184+
body: Some("case_2"),
184185
..Default::default()
185186
}),
186187
..Default::default()
187188
},
188189
TestCase {
189190
path: "/foo".to_owned(),
190191
resp: Ok(TestResponse {
191-
body: "case_3",
192+
body: Some("case_3"),
192193
..Default::default()
193194
}),
194195
..Default::default()
@@ -197,7 +198,7 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
197198
path: "/500".to_owned(),
198199
resp: Ok(TestResponse {
199200
status: 500,
200-
body: "case_4",
201+
body: Some("case_4"),
201202
..Default::default()
202203
}),
203204
..Default::default()
@@ -209,7 +210,7 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
209210
("multi".to_owned(), &["some", "thing"]),
210211
],
211212
resp: Ok(TestResponse {
212-
body: "case_5",
213+
body: Some("case_5"),
213214
..Default::default()
214215
}),
215216
..Default::default()
@@ -221,7 +222,24 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
221222
("foo".to_owned(), &["bar"]),
222223
("multi".to_owned(), &["some", "thing"]),
223224
],
224-
body: "case_6",
225+
body: Some("case_6"),
226+
..Default::default()
227+
}),
228+
..Default::default()
229+
},
230+
TestCase {
231+
path: "/body_in".to_owned(),
232+
requ_body: Some("foo"),
233+
resp: Ok(TestResponse {
234+
body: Some("case_7"),
235+
..Default::default()
236+
}),
237+
..Default::default()
238+
},
239+
TestCase {
240+
path: "/no_body_out".to_owned(),
241+
resp: Ok(TestResponse {
242+
body: None,
225243
..Default::default()
226244
}),
227245
..Default::default()
@@ -245,6 +263,7 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
245263
let mut builder_method = StringBuilder::new();
246264
let mut builder_url = StringBuilder::new();
247265
let mut builder_headers = StringBuilder::new();
266+
let mut builder_body = StringBuilder::new();
248267
let mut builder_result = StringBuilder::new();
249268

250269
for case in &cases {
@@ -258,6 +277,7 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
258277
method,
259278
path,
260279
requ_headers,
280+
requ_body,
261281
resp,
262282
} = case;
263283

@@ -268,17 +288,20 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
268288
path
269289
));
270290
builder_headers.append_option(headers_to_string(requ_headers));
291+
builder_body.append_option(requ_body.map(|s| s.to_owned()));
271292

272293
match resp {
273294
Ok(TestResponse {
274295
status,
275296
headers,
276297
body,
277298
}) => {
278-
let resp_headers = headers_to_string(headers).unwrap_or_else(|| "n/a".to_owned());
279-
builder_result.append_value(format!(
280-
"OK: status={status} headers={resp_headers} body='{body}'"
281-
));
299+
let headers = headers_to_string(headers).unwrap_or_else(|| "n/a".to_owned());
300+
let body = body
301+
.map(|s| format!("'{s}'"))
302+
.unwrap_or_else(|| "n/a".to_owned());
303+
builder_result
304+
.append_value(format!("OK: status={status} headers={headers} body={body}"));
282305
}
283306
Err(e) => {
284307
builder_result.append_value(format!("ERR: {e}"));
@@ -302,11 +325,13 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
302325
ColumnarValue::Array(Arc::new(builder_method.finish())),
303326
ColumnarValue::Array(Arc::new(builder_url.finish())),
304327
ColumnarValue::Array(Arc::new(builder_headers.finish())),
328+
ColumnarValue::Array(Arc::new(builder_body.finish())),
305329
],
306330
arg_fields: vec![
307331
Arc::new(Field::new("method", DataType::Utf8, true)),
308332
Arc::new(Field::new("url", DataType::Utf8, true)),
309333
Arc::new(Field::new("headers", DataType::Utf8, true)),
334+
Arc::new(Field::new("body", DataType::Utf8, true)),
310335
],
311336
number_rows: cases.len(),
312337
return_field: Arc::new(Field::new("r", DataType::Utf8, true)),
@@ -321,15 +346,15 @@ def perform_request(method: str, url: str, headers: str | None) -> str:
321346
struct TestResponse {
322347
status: u16,
323348
headers: Vec<(String, &'static [&'static str])>,
324-
body: &'static str,
349+
body: Option<&'static str>,
325350
}
326351

327352
impl Default for TestResponse {
328353
fn default() -> Self {
329354
Self {
330355
status: 200,
331356
headers: vec![],
332-
body: "",
357+
body: None,
333358
}
334359
}
335360
}
@@ -339,6 +364,7 @@ struct TestCase {
339364
method: &'static str,
340365
path: String,
341366
requ_headers: Vec<(String, &'static [&'static str])>,
367+
requ_body: Option<&'static str>,
342368
resp: Result<TestResponse, String>,
343369
}
344370

@@ -349,6 +375,7 @@ impl Default for TestCase {
349375
method: "GET",
350376
path: "/".to_owned(),
351377
requ_headers: vec![],
378+
requ_body: None,
352379
resp: Ok(TestResponse::default()),
353380
}
354381
}
@@ -369,6 +396,7 @@ impl TestCase {
369396
method,
370397
path,
371398
requ_headers,
399+
requ_body,
372400
resp,
373401
} = self;
374402
if base.is_some() {
@@ -392,12 +420,18 @@ impl TestCase {
392420
builder = builder.and(matchers::headers(k.as_str(), v.to_vec()));
393421
}
394422

423+
if let Some(requ_body) = requ_body {
424+
builder = builder.and(matchers::body_string(*requ_body));
425+
}
426+
427+
let mut resp_template = ResponseTemplate::new(resp_status)
428+
.append_headers(resp_headers.iter().map(|(k, v)| (k, v.join(","))));
429+
if let Some(resp_body) = resp_body {
430+
resp_template = resp_template.set_body_string(resp_body);
431+
}
432+
395433
let mock = builder
396-
.respond_with(
397-
ResponseTemplate::new(resp_status)
398-
.set_body_string(resp_body)
399-
.append_headers(resp_headers.iter().map(|(k, v)| (k, v.join(",")))),
400-
)
434+
.respond_with(resp_template)
401435
.expect(resp.is_ok() as u64);
402436
Some(mock)
403437
}

0 commit comments

Comments
 (0)