Skip to content

Commit 90afe93

Browse files
committed
Create addParams, replacing queryEncoder
Summary: The existing `queryEncoder` function had a significant flaw: it always returned a string starting with `"?"`, but the initial delimiter should be `"&"` if the URL already contains a query component. This could happen unexpectedly if, say, an application embeds TensorBoard and monkey-patches the router to include some parameters in every URL, so that a call like `pluginRoute` has query parameters where the original author of the plugin expects none. A simple solution is to replace this with a function `addParams` that constructs the query string and attaches it to the URL in one go, adding to an existing string if one exists. This new function also now supports repeated parameters for convenience. Test Plan: I tested that all existing plugins work as intended. I didn’t test the JS test because we have no way to run them. wchargin-branch: add-params
1 parent b9ce895 commit 90afe93

File tree

4 files changed

+60
-22
lines changed

4 files changed

+60
-22
lines changed

tensorboard/components/tf_backend/router.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
1515

16-
import {queryEncoder} from './urlPathHelpers.js';
16+
import {addParams} from './urlPathHelpers.js';
1717

1818
export type RunTagUrlFn = (tag: string, run: string) => string;
1919

@@ -41,7 +41,7 @@ export function createRouter(dataDir = 'data', demoMode = false): Router {
4141
function standardRoute(route: string, demoExtension = '.json'):
4242
((tag: string, run: string) => string) {
4343
return function(tag: string, run: string): string {
44-
return dataDir + '/' + route + queryEncoder({tag: tag, run: run});
44+
return dataDir + '/' + addParams(route, {tag, run});
4545
};
4646
}
4747
function pluginRoute(pluginName: string, route: string): string {
@@ -50,7 +50,7 @@ export function createRouter(dataDir = 'data', demoMode = false): Router {
5050
function pluginRunTagRoute(pluginName: string, route: string):
5151
((tag: string, run: string) => string) {
5252
const base = pluginRoute(pluginName, route);
53-
return (tag, run) => base + queryEncoder({tag, run});
53+
return (tag, run) => addParams(base, {tag, run});
5454
}
5555
return {
5656
logdir: () => dataDir + '/logdir',

tensorboard/components/tf_backend/test/backendTests.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,26 @@ limitations under the License.
1616
import {filterTags, getRuns, getTags, RunToTag, TYPES} from '../backend.js';
1717
import {RequestManager} from '../requestManager.js';
1818
import {createRouter, setRouter} from '../router.js';
19-
import {queryEncoder} from '../urlPathHelpers.js';
19+
import {addParams} from '../urlPathHelpers.js';
2020

2121
describe('urlPathHelpers', () => {
22-
it('queryEncoder works on spaces and parens', () => {
23-
const params = {foo: 'something with spaces and (parens)'};
24-
const actual = queryEncoder(params);
25-
const expected = 'something+with+spaces+and+%28parens%29';
22+
it('addParams leaves input untouched when there are no parameters', () => {
23+
const actual = addParams('http://foo', {a: undefined, b: undefined});
24+
const expected = 'http://foo';
25+
chai.assert.equal(actual, expected);
26+
});
27+
it('addParams adds parameters to a URL without parameters', () => {
28+
const actual = addParams(
29+
'http://foo',
30+
{a: "1", b: ["2", "3+4"], c: "5", d: undefined});
31+
const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5';
32+
chai.assert.equal(actual, expected);
33+
});
34+
it('addParams adds parameters to a URL with parameters', () => {
35+
const actual = addParams(
36+
'http://foo?a=1',
37+
{b: ["2", "3+4"], c: "5", d: undefined});
38+
const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5';
2639
chai.assert.equal(actual, expected);
2740
});
2841
});

tensorboard/components/tf_backend/urlPathHelpers.ts

+37-12
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,42 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
15-
export function queryEncoder(params?: any): string {
16-
// It's important that the keys be sorted, so we always grab the right file
17-
// if we are talking to the backend generated by serialze_tensorboard.py
18-
if (params == null) {
19-
return '';
15+
/**
16+
* A query parameter value can either be a string or a list of strings.
17+
* A string `"foo"` is encoded as `key=foo`; a list `["foo", "bar"]` is
18+
* encoded as `key=foo&key=bar`.
19+
*/
20+
export type QueryValue = string | string[];
21+
22+
/**
23+
* Add query parameters to a URL. Values will be URL-encoded. The URL
24+
* may or may not already have query parameters. For convenience,
25+
* parameters whose value is `undefined` will be dropped.
26+
*
27+
* For example, the following expressions are equivalent:
28+
*
29+
* addParams("http://foo", {a: "1", b: ["2", "3+4"], c: "5"})
30+
* addParams("http://foo?a=1", {b: ["2", "3+4"], c: "5", d: undefined})
31+
* "http://foo?a=1&b=2&b=3%2B4&c=5"
32+
*/
33+
export function addParams(
34+
baseURL: string,
35+
params: {[param: string]: QueryValue}): string {
36+
const keys = Object.keys(params).sort().filter(k => params[k] !== undefined);
37+
if (!keys.length) {
38+
return baseURL; // no need to change '/foo' to '/foo?'
2039
}
21-
const components = _.keys(params)
22-
.sort()
23-
.filter((k) => params[k] !== undefined)
24-
.map((k) => k + '=' + encodeURIComponent(params[k]));
25-
const result = components.length ? '?' + components.join('&') : '';
26-
// Replace parens for consistency with urllib.urlencode
27-
return result.replace(/\(/g, '%28').replace(/\)/g, '%29');
40+
const delimiter = baseURL.indexOf('?') !== -1 ? '&' : '?';
41+
const parts = [].concat(...keys.map(key => {
42+
const rawValue = params[key];
43+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
44+
return values.map(value => `${key}=${_encodeURIComponent(value)}`);
45+
}));
46+
const query = parts.join('&');
47+
return baseURL + delimiter + query;
48+
}
49+
50+
function _encodeURIComponent(x: string): string {
51+
// Replace parentheses for consistency with Python's urllib.urlencode.
52+
return encodeURIComponent(x).replace(/\(/g, '%28').replace(/\)/g, '%29');
2853
}

tensorboard/plugins/graph/tf_graph_dashboard/tf-graph-dashboard.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ <h3>No graph definition files were found.</h3>
129129
import {Canceller} from "../tf-backend/canceller.js";
130130
import {compareTagNames} from "../vz-sorting/sorting.js";
131131
import {getRouter} from "../tf-backend/router.js";
132-
import {queryEncoder} from "../tf-backend/urlPathHelpers.js";
132+
import {addParams} from "../tf-backend/urlPathHelpers.js";
133133

134134
Polymer({
135135
is: 'tf-graph-dashboard',
@@ -271,7 +271,7 @@ <h3>No graph definition files were found.</h3>
271271
'large_attrs_key': optional(largeAttrsKey),
272272
};
273273
const extension = demoMode ? '.pbtxt' : '';
274-
return base + queryEncoder(parameters) + extension;
274+
return addParams(base, parameters) + extension;
275275
},
276276
_shouldRequestHealthPills: function() {
277277
// Do not load debugger data if the feature is disabled, if the user toggled off the feature,

0 commit comments

Comments
 (0)