Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WSGI Content-Length and Transfer-Encoding #75

Merged
merged 7 commits into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bench/asgi_wsgi/raw-wsgi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from socketify import WSGI

def app(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '14')])
yield b'Hello, World!\n'

if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions src/socketify/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
void uws_filter(int ssl, uws_app_t *app, uws_filter_handler handler, void *user_data);


void uws_res_close(int ssl, uws_res_t *res);
void uws_res_end(int ssl, uws_res_t *res, const char *data, size_t length, bool close_connection);
void uws_res_pause(int ssl, uws_res_t *res);
void uws_res_resume(int ssl, uws_res_t *res);
Expand Down
2 changes: 1 addition & 1 deletion src/socketify/uWebSockets
136 changes: 110 additions & 26 deletions src/socketify/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ def wsgi(ssl, response, info, user_data, aborted):
headers_set = None
headers_written = False
status_text = None
is_chunked = False
content_length = -1
def write_headers(headers):
nonlocal headers_written, headers_set, status_text
nonlocal headers_written, headers_set, status_text, content_length, is_chunked
if headers_written or not headers_set:
return

Expand All @@ -178,20 +180,33 @@ def write_headers(headers):
if (
key == "content-length"
or key == "Content-Length"
or key == "Transfer-Encoding"
):
content_length = int(value)
continue # auto generated by try_end
if (
key == "Transfer-Encoding"
or key == "transfer-encoding"
):
continue # auto
is_chunked = str(value) == "chunked"
if is_chunked:
continue

key_data = key.encode("utf-8")
elif isinstance(key, bytes):
# this is faster than using .lower()
if (
key == b"content-length"
or key == b"Content-Length"
or key == b"Transfer-Encoding"
or key == b"transfer-encoding"
):
content_length = int(value)
continue # auto
if (
key == b"Transfer-Encoding"
or key == b"transfer-encoding"
):
is_chunked = str(value) == "chunked"
if is_chunked:
continue
key_data = key


Expand All @@ -212,6 +227,10 @@ def write_headers(headers):
lib.uws_res_write_header(
ssl, response, key_data, len(key_data), value_data, len(value_data)
)
# no content-length
if content_length < 0:
is_chunked = True

def start_response(status, headers, exc_info=None):
nonlocal headers_set, status_text
if exc_info:
Expand All @@ -229,8 +248,11 @@ def start_response(status, headers, exc_info=None):
status_text = status

def write(data):
nonlocal is_chunked
if not headers_written:
write_headers(headers_set)
# will allow older frameworks only with is_chunked
is_chunked = True

if isinstance(data, bytes):
lib.uws_res_write(ssl, response, data, len(data))
Expand All @@ -243,8 +265,9 @@ def write(data):
if bool(info.has_content):
WSGI_INPUT = BytesIO()
environ["wsgi.input"] = WSGIBody(WSGI_INPUT)

failed_chunks = None
def on_data(data_response, response):
nonlocal failed_chunks
if bool(data_response.aborted[0]):
return

Expand All @@ -255,24 +278,53 @@ def on_data(data_response, response):
)
try:
for data in app_iter:
if data and not headers_written:
write_headers(headers_set)
if data:
if not headers_written:
write_headers(headers_set)
if is_chunked:
if isinstance(data, bytes):
lib.uws_res_write(ssl, response, data, len(data))
elif isinstance(data, str):
data = data.encode("utf-8")
lib.uws_res_write(ssl, response, data, len(data))
else:
if isinstance(data, str):
data = data.encode("utf-8")
if failed_chunks:
failed_chunks.append(data)
else:
result = lib.uws_res_try_end(
ssl,
response,
data,
len(data),
ffi.cast("uintmax_t", content_length),
0,
)
# this should be very very rare fot HTTP
cirospaciari marked this conversation as resolved.
Show resolved Hide resolved
if not bool(result.ok):
failed_chunks = []
# just mark the chunks
failed_chunks.append(data)
break

if isinstance(data, bytes):
lib.uws_res_write(ssl, response, data, len(data))
elif isinstance(data, str):
data = data.encode("utf-8")
lib.uws_res_write(ssl, response, data, len(data))

except Exception as error:
logging.exception(error)
finally:
if hasattr(app_iter, "close"):
app_iter.close()

if not headers_written:
write_headers(headers_set)
lib.uws_res_end_without_body(ssl, response, 0)
write_headers(headers_set)
if is_chunked:
lib.uws_res_end_without_body(ssl, response, 0)
elif result is None or not bool(result.has_responded): # not reachs Content-Length
logging.error(AssertionError("Content-Length do not match sended content"))
lib.uws_res_close(
ssl,
response
)


data_response = WSGIDataResponse(
app, environ, start_response, aborted, WSGI_INPUT, on_data
Expand All @@ -282,25 +334,57 @@ def on_data(data_response, response):
else:
environ["wsgi.input"] = None
app_iter = app.wsgi(environ, start_response)
result = None
try:
for data in app_iter:
if data and not headers_written:
write_headers(headers_set)

if isinstance(data, bytes):
lib.uws_res_write(ssl, response, data, len(data))
elif isinstance(data, str):
data = data.encode("utf-8")
lib.uws_res_write(ssl, response, data, len(data))
if data:
if not headers_written:
write_headers(headers_set)
if is_chunked:
if isinstance(data, bytes):
lib.uws_res_write(ssl, response, data, len(data))
elif isinstance(data, str):
data = data.encode("utf-8")
lib.uws_res_write(ssl, response, data, len(data))
else:
if isinstance(data, str):
data = data.encode("utf-8")
if failed_chunks:
failed_chunks.append(data)
else:
result = lib.uws_res_try_end(
ssl,
response,
data,
len(data),
ffi.cast("uintmax_t", content_length),
0,
)
# this should be very very rare fot HTTP
if not bool(result.ok):
failed_chunks = []
# just mark the chunks
failed_chunks.append(data)
break


except Exception as error:
logging.exception(error)
logging.error(error)
finally:
if hasattr(app_iter, "close"):
app_iter.close()

if not headers_written:
write_headers(headers_set)
lib.uws_res_end_without_body(ssl, response, 0)
if is_chunked:
lib.uws_res_end_without_body(ssl, response, 0)
elif result is None or not bool(result.has_responded): # not reachs Content-Length
logging.error(AssertionError("Content-Length do not match sended content"))
lib.uws_res_close(
ssl,
response
)


def is_asgi(module):
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
Expand Down