Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 9 additions & 1 deletion poorwsgi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,15 @@ def uri(self):
@property
def path(self):
"""Path part of url."""
return self.__environ.get("PATH_INFO").encode("iso-8859-1").decode()
try:
return (
self.__environ.get("PATH_INFO").encode("iso-8859-1").decode()
)
except (UnicodeDecodeError, UnicodeEncodeError) as err:
log.warning("Invalid PATH_INFO encoding: %s", err)
raise HTTPException(
HTTP_BAD_REQUEST, error="Invalid PATH_INFO encoding"
) from err

@property
def query(self):
Expand Down
13 changes: 11 additions & 2 deletions poorwsgi/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def internal_server_error(req, *_):
in dispatch_table.errors. If poor_Debug variable is to On, Tracaback
will be generated.
"""
handler = {"module": None, "name": None}
handler = {"module": None, "name": None, "args": None}
if req.uri_handler:
handler["module"] = req.uri_handler.__module__
handler["name"] = req.uri_handler.__name__
Expand Down Expand Up @@ -187,6 +187,13 @@ def bad_request(req, error=None):
""" 400 Bad Request server error handler. """
if error:
log.warning("400 - Bad Request: %s", error)
path = "[ NOT PARSED ]"
try:
path = req.path
except HTTPException:
Comment thread
ondratu marked this conversation as resolved.
# If obtaining req.path fails, keep the default placeholder value.
pass

content = (
"<!DOCTYPE html>\n"
"<html>\n"
Expand All @@ -203,10 +210,12 @@ def bad_request(req, error=None):
" <body>\n"
" <h1>400 - Bad Request</h1>\n"
" <p>Method %s for %s uri.</p>\n"
" <pre>%s</pre>\n"
" <hr>\n"
" <small><i>webmaster: %s </i></small>\n"
" </body>\n"
"</html>" % (req.method, html_escape(req.uri), req.server_admin))
"</html>" % (req.method, html_escape(path), error or "",
req.server_admin))
return Response(content, status_code=HTTP_BAD_REQUEST)


Expand Down
39 changes: 39 additions & 0 deletions tests/test_application_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Unit test for unicode decode error propagation."""
from io import BytesIO
from time import time

import pytest

from poorwsgi.request import Request
from poorwsgi.wsgi import Application


def test_keyerror_on_internal_error(monkeypatch):
"""Test for KeyError: 'args' on unicode decode error in path."""
app = Application()

def mock_path(self):
"""Mock of old Request.path property."""
# We are mocking the old behavior, where UnicodeDecodeError was not
# caught inside path property and propagated to __request__ method.
raise UnicodeDecodeError("utf-8", b"\xc0", 0, 1, "invalid start byte")

monkeypatch.setattr(Request, "path", property(mock_path))

environ = {
"PATH_INFO": "/foo",
"REQUEST_METHOD": "GET",
"SERVER_NAME": "localhost",
"SERVER_PORT": "80",
"wsgi.url_scheme": "http",
"wsgi.input": BytesIO(b""),
"wsgi.errors": BytesIO(),
"REQUEST_STARTTIME": time(),
}

def start_response(*_):
# This is mock, we check response from app call
pass

with pytest.raises(UnicodeDecodeError, match="invalid start byte"):
app(environ, start_response)
34 changes: 34 additions & 0 deletions tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,37 @@ def test_empty_form(self, app):
assert req.is_body_request is False
assert req.mime_type in app.form_mime_types
assert isinstance(req.form, EmptyForm)


def test_bad_path_info_triggers_400(app):
"""Test that bad PATH_INFO encoding is handled and returns 400."""
captured_status = None
captured_headers = None

def start_response(status, headers):
nonlocal captured_status, captured_headers
captured_status = status
captured_headers = headers

# This char in iso-8859-1 is 0xc0, an invalid start byte in utf-8
bad_char = 'À'
bad_path = f'/foo{bad_char}bar'

environ = {
'PATH_INFO': bad_path,
'REQUEST_METHOD': 'GET',
'SERVER_NAME': 'localhost',
'SERVER_PORT': '80',
'wsgi.url_scheme': 'http',
'REQUEST_STARTTIME': time()
}

# The app.__call__ should catch the HTTPException and generate a 400
response_body = app(environ, start_response)

assert captured_status == '400 Bad Request'
# Also check body content
body_str = b''.join(response_body).decode()
assert '400' in body_str
assert 'Bad Request' in body_str
assert 'Invalid PATH_INFO encoding' in body_str