-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
application: extract path prefix logic to middleware #2733
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the semantics are more obvious if we require that the path prefix end in a slash, since then we don't necessarily even need to support the redirect, and there's no confusion about what the canonical URL will be. WDYT? I think we could fairly consider that not to be a breaking change given that using the prefix without the trailing slash is broken today due to the lack of redirect. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m fine with making that change at the user level (i.e., a requirement Given that this PR doesn’t actually make any user-facing changes modulo |
||
""" | ||
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) |
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no action required; I simply dunno: how does our server respond when the path is "" vs. "/"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. The answer is that Werkzeug normalizes that out, so our # main.py
import werkzeug
def bare_app(environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
return [repr(environ.get("PATH_INFO")).encode("utf-8")]
@werkzeug.Request.application
def werkzeug_app(request):
return werkzeug.Response(repr(request.path), content_type="text/plain")
start_response = lambda status, headers: None
request_empty = {"REQUEST_METHOD": "GET", "PATH_INFO": ""}
request_root = {"REQUEST_METHOD": "GET", "PATH_INFO": "/"}
print("With base WSGI:")
print(list(bare_app(request_empty, start_response)))
print(list(bare_app(request_root, start_response)))
print("With Werkzeug:")
print(list(werkzeug_app(request_empty, start_response)))
print(list(werkzeug_app(request_root, start_response)))
|
||
|
||
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I had been thinking we could generalize this as taking a dict mapping prefixes to WSGI apps. This unblocks using this for the plugin routing as well as described in #2573.
Feel free to leave this PR as-is, but just FYI, I think it will make sense to fold this into a more generic middleware sooner or later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me.