nginx / unit

NGINX Unit - universal web app server - a lightweight and versatile open source server that simplifies the application stack by natively executing application code across eight different programming language runtimes.
https://unit.nginx.org
Apache License 2.0
5.39k stars 327 forks source link

Python ASGI: support bytearray, memoryview in response body #648

Open filiphanes opened 2 years ago

filiphanes commented 2 years ago

When sending ASGI response:

await send({
    "type": "http.response.body",
    "body": body,
    "more_body": False,
})

Nginx Unit currently allows body to be onlybytes type, but Python types bytearray and memoryview could be supported, to avoid conversion to bytes and copying memory. bytearray is faster then bytes when appending lots of small parts. memoryview over bytes or bytearray objects is used to avoid memory copying.

Maybe all objects with buffer protocol could be supported https://docs.python.org/3/c-api/buffer.html

Looking at file https://github.com/nginx/unit/blob/master/src/python/nxt_python_asgi_http.c and https://docs.python.org/3/c-api/memoryview.html https://docs.python.org/3/c-api/bytearray.html this should be straightforward.

Uvicorn ASGI server support these types.

andrey-zelenkov commented 11 months ago

Hi @filiphanes,

Thank you for your report. Could you please try following patch with bytearray support if it works for you:

diff --git a/src/python/nxt_python_asgi_http.c b/src/python/nxt_python_asgi_http.c
--- a/src/python/nxt_python_asgi_http.c
+++ b/src/python/nxt_python_asgi_http.c
@@ -362,16 +362,6 @@ nxt_py_asgi_http_response_body(nxt_py_as
     Py_ssize_t              body_len, body_off;
     nxt_py_asgi_ctx_data_t  *ctx_data;

-    body = PyDict_GetItem(dict, nxt_py_body_str);
-    if (nxt_slow_path(body != NULL && !PyBytes_Check(body))) {
-        return PyErr_Format(PyExc_TypeError, "'body' is not a byte string");
-    }
-
-    more_body = PyDict_GetItem(dict, nxt_py_more_body_str);
-    if (nxt_slow_path(more_body != NULL && !PyBool_Check(more_body))) {
-        return PyErr_Format(PyExc_TypeError, "'more_body' is not a bool");
-    }
-
     if (nxt_slow_path(http->complete)) {
         return PyErr_Format(PyExc_RuntimeError,
                             "Unexpected ASGI message 'http.response.body' "
@@ -382,9 +372,26 @@ nxt_py_asgi_http_response_body(nxt_py_as
         return PyErr_Format(PyExc_RuntimeError, "Concurrent send");
     }

+    more_body = PyDict_GetItem(dict, nxt_py_more_body_str);
+    if (nxt_slow_path(more_body != NULL && !PyBool_Check(more_body))) {
+        return PyErr_Format(PyExc_TypeError, "'more_body' is not a bool");
+    }
+
+    body = PyDict_GetItem(dict, nxt_py_body_str);
+
     if (body != NULL) {
-        body_str = PyBytes_AS_STRING(body);
-        body_len = PyBytes_GET_SIZE(body);
+        if (PyBytes_Check(body)) {
+            body_str = PyBytes_AS_STRING(body);
+            body_len = PyBytes_GET_SIZE(body);
+
+        } else if (PyByteArray_Check(body)) {
+            body_str = PyByteArray_AS_STRING(body);
+            body_len = PyByteArray_GET_SIZE(body);
+
+        } else {
+            return PyErr_Format(PyExc_TypeError,
+                                "'body' is not a byte string or bytearray");
+        }

         nxt_unit_req_debug(http->req, "asgi_http_response_body: %d, %d",
                            (int) body_len, (more_body == Py_True) );