Repository: pallets/werkzeug

Issue:
url parameters casting bytes to str
Hi there! I noticed a change in behavior in werkzeug 0.15.x whereby url parameters used to be safe to send as bytes (on python 3.6).

I apologize for not showing a more direct example in werkzeug, but like most users of werkzeug - I don't use it directly - I use it through flask.

Demonstration (using Flask)

```python
from flask import Flask, url_for

app = Flask(__name__)
app.config['SERVER_NAME'] = 'www.foo.com'


@app.route('/<string:foo>')
def hello(foo):
    return 'Hello, World!'


with app.app_context():
    print(url_for('hello', foo=b'bar'))
```

With `werkzeug<0.15`, this will output:

```
http://www.foo.com/bar
```

However as of `werkzeug>=0.15`, it now returns this malformed url:

```
http://www.foo.com/b%27bar%27
```

I would expect that if `werkzeug` isn't going to decode bytes for you anymore, that it ought to raise a `TypeError` indicating that `bytes` is no longer safe to pass in.

I hope this example makes sense! Thanks for reviewing my issue - I'd be happy to (attempt to) submit a pull request if y'all agree that this is not desirable behavior.
url parameters casting bytes to str
Hi there! I noticed a change in behavior in werkzeug 0.15.x whereby url parameters used to be safe to send as bytes (on python 3.6).

I apologize for not showing a more direct example in werkzeug, but like most users of werkzeug - I don't use it directly - I use it through flask.

Demonstration (using Flask)

```python
from flask import Flask, url_for

app = Flask(__name__)
app.config['SERVER_NAME'] = 'www.foo.com'


@app.route('/<string:foo>')
def hello(foo):
    return 'Hello, World!'


with app.app_context():
    print(url_for('hello', foo=b'bar'))
```

With `werkzeug<0.15`, this will output:

```
http://www.foo.com/bar
```

However as of `werkzeug>=0.15`, it now returns this malformed url:

```
http://www.foo.com/b%27bar%27
```

I would expect that if `werkzeug` isn't going to decode bytes for you anymore, that it ought to raise a `TypeError` indicating that `bytes` is no longer safe to pass in.

I hope this example makes sense! Thanks for reviewing my issue - I'd be happy to (attempt to) submit a pull request if y'all agree that this is not desirable behavior.


Discussion:
I'll have a look at this. We (evidently) don't have tests for this, so I suspect accepting bytes was never intended behaviour.
I'll have a look at this. We (evidently) don't have tests for this, so I suspect accepting bytes was never intended behaviour.

Resolving patch:
diff --git a/src/werkzeug/routing.py b/src/werkzeug/routing.py
--- a/src/werkzeug/routing.py
+++ b/src/werkzeug/routing.py
@@ -1052,8 +1052,9 @@ def compile(self, append_unknown=True):
                 # resolve it to a constant ahead of time
                 if is_dynamic and data in self.defaults:
                     data = self.rule._converters[data].to_url(self.defaults[data])
-                    is_dynamic = False
-                if not is_dynamic:
+                    opl.append((None, data))
+                    continue
+                elif not is_dynamic:
                     opl.append(
                         (
                             None,
@@ -1260,6 +1261,8 @@ def to_python(self, value):
         return value
 
     def to_url(self, value):
+        if isinstance(value, (bytes, bytearray)):
+            return _fast_url_quote(value)
         return _fast_url_quote(text_type(value).encode(self.map.charset))
 
 


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -822,6 +822,18 @@ def test_double_defaults(prefix):
     assert a.build("x", {"bar": True}) == prefix + "/bar/"
 
 
+def test_building_bytes():
+    m = r.Map(
+        [
+            r.Rule("/<a>", endpoint="a"),
+            r.Rule("/<b>", defaults={"b": b"\x01\x02\x03"}, endpoint="b"),
+        ]
+    )
+    a = m.bind("example.org", "/")
+    assert a.build("a", {"a": b"\x01\x02\x03"}) == "/%01%02%03"
+    assert a.build("b") == "/%01%02%03"
+
+
 def test_host_matching():
     m = r.Map(
         [