DioxusLabs / DioxusLabs/dioxus
Query values containing `&` are silently truncated, and percent-escaping them doesn't help
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
**Problem**
I think the query string is percent-decoded as a whole before it gets split on `&`, which (unless I'm misreading the generated code) would mean a value containing `&` can't survive a round trip, and can't be rescued by escaping it either. I may well be missing something here, so I've tried to make this easy to check.
Escaping doesn't seem to be a way out. Parsing `/search?query=one%20%26%20two&sort=name`, where the `&` is properly escaped, also gives back `"one "`.
**Steps To Reproduce**
With this route:
```rust
#[derive(Routable, Clone, PartialEq, Debug)]
enum Route {
#[route("/search?:query&:sort")]
Search { query: String, sort: String },
}
```
round-tripping a few values through `to_string()` and then `parse()`:
| input value | written URL | parsed back as |
|---|---|---|
| `one & two` | `/search?query=one%20&%20two&sort=name` | `"one "` |
| `%26` (literal text) | `/search?query=%26&sort=name` | `""` |
| `a=b` | `/search?query=a=b&sort=name` | `"a=b"` (ok) |
| `100% sure` | `/search?query=100%%20sure&sort=name` | `"100% sure"` (ok) |
| `a+b`, `a/b`, `a?b`, `a#b` | | all ok |
So this looks specific to `&`, plus text that happens to look like a percent-escape. `=` is fine, since `split_once('=')` only splits at the first one.
**Where I think it comes from**
In `dioxus-router-macro/src/lib.rs`, the generated `from_str` decodes the whole query up front:
```rust
let (route, query) = route.split_once('?').unwrap_or((route, ""));
let query = percent_decode_str(query).decode_utf8().unwrap_or(query.into());
```
and `query.rs` splits afterwards:
```rust
let split_query: HashMap<&str, &str> =
query.split('&').filter_map(|s| s.split_once('=')).collect();
```
If that ordering is the cause, an escaped `%26` would be turned back into a real `&` before the split ever sees it, which would explain why escaping doesn't help. On the write side, `ToQueryArgument`'s blanket impl is `write!(f, "{}={}", query_name, self)`, and `QUERY_ASCII_SET` doesn't include `&`, so the value goes out raw.
**Expected behavior**
I'd expect a value to survive a `to_string()` and `parse()` round trip whatever characters it contains, or failing that, for the docs to say which characters aren't safe in a query value.
The guide's own examples use `String` query values (`?:name&:surname`, and a `?:query&:sort` search box), and free-text search input seems like a realistic place for an `&` to turn up. Since the docs also say "parsing a Query segment must not fail", the value ends up silently truncated rather than raising an error, which took me a while to notice. The tests in `tests/parsing.rs` use numeric values, which may be why it hasn't come up before.
That said, it's quite possible this is a known trade-off, or that there's a constraint I can't see from outside.
**Possible directions**
I'm not sure which of these fits your plans, and there may be better options I haven't considered:
- Split on `&` and `=` first, then percent-decode each key and value individually. That's what `form_urlencoded` and `UrlSearchParams` do, and I think it would also cover #5280 (`+` as space), since that's a per-value decoding concern too. The catch is that a spread `?:..params` `FromQuery` impl currently receives a decoded string, so this would change what those impls see. (I checked: `?raw=a%26b` currently reaches `from_query` as `raw=a&b`.)
- Escape `&` in the value on write. This looks harder than it sounds, because `ToQueryArgument` formats the whole `key=value` pair, so the separator and the value can't be escaped differently without changing that trait. Adding `&` or `=` to `QUERY_ASCII_SET` would escape the separator too, which I gather is what #2984 was about.
Happy to put a PR together if you tell me which direction you'd prefer. I'd rather get your steer first, since both options look like breaking changes.
**Workaround for anyone else who hits this**
Escaping twice on write (`&` becomes `%2526`) survives the single decode pass; then decode once more inside your own `FromQuery`.
**Environment:**
- Dioxus version: 0.7.10 (appears to be present in 0.6 as well)
- Rust version: 1.99.0-nightly
- OS info: Linux 7.1.4 (Arch)
- App platform: web
**Questionnaire**
I would like to fix and I have a solution.
**Related:** #2984 (closed), #5280 (open, same parsing path, for `+`).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in dioxus-router-macro/src/lib.rs and query.rs, tracing the generated from_str path and query splitting order. Reproduce the ampersand and percent-escape cases, then inspect tests/parsing.rs and the related query behavior in #5280 and #2984. Done means the chosen parsing or escaping behavior is covered by round-trip tests, or the supported query-value characters are documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100