-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
application: extract path prefix logic to middleware (#2733)
Summary: This way, we can reason about and test it in isolation as we add more middleware to the application. Note that when using `--path_prefix`, loading the main TensorBoard page without a trailing slash on the URL has long been broken (#1117). This commit does not fix that, but changes the exact failure mode: rather than 404ing, we now load a broken TensorBoard (relative URLs don’t resolve). A follow-up commit will fix this properly. Test Plan: Verify that TensorBoard still works fully, with `--path_prefix` and without it. wchargin-branch: path-prefix-middleware
- Loading branch information
Showing
4 changed files
with
214 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Copyright 2019 The TensorFlow Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ============================================================================== | ||
"""Internal path prefix support for TensorBoard. | ||
Using a path prefix of `/foo/bar` enables TensorBoard to serve from | ||
`http://localhost:6006/foo/bar/` rather than `http://localhost:6006/`. | ||
See the `--path_prefix` flag docs for more details. | ||
""" | ||
|
||
from __future__ import absolute_import | ||
from __future__ import division | ||
from __future__ import print_function | ||
|
||
from tensorboard import errors | ||
|
||
|
||
class PathPrefixMiddleware(object): | ||
"""WSGI middleware for path prefixes. | ||
All requests to this middleware must begin with the specified path | ||
prefix (otherwise, a 404 will be returned immediately). Requests will | ||
be forwarded to the underlying application with the path prefix | ||
stripped and appended to `SCRIPT_NAME` (see the WSGI spec, PEP 3333, | ||
for details). | ||
""" | ||
|
||
def __init__(self, application, path_prefix): | ||
"""Initializes this middleware. | ||
Args: | ||
application: The WSGI application to wrap (see PEP 3333). | ||
path_prefix: A string path prefix to be stripped from incoming | ||
requests. If empty, this middleware is a no-op. If non-empty, | ||
the path prefix must start with a slash and not end with one | ||
(e.g., "/tensorboard"). | ||
""" | ||
if path_prefix.endswith("/"): | ||
raise ValueError("Path prefix must not end with slash: %r" % path_prefix) | ||
if path_prefix and not path_prefix.startswith("/"): | ||
raise ValueError( | ||
"Non-empty path prefix must start with slash: %r" % path_prefix | ||
) | ||
self._application = application | ||
self._path_prefix = path_prefix | ||
self._strict_prefix = self._path_prefix + "/" | ||
|
||
def __call__(self, environ, start_response): | ||
path = environ.get("PATH_INFO", "") | ||
if path != self._path_prefix and not path.startswith(self._strict_prefix): | ||
raise errors.NotFoundError() | ||
environ["PATH_INFO"] = path[len(self._path_prefix):] | ||
environ["SCRIPT_NAME"] = environ.get("SCRIPT_NAME", "") + self._path_prefix | ||
return self._application(environ, start_response) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# Copyright 2019 The TensorFlow Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ============================================================================== | ||
"""Tests for `tensorboard.backend.path_prefix`.""" | ||
|
||
from __future__ import absolute_import | ||
from __future__ import division | ||
from __future__ import print_function | ||
|
||
import json | ||
|
||
import werkzeug | ||
|
||
from tensorboard import errors | ||
from tensorboard import test as tb_test | ||
from tensorboard.backend import path_prefix | ||
|
||
|
||
class PathPrefixMiddlewareTest(tb_test.TestCase): | ||
"""Tests for `PathPrefixMiddleware`.""" | ||
|
||
def _echo_app(self, environ, start_response): | ||
# https://www.python.org/dev/peps/pep-0333/#environ-variables | ||
data = { | ||
"path": environ.get("PATH_INFO", ""), | ||
"script": environ.get("SCRIPT_NAME", ""), | ||
} | ||
body = json.dumps(data, sort_keys=True) | ||
start_response("200 OK", [("Content-Type", "application/json")]) | ||
return [body] | ||
|
||
def _assert_ok(self, response, path, script): | ||
self.assertEqual(response.status_code, 200) | ||
actual = json.loads(response.get_data()) | ||
expected = dict(path=path, script=script) | ||
self.assertEqual(actual, expected) | ||
|
||
def test_bad_path_prefix_without_leading_slash(self): | ||
with self.assertRaises(ValueError) as cm: | ||
path_prefix.PathPrefixMiddleware(self._echo_app, "hmm") | ||
msg = str(cm.exception) | ||
self.assertIn("must start with slash", msg) | ||
self.assertIn(repr("hmm"), msg) | ||
|
||
def test_bad_path_prefix_with_trailing_slash(self): | ||
with self.assertRaises(ValueError) as cm: | ||
path_prefix.PathPrefixMiddleware(self._echo_app, "/hmm/") | ||
msg = str(cm.exception) | ||
self.assertIn("must not end with slash", msg) | ||
self.assertIn(repr("/hmm/"), msg) | ||
|
||
def test_empty_path_prefix(self): | ||
app = path_prefix.PathPrefixMiddleware(self._echo_app, "") | ||
server = werkzeug.test.Client(app, werkzeug.BaseResponse) | ||
|
||
with self.subTest("at empty"): | ||
self._assert_ok(server.get(""), path="", script="") | ||
|
||
with self.subTest("at root"): | ||
self._assert_ok(server.get("/"), path="/", script="") | ||
|
||
with self.subTest("at subpath"): | ||
response = server.get("/foo/bar") | ||
self._assert_ok(server.get("/foo/bar"), path="/foo/bar", script="") | ||
|
||
def test_nonempty_path_prefix(self): | ||
app = path_prefix.PathPrefixMiddleware(self._echo_app, "/pfx") | ||
server = werkzeug.test.Client(app, werkzeug.BaseResponse) | ||
|
||
with self.subTest("at root"): | ||
response = server.get("/pfx") | ||
self._assert_ok(response, path="", script="/pfx") | ||
|
||
with self.subTest("at root with slash"): | ||
response = server.get("/pfx/") | ||
self._assert_ok(response, path="/", script="/pfx") | ||
|
||
with self.subTest("at subpath"): | ||
response = server.get("/pfx/foo/bar") | ||
self._assert_ok(response, path="/foo/bar", script="/pfx") | ||
|
||
with self.subTest("at non-path-component extension"): | ||
with self.assertRaises(errors.NotFoundError): | ||
server.get("/pfxz") | ||
|
||
with self.subTest("above path prefix"): | ||
with self.assertRaises(errors.NotFoundError): | ||
server.get("/hmm") | ||
|
||
def test_composition(self): | ||
app = self._echo_app | ||
app = path_prefix.PathPrefixMiddleware(app, "/bar") | ||
app = path_prefix.PathPrefixMiddleware(app, "/foo") | ||
server = werkzeug.test.Client(app, werkzeug.BaseResponse) | ||
|
||
response = server.get("/foo/bar/baz/quux") | ||
self._assert_ok(response, path="/baz/quux", script="/foo/bar") | ||
|
||
|
||
if __name__ == "__main__": | ||
tb_test.main() |