Skip to content

Commit 4ef66b5

Browse files
Changed Response to use body: bytes (#375)
* Changed Response to use body: bytes - Added ActixBytesWrapper to conform to pyclass and MessageBody * Made suggested improvements to allow both PyString and PyBytes - Reverted unnecessary casting and type changes * Added test for supplying unsupported type to body - WIP: Issue with async testing * Remove unused unsupported type test due to issues with raises * Fixed issue where .unwrap causes Panic and instead raises Exception - Added tests for bad and good body types * Removed unused return - Parameterized body test fixtures --------- Co-authored-by: Antoine Romero-Romero <45259848+AntoineRR@users.noreply.github.com>
1 parent 7e24f9c commit 4ef66b5

File tree

6 files changed

+196
-10
lines changed

6 files changed

+196
-10
lines changed

integration_tests/base_routes.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,32 @@ async def async_body_patch(request):
444444
return bytearray(request["body"]).decode("utf-8")
445445

446446

447-
# ===== Main =====
447+
@app.get("/binary_output_sync")
448+
def binary_output_sync(request):
449+
return b"OK"
450+
451+
452+
@app.get("/binary_output_response_sync")
453+
def binary_output_response_sync(request):
454+
return Response(
455+
status_code=200,
456+
headers={"Content-Type": "application/octet-stream"},
457+
body="OK",
458+
)
459+
460+
461+
@app.get("/binary_output_async")
462+
async def binary_output_async(request):
463+
return b"OK"
464+
465+
466+
@app.get("/binary_output_response_async")
467+
async def binary_output_response_async(request):
468+
return Response(
469+
status_code=200,
470+
headers={"Content-Type": "application/octet-stream"},
471+
body="OK",
472+
)
448473

449474

450475
@app.get("/sync/raise")
@@ -457,6 +482,9 @@ async def async_raise():
457482
raise Exception()
458483

459484

485+
# ===== Main =====
486+
487+
460488
if __name__ == "__main__":
461489
app.add_request_header("server", "robyn")
462490
app.add_directory(
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import requests
2+
3+
BASE_URL = "http://127.0.0.1:8080"
4+
5+
6+
def test_binary_output_sync(session):
7+
r = requests.get(f"{BASE_URL}/binary_output_sync")
8+
assert r.status_code == 200
9+
assert r.headers["Content-Type"] == "application/octet-stream"
10+
assert r.text == "OK"
11+
12+
13+
def test_binary_output_response_sync(session):
14+
r = requests.get(f"{BASE_URL}/binary_output_response_sync")
15+
assert r.status_code == 200
16+
assert r.headers["Content-Type"] == "application/octet-stream"
17+
assert r.text == "OK"
18+
19+
20+
def test_binary_output_async(session):
21+
r = requests.get(f"{BASE_URL}/binary_output_async")
22+
assert r.status_code == 200
23+
assert r.headers["Content-Type"] == "application/octet-stream"
24+
assert r.text == "OK"
25+
26+
27+
def test_binary_output_response_async(session):
28+
r = requests.get(f"{BASE_URL}/binary_output_response_async")
29+
assert r.status_code == 200
30+
assert r.headers["Content-Type"] == "application/octet-stream"
31+
assert r.text == "OK"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import pytest
2+
3+
from robyn.robyn import Response
4+
5+
6+
class A:
7+
pass
8+
9+
10+
bad_bodies = [
11+
None,
12+
1,
13+
True,
14+
A,
15+
{"body": "OK"},
16+
["OK", b"OK"],
17+
Response(
18+
status_code=200,
19+
headers={},
20+
body=b"OK",
21+
),
22+
]
23+
24+
good_bodies = ["OK", b"OK"]
25+
26+
27+
@pytest.mark.parametrize("body", bad_bodies)
28+
def test_bad_body_types(body):
29+
with pytest.raises(ValueError):
30+
_ = Response(
31+
status_code=200,
32+
headers={},
33+
body=body,
34+
)
35+
36+
37+
@pytest.mark.parametrize("body", good_bodies)
38+
def test_good_body_types(body):
39+
_ = Response(
40+
status_code=200,
41+
headers={},
42+
body=body,
43+
)

robyn/robyn.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class FunctionInfo:
1919
class Response:
2020
status_code: int
2121
headers: dict[str, str]
22-
body: str
22+
body: bytes
2323

2424
def set_file_path(self, file_path: str):
2525
pass

robyn/router.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,18 @@ def _format_response(self, res):
3939
response.set_file_path(file_path)
4040
elif type(res) == Response:
4141
response = res
42+
elif type(res) == bytes:
43+
response = Response(
44+
status_code=200,
45+
headers={"Content-Type": "application/octet-stream"},
46+
body=res,
47+
)
4248
else:
4349
response = Response(
44-
status_code=200, headers={"Content-Type": "text/plain"}, body=str(res)
50+
status_code=200,
51+
headers={"Content-Type": "text/plain"},
52+
body=str(res).encode("utf-8"),
4553
)
46-
4754
return response
4855

4956
def add_route(

src/types.rs

+83-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,89 @@
1+
use core::{mem};
2+
use std::ops::{Deref, DerefMut};
3+
use std::{
4+
convert::Infallible,
5+
task::{Context, Poll},
6+
pin::Pin,
7+
};
18
use std::collections::HashMap;
29

310
use actix_web::web::Bytes;
411
use actix_web::{http::Method, HttpRequest};
12+
use actix_http::body::MessageBody;
13+
use actix_http::body::BodySize;
14+
515
use anyhow::Result;
616
use dashmap::DashMap;
717
use pyo3::{exceptions, prelude::*};
18+
use pyo3::types::{PyBytes, PyString};
19+
use pyo3::exceptions::PyValueError;
820

921
use crate::io_helpers::read_file;
1022

23+
fn type_of<T>(_: &T) -> String {
24+
std::any::type_name::<T>().to_string()
25+
}
26+
27+
#[derive(Debug, Clone)]
28+
#[pyclass]
29+
pub struct ActixBytesWrapper(Bytes);
30+
31+
// provides an interface between pyo3::types::{PyString, PyBytes} and actix_web::web::Bytes
32+
impl ActixBytesWrapper {
33+
pub fn new(raw_body: &PyAny) -> PyResult<Self> {
34+
let value = if let Ok(v) = raw_body.downcast::<PyString>() {
35+
v.to_string().into_bytes()
36+
} else if let Ok(v) = raw_body.downcast::<PyBytes>() {
37+
v.as_bytes().to_vec()
38+
} else {
39+
return Err(PyValueError::new_err(
40+
format!("Could not convert {} specified body to bytes", type_of(raw_body))
41+
));
42+
};
43+
Ok(Self(Bytes::from(value)))
44+
}
45+
}
46+
47+
impl Deref for ActixBytesWrapper {
48+
type Target = Bytes;
49+
50+
fn deref(&self) -> &Self::Target {
51+
&self.0
52+
}
53+
}
54+
55+
impl DerefMut for ActixBytesWrapper {
56+
fn deref_mut(&mut self) -> &mut Self::Target {
57+
&mut self.0
58+
}
59+
}
60+
61+
impl MessageBody for ActixBytesWrapper {
62+
type Error = Infallible;
63+
64+
#[inline]
65+
fn size(&self) -> BodySize {
66+
BodySize::Sized(self.len() as u64)
67+
}
68+
69+
#[inline]
70+
fn poll_next(
71+
self: Pin<&mut Self>,
72+
_cx: &mut Context<'_>,
73+
) -> Poll<Option<Result<Bytes, Self::Error>>> {
74+
if self.is_empty() {
75+
Poll::Ready(None)
76+
} else {
77+
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
78+
}
79+
}
80+
81+
#[inline]
82+
fn try_into_bytes(self) -> Result<Bytes, Self> {
83+
Ok(self.0)
84+
}
85+
}
86+
1187
#[pyclass]
1288
#[derive(Debug, Clone)]
1389
pub struct FunctionInfo {
@@ -81,32 +157,33 @@ pub struct Response {
81157
pub status_code: u16,
82158
pub response_type: String,
83159
pub headers: HashMap<String, String>,
84-
pub body: String,
160+
pub body: ActixBytesWrapper,
85161
pub file_path: Option<String>,
86162
}
87163

88164
#[pymethods]
89165
impl Response {
90166
#[new]
91-
pub fn new(status_code: u16, headers: HashMap<String, String>, body: String) -> Self {
92-
Self {
167+
pub fn new(status_code: u16, headers: HashMap<String, String>, body: &PyAny) -> PyResult<Self> {
168+
Ok(Self {
93169
status_code,
94170
// we should be handling based on headers but works for now
95171
response_type: "text".to_string(),
96172
headers,
97-
body,
173+
body: ActixBytesWrapper::new(body)?,
98174
file_path: None,
99-
}
175+
})
100176
}
101177

102178
pub fn set_file_path(&mut self, file_path: &str) -> PyResult<()> {
103179
// we should be handling based on headers but works for now
104180
self.response_type = "static_file".to_string();
105181
self.file_path = Some(file_path.to_string());
106-
self.body = match read_file(file_path) {
182+
let response = match read_file(file_path) {
107183
Ok(b) => b,
108184
Err(e) => return Err(exceptions::PyIOError::new_err::<String>(e.to_string())),
109185
};
186+
self.body = ActixBytesWrapper(Bytes::from(response));
110187
Ok(())
111188
}
112189
}

0 commit comments

Comments
 (0)