{
"command": "git diff bottle.py | head -300",
"description": "Show diff of bottle.py modifications"
}diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[stdout]
diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
[metadata] {"noOutputExpected": false}{
"command": "git status && git diff --stat && ls -la",
"description": "Check git status and repository files"
}On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: bottle.py
no changes added to commit (use "git add" and/or "git commit -a")
bottle.py | 4 ----
1 file changed, 4 deletions(-)
total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:05 ..
drwxr-xr-x 1 root root 27 Sep 26 02:06 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
[stdout]
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: bottle.py
no changes added to commit (use "git add" and/or "git commit -a")
bottle.py | 4 ----
1 file changed, 4 deletions(-)
total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:05 ..
drwxr-xr-x 1 root root 27 Sep 26 02:06 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
[metadata] {"noOutputExpected": false}{
"command": "sed -n '1540,1600p' bottle.py",
"description": "Read header helper functions context"
}var = self.environ['bottle.request.ext.%s' % name]
return var.__get__(self) if hasattr(var, '__get__') else var
except KeyError:
raise AttributeError('Attribute %r not defined.' % name)
def __setattr__(self, name, value):
""" Define new attributes that are local to the bound request environment. """
if name == 'environ': return object.__setattr__(self, name, value)
key = 'bottle.request.ext.%s' % name
if hasattr(self, name):
raise AttributeError("Attribute already defined: %s" % name)
self.environ[key] = value
def __delattr__(self, name):
try:
del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
return value
class HeaderProperty:
def __init__(self, name, reader=None, writer=None, default=''):
self.name, self.default = name, default
self.reader, self.writer = reader, writer
self.__doc__ = 'Current value of the %r header.' % name.title()
def __get__(self, obj, _):
if obj is None: return self
value = obj.get_header(self.name, self.default)
return self.reader(value) if self.reader else value
def __set__(self, obj, value):
obj[self.name] = self.writer(value) if self.writer else value
def __delete__(self, obj):
del obj[self.name]
class BaseResponse:
""" Storage class for a response body as well as headers and cookies.
This class does support dict-like case-insensitive item-access to
headers, but is NOT a dict. Most notably, iterating over a response
yields parts of the body and not the headers.
"""
default_status = 200
default_content_type = 'text/html; charset=UTF-8'
# Header denylist for specific response codes
# (rfc2616 section 10.2.3 and 10.3.5)
[stdout]
var = self.environ['bottle.request.ext.%s' % name]
return var.__get__(self) if hasattr(var, '__get__') else var
except KeyError:
raise AttributeError('Attribute %r not defined.' % name)
def __setattr__(self, name, value):
""" Define new attributes that are local to the bound request environment. """
if name == 'environ': return object.__setattr__(self, name, value)
key = 'bottle.request.ext.%s' % name
if hasattr(self, name):
raise AttributeError("Attribute already defined: %s" % name)
self.environ[key] = value
def __delattr__(self, name):
try:
del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
return value
class HeaderProperty:
def __init__(self, name, reader=None, writer=None, default=''):
self.name, self.default = name, default
self.reader, self.writer = reader, writer
self.__doc__ = 'Current value of the %r header.' % name.title()
def __get__(self, obj, _):
if obj is None: return self
value = obj.get_header(self.name, self.default)
return self.reader(value) if self.reader else value
def __set__(self, obj, value):
obj[self.name] = self.writer(value) if self.writer else value
def __delete__(self, obj):
del obj[self.name]
class BaseResponse:
""" Storage class for a response body as well as headers and cookies.
This class does support dict-like case-insensitive item-access to
headers, but is NOT a dict. Most notably, iterating over a response
yields parts of the body and not the headers.
"""
default_status = 200
default_content_type = 'text/html; charset=UTF-8'
# Header denylist for specific response codes
# (rfc2616 section 10.2.3 and 10.3.5)
[metadata] {"noOutputExpected": false}{
"command": "grep -n \"_hkey\\|_hval\\|crlf\\|control character\\|ValueError\\|Header name\\|Header value\" bottle.py | head -50",
"description": "Find all header validation usages"
}377: except ValueError:
679: raise ValueError('WSGI applications cannot be mounted to "/".')
758: raise ValueError("Prefix must start with '/'")
1255: except (ValueError, TypeError) as err:
1281: except ValueError:
1560:def _hkey(key):
1565:def _hval(value):
1668: raise ValueError('Status line must not include control chars.')
1672: raise ValueError('String status line without a reason phrase.')
1674: raise ValueError('Status code out of range.')
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1718: self._headers[_hkey(name)] = [_hval(value)]
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
1826: raise ValueError('Content does not fit into a cookie.')
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2208: access to HTTP_* fields. Header names are case-insensitive and titled by default.
2239: yield _hkey(key[5:])
2241: yield _hkey(key)
2878: except (TypeError, ValueError, IndexError, OverflowError):
2889: except (KeyError, ValueError):
2908: except ValueError:
3025: raise ValueError("Invalid signature for yieldroutes: %s" % sig)
3151: """ Iterate over a binary file-like object (crlf terminated) line by
3152: line. Each line is returned as a (line, crlf) tuple. Lines larger
3154: has an empty string instead of crlf. Maximum chunk size is twice the
3854: raise ValueError("Application is not callable: %r" % app)
3871: raise ValueError("Unknown or unsupported server: %r" % server)
4552: except (UnicodeError, TypeError, ValueError) as error:
[stdout]
377: except ValueError:
679: raise ValueError('WSGI applications cannot be mounted to "/".')
758: raise ValueError("Prefix must start with '/'")
1255: except (ValueError, TypeError) as err:
1281: except ValueError:
1560:def _hkey(key):
1565:def _hval(value):
1668: raise ValueError('Status line must not include control chars.')
1672: raise ValueError('String status line without a reason phrase.')
1674: raise ValueError('Status code out of range.')
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1718: self._headers[_hkey(name)] = [_hval(value)]
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
1826: raise ValueError('Content does not fit into a cookie.')
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2208: access to HTTP_* fields. Header names are case-insensitive and titled by default.
2239: yield _hkey(key[5:])
2241: yield _hkey(key)
2878: except (TypeError, ValueError, IndexError, OverflowError):
2889: except (KeyError, ValueError):
2908: except ValueError:
3025: raise ValueError("Invalid signature for yieldroutes: %s" % sig)
3151: """ Iterate over a binary file-like object (crlf terminated) line by
3152: line. Each line is returned as a (line, crlf) tuple. Lines larger
3154: has an empty string instead of crlf. Maximum chunk size is twice the
3854: raise ValueError("Application is not callable: %r" % app)
3871: raise ValueError("Unknown or unsupported server: %r" % server)
4552: except (UnicodeError, TypeError, ValueError) as error:
[metadata] {"noOutputExpected": false}{
"command": "ls test/ && grep -rn \"_hkey\\|_hval\\|crlf\\|control character\\|CRLF\\|header\" test/ --include=\"*.py\" -i | head -40",
"description": "Search tests for header validation"
}__init__.py
build_python.sh
example_settings.py
test_app.py
test_auth.py
test_config.py
test_contextlocals.py
test_environ.py
test_exc.py
test_fileupload.py
test_formsdict.py
test_html_helper.py
test_importhook.py
test_jinja2.py
test_mako.py
test_mdict.py
test_mount.py
test_multipart.py
test_oorouting.py
test_outputfilter.py
test_plugins.py
test_resources.py
test_route.py
test_router.py
test_securecookies.py
test_sendfile.py
test_stpl.py
test_wsgi.py
tools.py
views
test/test_mdict.py:2:from bottle import MultiDict, HeaderDict
test/test_mdict.py:32: def test_isheader(self):
test/test_mdict.py:33: """ HeaderDict replaces by default and title()s its keys """
test/test_mdict.py:34: m = HeaderDict(abc_def=5)
test/test_mdict.py:41: def test_headergetbug(self):
test/test_mdict.py:42: ''' Assure HeaderDict.get() to be case insensitive '''
test/test_mdict.py:43: d = HeaderDict()
test/test_auth.py:7: def test__header(self):
test/test_auth.py:12: self.assertHeader('Www-Authenticate', 'Basic realm="private"')
test/test_contextlocals.py:35: self.assertEqual(bottle.response.headers['Content-Type'], 'test/thread')
test/test_contextlocals.py:39: self.assertEqual(bottle.response.headers['Content-Type'], 'test/main')
test/test_contextlocals.py:41: self.assertEqual(bottle.response.headers['Content-Type'], 'test/main')
test/test_html_helper.py:3:from bottle import _parse_http_header
test/test_html_helper.py:10: def test_accept_header(self):
test/test_html_helper.py:11: self.assertEqual(_parse_http_header(
test/test_mount.py:78: self.assertHeader('X-Test', 'WSGI', '/test/')
test/test_mount.py:87: c = self.urlopen('/test/cookie')['header']['Set-Cookie']
test/test_mount.py:96: self.assertHeader('Content-Type', 'test/test', '/test/')
test/test_mount.py:103: self.assertHeader('Content-Type', 'application/json', '/test/json')
test/test_wsgi.py:53: """ 204 responses must not return some entity headers """
test/test_wsgi.py:56: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:58: for h, v in bottle.response.headerlist:
test/test_wsgi.py:59: self.assertFalse(h.lower() in bad, "Header %s not deleted" % h)
test/test_wsgi.py:62: """ 304 responses must not return entity headers """
test/test_wsgi.py:67: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:69: for h, v in bottle.response.headerlist:
test/test_wsgi.py:70: self.assertFalse(h.lower() in bad, "Header %s not deleted" % h)
test/test_wsgi.py:105: def test_utf8_header(self):
test/test_wsgi.py:106: header = 'öäü'.encode('utf8').decode('latin1')
test/test_wsgi.py:109: h = bottle.request.get_header('X-Test')
test/test_wsgi.py:111: bottle.response.set_header('X-Test', h)
test/test_wsgi.py:112: self.assertHeader('X-Test', header, '/test', env={'HTTP_X_TEST': header})
test/test_wsgi.py:137: self.assertHeader('Location', 'http://127.0.0.1/yes', '/', env=env)
test/test_wsgi.py:140: self.assertHeader('Location', 'http://127.0.0.1/yes', '/', env=env)
test/test_wsgi.py:142: self.assertHeader('Location', 'http://127.0.0.1/yes', '/one', env=env)
test/test_wsgi.py:147: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:152: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:154: self.assertHeader('Test-Header', 'test', '/yield')
test/test_wsgi.py:156: self.assertHeader('Test-Header', 'test', '/yield_nothing')
test/test_wsgi.py:166: c = self.urlopen('/cookie')['header'].get_all('Set-Cookie', '')
[stdout]
__init__.py
build_python.sh
example_settings.py
test_app.py
test_auth.py
test_config.py
test_contextlocals.py
test_environ.py
test_exc.py
test_fileupload.py
test_formsdict.py
test_html_helper.py
test_importhook.py
test_jinja2.py
test_mako.py
test_mdict.py
test_mount.py
test_multipart.py
test_oorouting.py
test_outputfilter.py
test_plugins.py
test_resources.py
test_route.py
test_router.py
test_securecookies.py
test_sendfile.py
test_stpl.py
test_wsgi.py
tools.py
views
test/test_mdict.py:2:from bottle import MultiDict, HeaderDict
test/test_mdict.py:32: def test_isheader(self):
test/test_mdict.py:33: """ HeaderDict replaces by default and title()s its keys """
test/test_mdict.py:34: m = HeaderDict(abc_def=5)
test/test_mdict.py:41: def test_headergetbug(self):
test/test_mdict.py:42: ''' Assure HeaderDict.get() to be case insensitive '''
test/test_mdict.py:43: d = HeaderDict()
test/test_auth.py:7: def test__header(self):
test/test_auth.py:12: self.assertHeader('Www-Authenticate', 'Basic realm="private"')
test/test_contextlocals.py:35: self.assertEqual(bottle.response.headers['Content-Type'], 'test/thread')
test/test_contextlocals.py:39: self.assertEqual(bottle.response.headers['Content-Type'], 'test/main')
test/test_contextlocals.py:41: self.assertEqual(bottle.response.headers['Content-Type'], 'test/main')
test/test_html_helper.py:3:from bottle import _parse_http_header
test/test_html_helper.py:10: def test_accept_header(self):
test/test_html_helper.py:11: self.assertEqual(_parse_http_header(
test/test_mount.py:78: self.assertHeader('X-Test', 'WSGI', '/test/')
test/test_mount.py:87: c = self.urlopen('/test/cookie')['header']['Set-Cookie']
test/test_mount.py:96: self.assertHeader('Content-Type', 'test/test', '/test/')
test/test_mount.py:103: self.assertHeader('Content-Type', 'application/json', '/test/json')
test/test_wsgi.py:53: """ 204 responses must not return some entity headers """
test/test_wsgi.py:56: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:58: for h, v in bottle.response.headerlist:
test/test_wsgi.py:59: self.assertFalse(h.lower() in bad, "Header %s not deleted" % h)
test/test_wsgi.py:62: """ 304{
"command": "sed -n '1640,1760p' bottle.py",
"description": "Read BaseResponse header methods"
}cookies = copy._cookies = SimpleCookie()
for k, v in self._cookies.items():
cookies[k] = v.value
cookies[k].update(v) # also copy cookie attributes
return copy
def __iter__(self):
return iter(self.body)
def close(self):
if hasattr(self.body, 'close'):
self.body.close()
@property
def status_line(self):
""" The HTTP status line as a string (e.g. ``404 Not Found``)."""
return self._status_line
@property
def status_code(self):
""" The HTTP status code as an integer (e.g. 404)."""
return self._status_code
def _set_status(self, status):
if isinstance(status, int):
code, status = status, _HTTP_STATUS_LINES.get(status)
elif ' ' in status:
if '\n' in status or '\r' in status or '\0' in status:
raise ValueError('Status line must not include control chars.')
status = status.strip()
code = int(status.split()[0])
else:
raise ValueError('String status line without a reason phrase.')
if not 100 <= code <= 999:
raise ValueError('Status code out of range.')
self._status_code = code
self._status_line = str(status or ('%d Unknown' % code))
def _get_status(self):
return self._status_line
status = property(
_get_status, _set_status, None,
''' A writeable property to change the HTTP response status. It accepts
either a numeric code (100-999) or a string with a custom reason
phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
:data:`status_code` are updated accordingly. The return value is
always a status string. ''')
del _get_status, _set_status
@property
def headers(self):
""" An instance of :class:`HeaderDict`, a case-insensitive dict-like
view on the response headers. """
hdict = HeaderDict()
hdict.dict = self._headers
return hdict
def __contains__(self, name):
return _hkey(name) in self._headers
def __delitem__(self, name):
del self._headers[_hkey(name)]
def __getitem__(self, name):
return self._headers[_hkey(name)][-1]
def __setitem__(self, name, value):
self._headers[_hkey(name)] = [_hval(value)]
def get_header(self, name, default=None):
""" Return the value of a previously defined header. If there is no
header with that name, return a default value. """
return self._headers.get(_hkey(name), [default])[-1]
def set_header(self, name, value):
""" Create a new response header, replacing any previously defined
headers with the same name. """
self._headers[_hkey(name)] = [_hval(value)]
def add_header(self, name, value):
""" Add an additional response header, not removing duplicates. """
self._headers.setdefault(_hkey(name), []).append(_hval(value))
def iter_headers(self):
""" Yield (header, value) tuples, skipping headers that are not
allowed with the current response status code. """
return self.headerlist
def _wsgi_status_line(self):
""" WSGI conform status line (latin1-encodeable) """
return self._status_line.encode('utf8', 'surrogateescape').decode('latin1')
@property
def headerlist(self):
""" WSGI conform list of (header, value) tuples. """
out = []
headers = list(self._headers.items())
if 'Content-Type' not in self._headers:
headers.append(('Content-Type', [self.default_content_type]))
if self._status_code in self.bad_headers:
bad_headers = self.bad_headers[self._status_code]
headers = [h for h in headers if h[0] not in bad_headers]
out += [(name, val) for (name, vals) in headers for val in vals]
if self._cookies:
for c in self._cookies.values():
out.append(('Set-Cookie', _hval(c.OutputString())))
out = [(k, v.encode('utf8', 'surrogateescape').decode('latin1')) for (k, v) in out]
return out
content_type = HeaderProperty('Content-Type')
content_length = HeaderProperty('Content-Length', reader=int, default=-1)
expires = HeaderProperty(
'Expires',
reader=lambda x: datetime.fromtimestamp(parse_date(x), UTC),
writer=lambda x: http_date(x))
@property
def charset(self, default='UTF-8'):
""" Return the charset specified in the content-type header (default: utf8). """
if 'charset=' in self.content_type:
[stdout]
cookies = copy._cookies = SimpleCookie()
for k, v in self._cookies.items():
cookies[k] = v.value
cookies[k].update(v) # also copy cookie attributes
return copy
def __iter__(self):
return iter(self.body)
def close(self):
if hasattr(self.body, 'close'):
self.body.close()
@property
def status_line(self):
""" The HTTP status line as a string (e.g. ``404 Not Found``)."""
return self._status_line
@property
def status_code(self):
""" The HTTP status code as an integer (e.g. 404)."""
return self._status_code
def _set_status(self, status):
if isinstance(status, int):
code, status = status, _HTTP_STATUS_LINES.get(status)
elif ' ' in status:
if '\n' in status or '\r' in status or '\0' in status:
raise ValueError('Status line must not include control chars.')
status = status.strip()
code = int(status.split()[0])
else:
raise ValueError('String status line without a reason phrase.')
if not 100 <= code <= 999:
raise ValueError('Status code out of range.')
self._status_code = code
self._status_line = str(status{
"command": "git log --oneline -20 && git show HEAD:bottle.py | sed -n '1555,1575p'",
"description": "Show original header helpers from HEAD"
}0207a34 typing: Type hints for pyright
b1bd8bb Formatting and cleanup
e3d8e71 fix: Route.__repr__ and Route.get_undecorated_callback exceptions.
011016c Changes to make tests pass on Windows
21a8db0 build: Also test latest python release
b5631d1 feat: add support for repeated query params
57bdb52 Create FUNDING.yml
b2cf884 refactor: Change `dict()` to literal `{}` (#1318)
60fdec7 build: Remove outdated workflow step
b6fd20c Update copyrights from 2024 to 2025 (#1475)
3b666ca tutorial_app.rst: Fix undefined name in Create An SQL Database
1dab4e7 change: Dropped support for Python 3.8 (but should still work)
a565987 refactor: Class syntax changed ages ago
e0c5ed1 fix: Closeable body not closed in rare edge cases
2fa7afd Update tutorial_app.rst
c65347f fix: Remove old WSGIRequestHandler workaround
ee24927 build: Split dev dependencies into dev,docs
b26e507 Merge branch 'patch-remove-py2'
1a4618c test: Fix unstable (too time-critical) test case
750fcbe refactor: Switch to inspect.signature()
del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
if '\n' in key or '\r' in key or '\0' in key:
raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
if '\n' in value or '\r' in value or '\0' in value:
raise ValueError("Header value must not contain control characters: %r" % value)
return value
class HeaderProperty:
def __init__(self, name, reader=None, writer=None, default=''):
[stdout]
0207a34 typing: Type hints for pyright
b1bd8bb Formatting and cleanup
e3d8e71 fix: Route.__repr__ and Route.get_undecorated_callback exceptions.
011016c Changes to make tests pass on Windows
21a8db0 build: Also test latest python release
b5631d1 feat: add support for repeated query params
57bdb52 Create FUNDING.yml
b2cf884 refactor: Change `dict()` to literal `{}` (#1318)
60fdec7 build: Remove outdated workflow step
b6fd20c Update copyrights from 2024 to 2025 (#1475)
3b666ca tutorial_app.rst: Fix undefined name in Create An SQL Database
1dab4e7 change: Dropped support for Python 3.8 (but should still work)
a565987 refactor: Class syntax changed ages ago
e0c5ed1 fix: Closeable body not closed in rare edge cases
2fa7afd Update tutorial_app.rst
c65347f fix: Remove old WSGIRequestHandler workaround
ee24927 build: Split dev dependencies into dev,docs
b26e507 Merge branch 'patch-remove-py2'
1a4618c test: Fix unstable (too time-critical) test case
750fcbe refactor: Switch to inspect.signature()
del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
if '\n' in key or '\r' in key or '\0' in key:
raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
if '\n' in value or '\r' in value or '\0' in value:
raise ValueError("Header value must not contain control characters: %r" % value)
return value
class HeaderProperty:
def __init__(self, name, reader=None, writer=None, default=''):
[metadata] {"noOutputExpected": false}{
"command": "grep -n \"control char\\|\\\\\\\\n\\|\\\\\\\\r\\|\\\\\\\\0\\|_hkey\\|_hval\\|status_line\\|_status\" bottle.py | head -60",
"description": "Find related validation patterns"
}150: text = "Use of feature or API deprecated since Bottle-%d.%d\n"\
151: "Cause: %s\n"\
152: "Fix: %s\n" % (major, minor, cause, fix)
1062: if response._status_code in (100, 101, 204, 304) \
1069: start_response(response._wsgi_status_line(), response.headerlist, exc_info)
1079: err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1080: '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1270: rn, sem, bs = b'\r\n', b';', b''
1560:def _hkey(key):
1565:def _hval(value):
1596: default_status = 200
1622: self.status = status or self.default_status
1654: def status_line(self):
1656: return self._status_line
1661: return self._status_code
1663: def _set_status(self, status):
1667: if '\n' in status or '\r' in status or '\0' in status:
1668: raise ValueError('Status line must not include control chars.')
1675: self._status_code = code
1676: self._status_line = str(status or ('%d Unknown' % code))
1678: def _get_status(self):
1679: return self._status_line
1682: _get_status, _set_status, None,
1685: phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
1688: del _get_status, _set_status
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1718: self._headers[_hkey(name)] = [_hval(value)]
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1729: def _wsgi_status_line(self):
1731: return self._status_line.encode('utf8', 'surrogateescape').decode('latin1')
1740: if self._status_code in self.bad_headers:
1741: bad_headers = self.bad_headers[self._status_code]
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
1855: out += '%s: %s\n' % (name.title(), value.strip())
1894: _status_line = _local_property()
1895: _status_code = _local_property()
1918: other._status_code = self._status_code
1919: other._status_line = self._status_line
1928: default_status = 500
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2239: yield _hkey(key[5:])
2241: yield _hkey(key)
3007: return '"%s"' % html_escape(string).replace('\n', ' ') \
3008: .replace('\r', ' ').replace('\t', '	')
3147: if self.buffer_size - 6 < len(boundary): # "--boundary--\r\n"
3175: i = chunk.find(b'\r\n', scanpos)
3177: yield chunk[scanpos:i], b'\r\n'
3878: _stderr("Hit Ctrl-C to quit.\n")
[stdout]
150: text = "Use of feature or API deprecated since Bottle-%d.%d\n"\
151: "Cause: %s\n"\
152: "Fix: %s\n" % (major, minor, cause, fix)
1062: if response._status_code in (100, 101, 204, 304) \
1069: start_response(response._wsgi_status_line(), response.headerlist, exc_info)
1079: err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1080: '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1270: rn, sem, bs = b'\r\n', b';', b''
1560:def _hkey(key):
1565:def _hval(value):
1596: default_status = 200
1622: self.status = status or self.default_status
1654: def status_line(self):
1656: return self._status_line
1661: return self._status_code
1663: def _set_status(self, status):
1667: if '\n' in status or '\r' in status or '\0' in status:
1668: raise ValueError('Status line must not include control chars.')
1675: self._status_code = code
1676: self._status_line = str(status or ('%d Unknown' % code))
1678: def _get_status(self):
1679: return self._status_line
1682: _get_status, _set_status, None,
1685: phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
1688: del _get_status, _set_status
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1718: self._headers[_hkey(name)] = [_hval(value)]
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1729: def _wsgi_status_line(self):
1731: return self._status_line.encode('utf8', 'surrogateescape').decode('latin1')
1740: if self._status_code in self.bad_headers:
1741: bad_headers = self.bad_headers[self._status_code]
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
1855: out += '%s: %s\n' % (name.title(), value.strip())
1894: _status_line = _local_property()
1895: _status_code = _local_property()
1918: other._status_code = self._status_code
1919: other._status_line = self._status_line
1928: default_status = 500
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2239: {
"command": "grep -rn \"control\\|ValueError\\|_hkey\\|_hval\\|\\\\\\\\n\\|\\\\\\\\r\\|header\" test/test_wsgi.py test/test_environ.py 2>/dev/null | head -40",
"description": "Search tests for header validation expectations"
}test/test_wsgi.py:53: """ 204 responses must not return some entity headers """
test/test_wsgi.py:56: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:58: for h, v in bottle.response.headerlist:
test/test_wsgi.py:62: """ 304 responses must not return entity headers """
test/test_wsgi.py:67: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:69: for h, v in bottle.response.headerlist:
test/test_wsgi.py:105: def test_utf8_header(self):
test/test_wsgi.py:106: header = 'öäü'.encode('utf8').decode('latin1')
test/test_wsgi.py:109: h = bottle.request.get_header('X-Test')
test/test_wsgi.py:111: bottle.response.set_header('X-Test', h)
test/test_wsgi.py:112: self.assertHeader('X-Test', header, '/test', env={'HTTP_X_TEST': header})
test/test_wsgi.py:147: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:152: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:166: c = self.urlopen('/cookie')['header'].get_all('Set-Cookie', '')
test/test_wsgi.py:168: c = self.urlopen('/cookie')['header'].get('Set-Cookie', '').split(',')
test/test_wsgi.py:332: bottle.response.headers['X-Hook'] = 'after'
test/test_wsgi.py:410: def test_after_response_hook_can_set_headers(self):
test/test_wsgi.py:425: bottle.response.headers["X-Hook"] = 'works'
test/test_wsgi.py:466: result = '+base+\n+main+\n!1234!\n+include+\n-main-\n+include+\n-base-\n'
test/test_environ.py:129: def test_header_access(self):
test/test_environ.py:130: """ Environ: Request objects decode headers """
test/test_environ.py:136: self.assertTrue('Some-Header' in request.headers)
test/test_environ.py:137: self.assertTrue(request.headers['Some-Header'] == 'some value')
test/test_environ.py:138: self.assertTrue(request.headers['Some-Other-Header'] == 'some other value')
test/test_environ.py:140: def test_header_access_special(self):
test/test_environ.py:146: self.assertEqual(request.headers['Content-Type'], 'test')
test/test_environ.py:147: self.assertEqual(request.headers['Content-Length'], '123')
test/test_environ.py:298: self._test_chunked('1\r\nx\r\nff\r\n' + 'y'*255 + '\r\n0\r\n',
test/test_environ.py:300: self._test_chunked('8\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:301: self._test_chunked('0\r\n', '')
test/test_environ.py:304: self._test_chunked('8 ; foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:305: self._test_chunked('8;foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:306: self._test_chunked('8;foo=bar\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:309: self._test_chunked('1\r\nx\r\n', HTTPError)
test/test_environ.py:312: self._test_chunked('2\r\nx\r\n', HTTPError)
test/test_environ.py:315: self._test_chunked('x\r\nx\r\n', HTTPError)
test/test_environ.py:323: files = [('file1','filename1.txt','content1'), ('万难','万难foo.py', 'ä\nö\rü')]
test/test_environ.py:343: self.assertEqual(tob('ä\nö\rü'), x)
test/test_environ.py:364: def test_json_noheader(self):
test/test_environ.py:365: """ Environ: Request.json property with missing content-type header. """
[stdout]
test/test_wsgi.py:53: """ 204 responses must not return some entity headers """
test/test_wsgi.py:56: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:58: for h, v in bottle.response.headerlist:
test/test_wsgi.py:62: """ 304 responses must not return entity headers """
test/test_wsgi.py:67: bottle.response.set_header(h, 'foo')
test/test_wsgi.py:69: for h, v in bottle.response.headerlist:
test/test_wsgi.py:105: def test_utf8_header(self):
test/test_wsgi.py:106: header = 'öäü'.encode('utf8').decode('latin1')
test/test_wsgi.py:109: h = bottle.request.get_header('X-Test')
test/test_wsgi.py:111: bottle.response.set_header('X-Test', h)
test/test_wsgi.py:112: self.assertHeader('X-Test', header, '/test', env={'HTTP_X_TEST': header})
test/test_wsgi.py:147: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:152: bottle.response.headers['Test-Header'] = 'test'
test/test_wsgi.py:166: c = self.urlopen('/cookie')['header'].get_all('Set-Cookie', '')
test/test_wsgi.py:168: c = self.urlopen('/cookie')['header'].get('Set-Cookie', '').split(',')
test/test_wsgi.py:332: bottle.response.headers['X-Hook'] = 'after'
test/test_wsgi.py:410: def test_after_response_hook_can_set_headers(self):
test/test_wsgi.py:425: bottle.response.headers["X-Hook"] = 'works'
test/test_wsgi.py:466: result = '+base+\n+main+\n!1234!\n+include+\n-main-\n+include+\n-base-\n'
test/test_environ.py:129: def test_header_access(self):
test/test_environ.py:130: """ Environ: Request objects decode headers """
test/test_environ.py:136: self.assertTrue('Some-Header' in request.headers)
test/test_environ.py:137: self.assertTrue(request.headers['Some-Header'] == 'some value')
test/test_environ.py:138: self.assertTrue(request.headers['Some-Other-Header'] == 'some other value')
test/test_environ.py:140: def test_header_access_special(self):
test/test_environ.py:146: self.assertEqual(request.headers['Content-Type'], 'test')
test/test_environ.py:147: self.assertEqual(request.headers['Content-Length'], '123')
test/test_environ.py:298: self._test_chunked('1\r\nx\r\nff\r\n' + 'y'*255 + '\r\n0\r\n',
test/test_environ.py:300: self._test_chunked('8\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:301: self._test_chunked('0\r\n', '')
test/test_environ.py:304: self._test_chunked('8 ; foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:305: self._test_chunked('8;foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/t{
"command": "python -m pytest -rA -q 2>&1 | tail -60",
"description": "Run test suite to see failures"
}PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode_code PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator_issue_407 PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks_end_bug PASSED test/test_stpl.py::TestSTPLDir::test_end_keyword_on_same_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_block PASSED test/test_stpl.py::TestSTPLDir::test_multiline_comprehensions_in_code_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_after_end PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers PASSED test/test_wsgi.py::TestRouteDecorator::test_apply PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list PASSED test/test_wsgi.py::TestRouteDecorator::test_callback PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks PASSED test/test_wsgi.py::TestRouteDecorator::test_method PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list PASSED test/test_wsgi.py::TestRouteDecorator::test_name PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path PASSED test/test_wsgi.py::TestRouteDecorator::test_template PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts PASSED test/test_wsgi.py::TestDecorators::test_autoroute PASSED test/test_wsgi.py::TestDecorators::test_routebuild PASSED test/test_wsgi.py::TestDecorators::test_truncate_body PASSED test/test_wsgi.py::TestDecorators::test_view PASSED test/test_wsgi.py::TestDecorators::test_view_error PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name FAILED test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers 1 failed, 366 passed in 0.67s [stdout] PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode_code PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator_issue_407 PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks_end_bug PASSED test/test_stpl.py::TestSTPLDir::test_end_keyword_on_same_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_block PASSED test/test_stpl.py::TestSTPLDir::test_multiline_comprehensions_in_code_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_after_end PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED test/test_wsgi.py::TestRouteDecorator::tes
{
"command": "grep -n \"test_prevent_control_characters_in_headers\" -A 80 test/test_environ.py",
"description": "Read the failing control-character test"
}735: def test_prevent_control_characters_in_headers(self):
736- masks = '{}test', 'test{}', 'te{}st'
737- tests = '\n', '\r', '\n\r', '\0'
738-
739- # Test HeaderDict
740- apis = 'append', 'replace', '__setitem__', 'setdefault'
741- for api, mask, test in itertools.product(apis, masks, tests):
742- hd = bottle.HeaderDict()
743- func = getattr(hd, api)
744- value = mask.replace("{}", test)
745- self.assertRaises(ValueError, func, value, "test-value")
746- self.assertRaises(ValueError, func, "test-name", value)
747-
748- # Test functions on BaseResponse
749- apis = 'add_header', 'set_header', '__setitem__'
750- for api, mask, test in itertools.product(apis, masks, tests):
751- rs = bottle.BaseResponse()
752- func = getattr(rs, api)
753- value = mask.replace("{}", test)
754- self.assertRaises(ValueError, func, value, "test-value")
755- self.assertRaises(ValueError, func, "test-name", value)
756-
757- def test_expires_header(self):
758- import datetime
759- from bottle import UTC
760- response = BaseResponse()
761- now = datetime.datetime.now(UTC)
762- response.expires = now
763-
764- def seconds(a, b):
765- td = max(a,b) - min(a,b)
766- return td.days*360*24 + td.seconds
767-
768- self.assertEqual(0, seconds(response.expires, now))
769- now2 = datetime.datetime.fromtimestamp(
770- parse_date(response.headers['Expires']), tz=UTC)
771- self.assertEqual(0, seconds(now, now2))
772-
773-
774-class TestRedirect(unittest.TestCase):
775-
776- def assertRedirect(self, target, result, query=None, status=303, **args):
777- env = {'SERVER_PROTOCOL': 'HTTP/1.1'}
778- for key in list(args):
779- if key.startswith('wsgi'):
780- args[key.replace('_', '.', 1)] = args[key]
781- del args[key]
782- env.update(args)
783- request.bind(env)
784- bottle.response.bind()
785- try:
786- bottle.redirect(target, **(query or {}))
787- except bottle.HTTPResponse as E:
788- self.assertEqual(status, E.status_code)
789- self.assertTrue(E.headers)
790- self.assertEqual(result, E.headers['Location'])
791-
792- def test_absolute_path(self):
793- self.assertRedirect('/', 'http://127.0.0.1/')
794- self.assertRedirect('/test.html', 'http://127.0.0.1/test.html')
795- self.assertRedirect('/test.html', 'http://127.0.0.1/test.html',
796- PATH_INFO='/some/sub/path/')
797- self.assertRedirect('/test.html', 'http://127.0.0.1/test.html',
798- PATH_INFO='/some/sub/file.html')
799- self.assertRedirect('/test.html', 'http://127.0.0.1/test.html',
800- SCRIPT_NAME='/some/sub/path/')
801- self.assertRedirect('/foo/test.html', 'http://127.0.0.1/foo/test.html')
802- self.assertRedirect('/foo/test.html', 'http://127.0.0.1/foo/test.html',
803- PATH_INFO='/some/sub/file.html')
804-
805- def test_relative_path(self):
806- self.assertRedirect('./', 'http://127.0.0.1/')
807- self.assertRedirect('./test.html', 'http://127.0.0.1/test.html')
808- self.assertRedirect('./test.html', 'http://127.0.0.1/foo/test.html',
809- PATH_INFO='/foo/')
810- self.assertRedirect('./test.html', 'http://127.0.0.1/foo/test.html',
811- PATH_INFO='/foo/bar.html')
812- self.assertRedirect('./test.html', 'http://127.0.0.1/foo/test.html',
813- SCRIPT_NAME='/foo/')
814- self.assertRedirect('./test.html', 'http://127.0.0.1/foo/bar/test.html',
815- SCRIPT_NAME='/foo/', PATH_INFO='/bar/baz.html')
[stdout]
735: def test_prevent_control_characters_in_headers(self):
736- masks = '{}test', 'test{}', 'te{}st'
737- tests = '\n', '\r', '\n\r', '\0'
738-
739- # Test HeaderDict
740- apis = 'append', 'replace', '__setitem__', 'setdefault'
741- for api, mask, test in itertools.product(apis, masks, tests):
742- hd = bottle.HeaderDict()
743- func = getattr(hd, api)
744- value = mask.replace("{}", test)
745- self.assertRaises(ValueError, func, value, "test-value")
746- self.assertRaises(ValueError, func, "test-name", value)
747-
748- # Test functions on BaseResponse
749- apis = 'add_header', 'set_header', '__setitem__'
750- for api, mask, test in itertools.product(apis, masks, tests):
751- rs = bottle.BaseResponse()
752- func = getattr(rs, api)
753- value = mask.replace("{}", test)
754- self.assertRaises(ValueError, func, value, "test-value")
755- self.assertRaises(ValueError, func, "test-name", value)
756-
757- def test_expires_header(self):
758- import datetime
759- from bottle import UTC
760- response = BaseResponse()
761- now = datetime.datetime.now(UTC)
762- response.expires = now
763-
764- def seconds(a, b):
765- td = max(a,b) - min(a,b)
766- return td.days*360*24 + td.seconds
767-
768- self.assertEqual(0, seconds(response.expires, now))
769- now2 = datetime.datetime.fromtimestamp(
770- parse_date(response.headers['Expires']), tz=UTC)
771- self.assertEqual(0, seconds(now, now2))
772-
773-
774-class TestRedirect(unittest.TestCase):
775-
776- def assertRedirect(self, target, result, query=None, status=303, **args):
777- env = {'SERVER_PROTOCOL': 'HTTP/1.1'}
778- for key in list(args):
779- if key.startswith('wsgi'):
780- args[key.replace('_', '.', 1)] = args[key]
781- {
"command": "sed -n '2140,2250p' bottle.py",
"description": "Read HeaderDict and MultiDict methods"
}Additionally to the normal dict-like item access methods, this container
also supports attribute-like access to its values. Missing attributes
default to an empty string.
.. versionchanged:: 0.14
All keys and values are now decoded as utf8 by default, item and
attribute access will return the same string.
"""
def decode(self, encoding=None):
""" (deprecated) Starting with 0.13 all keys and values are already
correctly decoded. """
copy = FormsDict()
for key, value in self.allitems():
copy[key] = value
return copy
def getunicode(self, name, default=None, encoding=None):
""" (deprecated) Return the value as a unicode string, or the default. """
return self.get(name, default)
def __getattr__(self, name, default=str()):
# Without this guard, pickle generates a cryptic TypeError:
if name.startswith('__') and name.endswith('__'):
return super(FormsDict, self).__getattr__(name)
return self.get(name, default=default)
class HeaderDict(MultiDict):
""" A case-insensitive version of :class:`MultiDict` that defaults to
replace the old value instead of appending it. """
def __init__(self, *a, **ka):
self.dict = {}
if a or ka: self.update(*a, **ka)
def __contains__(self, key):
return _hkey(key) in self.dict
def __delitem__(self, key):
del self.dict[_hkey(key)]
def __getitem__(self, key):
return self.dict[_hkey(key)][-1]
def __setitem__(self, key, value):
self.dict[_hkey(key)] = [_hval(value)]
def append(self, key, value):
self.dict.setdefault(_hkey(key), []).append(_hval(value))
def replace(self, key, value):
self.dict[_hkey(key)] = [_hval(value)]
def getall(self, key):
return self.dict.get(_hkey(key)) or []
def get(self, key, default=None, index=-1):
return MultiDict.get(self, _hkey(key), default, index)
def filter(self, names):
for name in (_hkey(n) for n in names):
if name in self.dict:
del self.dict[name]
class WSGIHeaderDict(DictMixin):
""" This dict-like class wraps a WSGI environ dict and provides convenient
access to HTTP_* fields. Header names are case-insensitive and titled by default.
"""
#: List of keys that do not have a ``HTTP_`` prefix.
cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
def __init__(self, environ):
self.environ = environ
def _ekey(self, key):
""" Translate header field name to CGI/WSGI environ key. """
key = key.replace('-', '_').upper()
if key in self.cgikeys:
return key
return 'HTTP_' + key
def raw(self, key, default=None):
""" Return the header value as is (not utf8-translated). """
return self.environ.get(self._ekey(key), default)
def __getitem__(self, key):
return _wsgi_recode(self.environ[self._ekey(key)])
def __setitem__(self, key, value):
raise TypeError("%s is read-only." % self.__class__)
def __delitem__(self, key):
raise TypeError("%s is read-only." % self.__class__)
def __iter__(self):
for key in self.environ:
if key[:5] == 'HTTP_':
yield _hkey(key[5:])
elif key in self.cgikeys:
yield _hkey(key)
def keys(self):
return [x for x in self]
def __len__(self):
return len(self.keys())
def __contains__(self, key):
return self._ekey(key) in self.environ
[stdout]
Additionally to the normal dict-like item access methods, this container
also supports attribute-like access to its values. Missing attributes
default to an empty string.
.. versionchanged:: 0.14
All keys and values are now decoded as utf8 by default, item and
attribute access will return the same string.
"""
def decode(self, encoding=None):
""" (deprecated) Starting with 0.13 all keys and values are already
correctly decoded. """
copy = FormsDict()
for key, value in self.allitems():
copy[key] = value
return copy
def getunicode(self, name, default=None, encoding=None):
""" (deprecated) Return the value as a unicode string, or the default. """
return self.get(name, default)
def __getattr__(self, name, default=str()):
# Without this guard, pickle generates a cryptic TypeError:
if name.startswith('__') and name.endswith('__'):
return super(FormsDict, self).__getattr__(name)
return self.get(name, default=default)
class HeaderDict(MultiDict):
""" A case-insensitive version of :class:`MultiDict` that defaults to
replace the old value instead of appending it. """
def __init__(self, *a, **ka):
self.dict = {}
if a or ka: self.update(*a, **ka)
def __contains__(self, key):
return _hkey(key) in self.dict
def __delitem__(self, key):
del self.dict[_hkey(key)]
def __getitem__(self, key):
return self.dict[_hkey(key)][-1]
def __setitem__(self, key, value):
self.dict[_hkey(key)] = [_hval(value)]
def append(self, key, value):
self.dict.setdefault(_hkey(key), []).append(_hval(value))
def replace(self, key, value):
self.dict[_hkey(key)] = [_hval(value)]
def getall(self, key):
return self.dict.get(_hkey(key)) or []
def get(self, key, default=None, index=-1):
return MultiDict.get(self, _hkey(key), default, index)
def filter(self, names):
for name in (_hkey(n) for n in names):
if name in self.dict:
del self.dict[name]
class WSGIHeaderDict(DictMixin):
""" This dict-like class wraps a WSGI environ dict and provides convenient
access to HTTP_* fields. Header names are ca{
"command": "grep -n \"class HeaderDict\\|class MultiDict\\|def append\\|def replace\\|def setdefault\\|def __setitem__\" bottle.py",
"description": "Locate dict class method definitions"
}1515: def __setitem__(self, key, value):
1707: def __setitem__(self, name, value):
2060:class MultiDict(DictMixin):
2084: def __setitem__(self, key, value):
2121: def append(self, key, value):
2125: def replace(self, key, value):
2168:class HeaderDict(MultiDict):
2185: def __setitem__(self, key, value):
2188: def append(self, key, value):
2191: def replace(self, key, value):
2230: def __setitem__(self, key, value):
2354: def setdefault(self, key, value=None):
2359: def __setitem__(self, key, value):
[stdout]
1515: def __setitem__(self, key, value):
1707: def __setitem__(self, name, value):
2060:class MultiDict(DictMixin):
2084: def __setitem__(self, key, value):
2121: def append(self, key, value):
2125: def replace(self, key, value):
2168:class HeaderDict(MultiDict):
2185: def __setitem__(self, key, value):
2188: def append(self, key, value):
2191: def replace(self, key, value):
2230: def __setitem__(self, key, value):
2354: def setdefault(self, key, value=None):
2359: def __setitem__(self, key, value):
[metadata] {"noOutputExpected": false}{
"command": "sed -n '2060,2140p' bottle.py && echo \"====\" && sed -n '2330,2380p' bottle.py",
"description": "Read MultiDict and setdefault implementations"
}class MultiDict(DictMixin):
""" This dict stores multiple values per key, but behaves exactly like a
normal dict in that it returns only the newest value for any given key.
There are special methods available to access the full list of values.
"""
def __init__(self, *a, **k):
self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
def __len__(self):
return len(self.dict)
def __iter__(self):
return iter(self.dict)
def __contains__(self, key):
return key in self.dict
def __delitem__(self, key):
del self.dict[key]
def __getitem__(self, key):
return self.dict[key][-1]
def __setitem__(self, key, value):
self.append(key, value)
def keys(self):
return self.dict.keys()
def values(self):
return (v[-1] for v in self.dict.values())
def items(self):
return ((k, v[-1]) for k, v in self.dict.items())
def allitems(self):
return ((k, v) for k, vl in self.dict.items() for v in vl)
iterkeys = keys
itervalues = values
iteritems = items
iterallitems = allitems
def get(self, key, default=None, index=-1, type=None):
""" Return the most recent value for a key.
:param default: The default value to be returned if the key is not
present or the type conversion fails.
:param index: An index for the list of available values.
:param type: If defined, this callable is used to cast the value
into a specific type. Exception are suppressed and result in
the default value to be returned.
"""
try:
val = self.dict[key][index]
return type(val) if type else val
except Exception:
pass
return default
def append(self, key, value):
""" Add a new value to the list of values for this key. """
self.dict.setdefault(key, []).append(value)
def replace(self, key, value):
""" Replace the list of values with a single value. """
self.dict[key] = [value]
def getall(self, key):
""" Return a (possibly empty) list of values for a key. """
return self.dict.get(key) or []
#: Aliases for WTForms to mimic other multi-dict APIs (Django)
getone = get
getlist = getall
class FormsDict(MultiDict):
""" This :class:`MultiDict` subclass is used to store request form data.
Additionally to the normal dict-like item access methods, this container
====
if isinstance(key, str):
nskey = (namespace + '.' + key).strip('.')
if isinstance(value, dict):
self.load_dict(value, namespace=nskey)
else:
self[nskey] = value
else:
raise TypeError('Key has type %r (not a string)' % type(key))
return self
def update(self, *a, **ka):
""" If the first parameter is a string, all keys are prefixed with this
namespace. Apart from that it works just as the usual dict.update().
>>> c = ConfigDict()
>>> c.update('some.namespace', key='value')
"""
prefix = ''
if a and isinstance(a[0], str):
prefix = a[0].strip('.') + '.'
a = a[1:]
for key, value in dict(*a, **ka).items():
self[prefix + key] = value
def setdefault(self, key, value=None):
if key not in self:
self[key] = value
return self[key]
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError('Key has type %r (not a string)' % type(key))
self._virtual_keys.discard(key)
value = self.meta_get(key, 'filter', lambda x: x)(value)
if key in self and self[key] is value:
return
self._on_change(key, value)
dict.__setitem__(self, key, value)
for overlay in self._iter_overlays():
overlay._set_virtual(key, value)
def __delitem__(self, key):
if key not in self:
raise KeyError(key)
if key in self._virtual_keys:
raise KeyError("Virtual keys cannot be deleted: %s" % key)
[stdout]
class MultiDict(DictMixin):
""" This dict stores multiple values per key, but behaves exactly like a
normal dict in that it returns only the newest value for any given key.
There are special methods available to access the full list of values.
"""
def __init__(self, *a, **k):
self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
def __len__(self):
return len(self.dict)
def __iter__(self):
return iter(self.dict)
def __contains__(self, key):
return key in self.dict
def __delitem__(self, key):
del self.dict[key]
def __getitem__(self, key):
return self.dict[key][-1]
def __setitem__(self, key, value):
self.append(key, value)
def keys(self):
return self.dict.keys()
def values(self):
return (v[-1] for v in self.dict.values())
def items(self):
return ((k, v[-1]) for k, v in self.dict.items())
def allitems(self):
return ((k, v) for k, vl in self.dict.items() for v in vl)
iterkeys = keys
itervalues = values
iteritems = items
iterallitems = allitems
def get(self, key, default=None, index=-1, type=None):
""" Return the most recent value for a key.
:param default: The default value to be returned if the key is not
present or the type conversion fails.
:param index: An index for the list of available values.
:param type: If defined, this callable is used to cast the value
into a specific type. Exception are suppressed and result in
the default value to be returned.
"""
try:
{
"command": "grep -n \"class DictMixin\\|def setdefault\\|def update\\|def set_item\\|class _Ondemand\" bottle.py | head -20",
"description": "Find DictMixin and setdefault definitions"
}138:def update_wrapper(wrapper, wrapped, *a, **ka):
2340: def update(self, *a, **ka):
2354: def setdefault(self, key, value=None):
[stdout]
138:def update_wrapper(wrapper, wrapped, *a, **ka):
2340: def update(self, *a, **ka):
2354: def setdefault(self, key, value=None):
[metadata] {"noOutputExpected": false}{
"replace_all": false,
"file_path": "/app/bottle.py",
"old_string": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n return value",
"new_string": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value"
}The file /app/bottle.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/app/bottle.py", "oldString": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n return value", "newString": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value", "originalFile": null, "structuredPatch": [{"oldStart": 1559, "oldLines": 11, "newStart": 1559, "newLines": 15, "lines": [" ", " def _hkey(key):", " key = touni(key)", "+ if '\\n' in key or '\\r' in key or '\\0' in key:", "+ raise ValueError(\"Header names must not contain control characters: %r\" % key)", " return key.title().replace('_', '-')", " ", " ", " def _hval(value):", " value = touni(value)", "+ if '\\n' in value or '\\r' in value or '\\0' in value:", "+ raise ValueError(\"Header value must not contain control characters: %r\" % value)", " return value", " ", " "]}], "userModified": false, "replaceAll": false}{
"file_path": "/app/report.jsonl",
"content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\", \"cwe-20\"]}\n"
}File created successfully at: /app/report.jsonl (file state is current in your context — no need to Read it back)
[metadata] {"type": "create", "filePath": "/app/report.jsonl", "content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\", \"cwe-20\"]}\n", "structuredPatch": [], "originalFile": null, "userModified": false}{
"command": "python -m pytest -rA 2>&1 | tail -30",
"description": "Run full test suite to verify fix"
}PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing
PASSED test/test_wsgi.py::TestCloseable::test_direct
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers
PASSED test/test_wsgi.py::TestRouteDecorator::test_apply
PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_callback
PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators
PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks
PASSED test/test_wsgi.py::TestRouteDecorator::test_method
PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_name
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_template
PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts
PASSED test/test_wsgi.py::TestDecorators::test_autoroute
PASSED test/test_wsgi.py::TestDecorators::test_routebuild
PASSED test/test_wsgi.py::TestDecorators::test_truncate_body
PASSED test/test_wsgi.py::TestDecorators::test_view
PASSED test/test_wsgi.py::TestDecorators::test_view_error
PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name
============================= 367 passed in 0.54s ==============================
[stdout]
PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing
PASSED test/test_wsgi.py::TestCloseable::test_direct
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response
PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers
PASSED test/test_wsgi.py::TestRouteDecorator::test_apply
PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_callback
PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators
PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks
PASSED test/test_wsgi.py::TestRouteDecorator::test_method
PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_name
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all
PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list
PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path
PASSED test/test_wsgi.py::TestRouteDecorator::test_template
PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts
PASSED test/test_wsgi.py::TestDecorators::test_autoroute
PASSED test/test_wsgi.py::TestDecorators::test_routebuild
PASSED test/test_wsgi.py::TestDecorators::test_truncate_body
PASSED test/test_wsgi.py::TestDecorators::test_view
PASSED test/test_wsgi.py::TestDecorators::test_view_error
PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts
PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name
============================= 367 passed in 0.54s ==============================
[metadata] {"noOutputExpected": false}{
"command": "python -c \"\nimport bottle\nfrom bottle import BaseResponse, HeaderDict\n\n# CRLF / header-injection attempts must raise ValueError\nfor payload in ['evil\\r\\nX-Injected: yes', 'name\\n', 'v\\r', 'x\\0y', 'te{}st'.format('\\n\\r')]:\n for fn, args in [\n (lambda: BaseResponse().set_header(payload, 'v'), 'set_header name'),\n (lambda: BaseResponse().set_header('X-Test', payload), 'set_header value'),\n (lambda: BaseResponse().add_header(payload, 'v'), 'add_header name'),\n (lambda: BaseResponse().__setitem__('X-Test', payload), '__setitem__ value'),\n (lambda: HeaderDict().append(payload, 'v'), 'HeaderDict.append name'),\n (lambda: HeaderDict().replace('n', payload), 'HeaderDict.replace value'),\n (lambda: HeaderDict().setdefault(payload, 'v'), 'HeaderDict.setdefault name'),\n (lambda: HeaderDict().setdefault('n', payload), 'HeaderDict.setdefault value'),\n ]:\n try:\n fn()\n print('FAIL (accepted):', args, repr(payload))\n except ValueError as e:\n pass\n except Exception as e:\n print('FAIL (wrong error %s):' % type(e).__name__, args, repr(payload))\nprint('OK: all injection attempts raise ValueError')\n\n# Valid headers still work\nr = BaseResponse()\nr.set_header('X-Test', 'ok')\nr.headers['Content-Type'] = 'text/plain'\nassert r.headers['X-Test'] == 'ok'\nassert r['Content-Type'] == 'text/plain'\nprint('OK: valid headers still work')\n\"",
"description": "Verify CRLF injection raises ValueError"
}OK: all injection attempts raise ValueError
OK: valid headers still work
[stdout]
OK: all injection attempts raise ValueError
OK: valid headers still work
[metadata] {"noOutputExpected": false}