fix: no message on bare curl

This commit is contained in:
2026-04-27 14:43:27 +02:00
committed by Kleπek
parent 6db1b561f0
commit 95514f16cb
3 changed files with 111 additions and 78 deletions
+14 -10
View File
@@ -112,33 +112,33 @@ PUT /{page}/
```bash ```bash
# create archive from 'dist' dir and upload it to /my-project/ # create archive from 'dist' dir and upload it to /my-project/
tar -czC dist -f dist.tar.gz . tar -czC dist -f dist.tar.gz .
curl -v -X PUT \ curl -X PUT \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
--data-binary "@dist.tar.gz" \ --data-binary "@dist.tar.gz" \
https://stapler-host/my-project/ https://stapler-host/my-project/
# same thing but one-liner # same thing but one-liner
tar -czC dist . | curl -v -X PUT \ tar -czC dist . | curl -X PUT \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
--data-binary @- \ --data-binary @- \
https://stapler-host/my-project/ https://stapler-host/my-project/
# make stapler server identify myproject.example.com and /my-project/ # make stapler server identify myproject.example.com and /my-project/
tar -czC dist . | curl -v -X PUT \ tar -czC dist . | curl -X PUT \
--data-binary @- \ --data-binary @- \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-Host: myproject.example.com' \ -H 'X-Host: myproject.example.com' \
https://stapler-host/my-project/ https://stapler-host/my-project/
# make stapler server identifiers myproject.example.com only # make stapler server identifiers myproject.example.com only
tar -czC dist . | curl -v -X PUT \ tar -czC dist . | curl -X PUT \
--data-binary @- \ --data-binary @- \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-Host-Only: myproject.example.com' \ -H 'X-Host-Only: myproject.example.com' \
https://stapler-host/my-project/ https://stapler-host/my-project/
# make a SPA site at /my-project/index.html # make a SPA site at /my-project/index.html
tar -czC dist . | curl -v -X PUT \ tar -czC dist . | curl -X PUT \
--data-binary @- \ --data-binary @- \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-SPA: index.html' \ -H 'X-SPA: index.html' \
@@ -160,13 +160,13 @@ PUT /{page}/
```bash ```bash
# create /my-project/ that redirects to https://github.com/my-project # create /my-project/ that redirects to https://github.com/my-project
curl -v -X PUT \ curl -X PUT \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-Redirect: https://github.com/my-project' \ -H 'X-Redirect: https://github.com/my-project' \
https://stapler-host/my-project/ https://stapler-host/my-project/
# simple redirect from root host to www # simple redirect from root host to www
curl -v -X PUT \ curl -X PUT \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-Proxy: https://www.my-website.com' \ -H 'X-Proxy: https://www.my-website.com' \
-H 'X-Host: my-website.com' \ -H 'X-Host: my-website.com' \
@@ -185,7 +185,7 @@ PUT /{page}/
```bash ```bash
# create /my-website/ that proxies to http://host.containers.internal:8000 # create /my-website/ that proxies to http://host.containers.internal:8000
curl -v -X PUT \ curl -X PUT \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
-H 'X-Proxy: http://host.containers.internal:8000' \ -H 'X-Proxy: http://host.containers.internal:8000' \
https://stapler-host/my-project/ https://stapler-host/my-project/
@@ -200,7 +200,7 @@ DELETE /{page}/
```bash ```bash
# delete /my-project/ # delete /my-project/
curl -v -X DELETE \ curl -X DELETE \
-H 'X-Token: <TOKEN>' \ -H 'X-Token: <TOKEN>' \
https://stapler-host/my-project/ https://stapler-host/my-project/
``` ```
@@ -226,7 +226,11 @@ curl -v -X DELETE \
- name: Create archive - name: Create archive
run: tar -czC dist -f dist.tar.gz . run: tar -czC dist -f dist.tar.gz .
- name: Deploy to Stapler server - name: Deploy to Stapler server
run: curl -v -X PUT -H 'X-Token: ${{ secrets.STAPLER_TOKEN }}' -H 'X-Host: ${{ vars.TARGET_HOST }}' --data-binary "@dist.tar.gz" https://stapler-host/my-project/ run: |
curl -X PUT --data-binary "@dist.tar.gz" \
-H 'X-Token: ${{ secrets.STAPLER_TOKEN }}' \
-H 'X-Host: ${{ vars.TARGET_HOST }}' \
${{ vars.STAPLER_URL }}
``` ```
### Redirecting hosts with DNS ### Redirecting hosts with DNS
+72 -57
View File
@@ -45,13 +45,25 @@ class BaseHandler(abc.ABC, http.server.BaseHTTPRequestHandler):
code: int, code: int,
message: str | None = None, message: str | None = None,
explain: str | None = None, explain: str | None = None,
) -> None:
self.send_status(code, message, explain)
def send_status(
self,
code: int,
message: str | None = None,
explain: str | None = None,
) -> None: ) -> None:
shortmsg, longmsg = self.responses[code] shortmsg, longmsg = self.responses[code]
if message is None: if message is None:
message = shortmsg message = shortmsg
if explain is None: if explain is None:
explain = longmsg explain = longmsg
if "text/" in self._get_header("Accept"): if (
not self._has_header("Accept")
or self._get_header("Accept").startswith("*/")
or self._get_header("Accept").startswith("text/")
):
self.send_basic_body( self.send_basic_body(
f"{code} {message}\n{explain}\n\n{self.server_signature()}", f"{code} {message}\n{explain}\n\n{self.server_signature()}",
code=code, code=code,
@@ -339,28 +351,26 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
def do_PUT(self) -> None: def do_PUT(self) -> None:
self._pre_log_request() self._pre_log_request()
if self._proxy_or_redirect(): if self._proxy_or_redirect():
return None return
if (path := self.__check_update_request()) is None: if (path := self.__check_put_request()) is None:
return None return
if not self.__check_put_headers():
return None
if (
self.has_target_host
and (page := self.registry.get_from_host(self.target_host)) is not None
and page.path != path
):
return self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
if self.has_target_redirect: if self.has_target_redirect:
self._update_redirect(path) if not self._update_redirect(path):
return
elif self.has_target_proxy: elif self.has_target_proxy:
self._update_proxy(path) if not self._update_proxy(path):
else: return
self._update_extract(path) elif not self._update_extract(path):
return
if self.has_request_host: if self.has_request_host:
self.registry.set_host(path, self.target_host) self.registry.set_host(path, self.target_host)
if self.has_request_host_only: if self.has_request_host_only:
self.registry.set_host_only(path, self.target_host) self.registry.set_host_only(path, self.target_host)
return None self.send_status(
http.HTTPStatus.CREATED,
"Resource updated",
str(self.registry.get_from_path(path)),
)
def do_POST(self) -> None: def do_POST(self) -> None:
self.do_PUT() # be gentle on them self.do_PUT() # be gentle on them
@@ -371,10 +381,15 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
def do_DELETE(self) -> None: def do_DELETE(self) -> None:
self._pre_log_request() self._pre_log_request()
if self._proxy_or_redirect(): if self._proxy_or_redirect():
return None return
if (path := self.__check_update_request()) is None: if (path := self.__check_update_request()) is None:
return None return
return self._update_remove(path) if self._update_remove(path):
self.send_status(
http.HTTPStatus.OK,
f"Resource /{path}/ removed",
)
return
def do_CONNECT(self) -> None: def do_CONNECT(self) -> None:
self._pre_log_request() self._pre_log_request()
@@ -391,73 +406,64 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
if not self._proxy_or_redirect(): if not self._proxy_or_redirect():
self.send_error(http.HTTPStatus.METHOD_NOT_ALLOWED) self.send_error(http.HTTPStatus.METHOD_NOT_ALLOWED)
def _update_extract(self, path: str) -> None: def _update_extract(self, path: str) -> bool:
if self.in_size == 0: if self.in_size == 0:
return self.send_error(http.HTTPStatus.LENGTH_REQUIRED, "No body found") self.send_error(http.HTTPStatus.LENGTH_REQUIRED, "No body found")
return False
if self.in_size > self.max_size_bytes: if self.in_size > self.max_size_bytes:
return self.send_error( self.send_error(
http.HTTPStatus.CONTENT_TOO_LARGE, http.HTTPStatus.CONTENT_TOO_LARGE,
"Archive too large", "Archive too large",
) )
return False
try: try:
file_bytes = io.BytesIO(self.rfile.read(self.in_size)) file_bytes = io.BytesIO(self.rfile.read(self.in_size))
self.data_dir.extract_tar_bytes(path, file_bytes) self.data_dir.extract_tar_bytes(path, file_bytes)
except tarfile.TarError: except tarfile.TarError:
return self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid tar archive") self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid tar archive")
return False
except Exception as e: except Exception as e:
return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e))
return False
self.registry.add(path) self.registry.add(path)
self.token_manager.set_token(path, self.token) self.token_manager.set_token(path, self.token)
if self.has_target_spa: if self.has_target_spa:
self.registry.set_spa(path, self.target_spa) self.registry.set_spa(path, self.target_spa)
self.send_status_only( return True
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
def _update_redirect(self, path: str) -> None: def _update_redirect(self, path: str) -> bool:
if self.in_size > 0: if self.in_size > 0:
return self.send_error( self.send_error(
http.HTTPStatus.BAD_REQUEST, http.HTTPStatus.BAD_REQUEST,
f"No content must be sent with {self.REDIRECT_HEADER}", f"No content must be sent with {self.REDIRECT_HEADER}",
) )
return False
self.registry.set_redirect(path, self.target_redirect) self.registry.set_redirect(path, self.target_redirect)
self.token_manager.set_token(path, self.token) self.token_manager.set_token(path, self.token)
self.send_status_only( return True
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
def _update_proxy(self, path: str) -> None: def _update_proxy(self, path: str) -> bool:
if self.in_size > 0: if self.in_size > 0:
return self.send_error( self.send_error(
http.HTTPStatus.BAD_REQUEST, http.HTTPStatus.BAD_REQUEST,
f"No content must be sent with {self.PROXY_HEADER}", f"No content must be sent with {self.PROXY_HEADER}",
) )
return False
self.registry.set_proxy(path, self.target_proxy) self.registry.set_proxy(path, self.target_proxy)
self.token_manager.set_token(path, self.token) self.token_manager.set_token(path, self.token)
self.send_status_only( return True
http.HTTPStatus.CREATED,
f"Resource /{path}/ updated",
)
return None
def _update_remove(self, path: str) -> None: def _update_remove(self, path: str) -> bool:
if not self.data_dir.exists(path): if not self.data_dir.exists(path):
self.send_error(http.HTTPStatus.NOT_FOUND, "Not found") self.send_error(http.HTTPStatus.NOT_FOUND, "Not found")
return None return False
try: try:
self.data_dir.remove(path) self.data_dir.remove(path)
except Exception as e: except Exception as e:
return self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, str(e))
self.send_status_only( return False
http.HTTPStatus.NO_CONTENT,
f"Resource /{path}/ removed",
)
self.registry.remove(path) self.registry.remove(path)
return None return True
def _proxy_or_redirect(self) -> bool: def _proxy_or_redirect(self) -> bool:
if self.has_token or self.path.startswith(self.CERTBOT_CHALLENGE_PATH): if self.has_token or self.path.startswith(self.CERTBOT_CHALLENGE_PATH):
@@ -518,23 +524,32 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler, BaseHandler):
return None return None
return sub_path return sub_path
def __check_put_headers(self) -> bool: def __check_put_request(self) -> str | None:
if (path := self.__check_update_request()) is None:
return None
if self.has_request_host and self.has_request_host_only: if self.has_request_host and self.has_request_host_only:
self.send_error( self.send_error(
http.HTTPStatus.BAD_REQUEST, http.HTTPStatus.BAD_REQUEST,
f"Cannot use {self.HOST_ONLY_HEADER} with {self.HOST_HEADER}", f"Cannot use {self.HOST_ONLY_HEADER} with {self.HOST_HEADER}",
) )
return False return None
if self.has_target_host and not self.__valid_host(self.target_host): if self.has_target_host and not self.__valid_host(self.target_host):
self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid requested host") self.send_error(http.HTTPStatus.BAD_REQUEST, "Invalid requested host")
return False return None
if self.has_target_proxy and self.has_target_redirect: if self.has_target_proxy and self.has_target_redirect:
self.send_error( self.send_error(
http.HTTPStatus.BAD_REQUEST, http.HTTPStatus.BAD_REQUEST,
f"Cannot use {self.PROXY_HEADER} with {self.REDIRECT_HEADER}", f"Cannot use {self.PROXY_HEADER} with {self.REDIRECT_HEADER}",
) )
return False return None
return True if (
self.has_target_host
and (page := self.registry.get_from_host(self.target_host)) is not None
and page.path != path
):
self.send_error(http.HTTPStatus.FORBIDDEN, "Host already taken")
return None
return path
def __get_path(self, path: str, regex: re.Pattern) -> str | None: def __get_path(self, path: str, regex: re.Pattern) -> str | None:
if (match := regex.match(path.lower())) is not None: if (match := regex.match(path.lower())) is not None:
+25 -11
View File
@@ -136,6 +136,8 @@ class TestRequestHandler(BaseHandlerTestCase):
) -> RequestHandler: ) -> RequestHandler:
if headers is None: if headers is None:
headers = {} headers = {}
if "Accept" not in headers:
headers["Accept"] = "nothing"
with self.patch("http.server.BaseHTTPRequestHandler.__init__"): with self.patch("http.server.BaseHTTPRequestHandler.__init__"):
handler = RequestHandler( handler = RequestHandler(
unittest.mock.MagicMock(), unittest.mock.MagicMock(),
@@ -433,8 +435,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call_unchecked(self.data_dir.extract_tar_bytes), self.mock_call_unchecked(self.data_dir.extract_tar_bytes),
self.mock_call(self.registry.add, ["path"]), self.mock_call(self.registry.add, ["path"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -458,8 +461,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.add, ["path"]), self.mock_call(self.registry.add, ["path"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_host, ["path", "example.com"]), self.mock_call(self.registry.set_host, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -481,8 +485,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.add, ["path"]), self.mock_call(self.registry.add, ["path"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_spa, ["path", "index.html"]), self.mock_call(self.registry.set_spa, ["path", "index.html"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -530,8 +535,9 @@ class TestRequestHandler(BaseHandlerTestCase):
), ),
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]), self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -557,8 +563,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]), self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_host, ["path", "example.com"]), self.mock_call(self.registry.set_host, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -584,8 +591,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]), self.mock_call(self.registry.set_redirect, ["path", "https://example.com"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_host_only, ["path", "example.com"]), self.mock_call(self.registry.set_host_only, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -633,8 +641,9 @@ class TestRequestHandler(BaseHandlerTestCase):
), ),
self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]), self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -660,8 +669,9 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]), self.mock_call(self.registry.set_proxy, ["path", "https://example.com"]),
self.mock_call(self.token_manager.set_token, ["path", "secret"]), self.mock_call(self.token_manager.set_token, ["path", "secret"]),
self.mock_call(self.registry.set_host, ["path", "example.com"]), self.mock_call(self.registry.set_host, ["path", "example.com"]),
self.mock_call(self.registry.get_from_path, ["path"]),
self.expects_status_only( self.expects_status_only(
handler, http.HTTPStatus.CREATED, "Resource /path/ updated" handler, http.HTTPStatus.CREATED, "Resource updated"
), ),
self.seal_mocks(), self.seal_mocks(),
): ):
@@ -783,9 +793,7 @@ class TestRequestHandler(BaseHandlerTestCase):
self.mock_call(self.data_dir.exists, ["path"], True), # noqa: FBT003 self.mock_call(self.data_dir.exists, ["path"], True), # noqa: FBT003
self.mock_call(self.data_dir.remove, ["path"]), self.mock_call(self.data_dir.remove, ["path"]),
self.mock_call(self.registry.remove, ["path"]), self.mock_call(self.registry.remove, ["path"]),
self.expects_error( self.expects_error(handler, http.HTTPStatus.OK, "Resource /path/ removed"),
handler, http.HTTPStatus.NO_CONTENT, "Resource /path/ removed"
),
self.seal_mocks(), self.seal_mocks(),
): ):
handler.do_DELETE() handler.do_DELETE()
@@ -813,6 +821,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None, "data": None,
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost", "X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",
@@ -855,6 +864,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": b"hello", "data": b"hello",
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost", "X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",
@@ -897,6 +907,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None, "data": None,
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost", "X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",
@@ -930,6 +941,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None, "data": None,
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost", "X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",
@@ -972,6 +984,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None, "data": None,
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "localhost", "X-Forwarded-Host": "localhost",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",
@@ -1011,6 +1024,7 @@ class TestRequestHandler(BaseHandlerTestCase):
"data": None, "data": None,
"headers": { "headers": {
"Host": "example.com", "Host": "example.com",
"Accept": "nothing",
"X-Real-IP": "127.0.0.1", "X-Real-IP": "127.0.0.1",
"X-Forwarded-Host": "host", "X-Forwarded-Host": "host",
"X-Forwarded-For": "127.0.0.1", "X-Forwarded-For": "127.0.0.1",