DioxusLabs / DioxusLabs/dioxus
Router routes with query segments serialize a trailing `?` even when the query is empty
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
When a route has a query segment, its `Display` impl always writes the `?`, even when nothing follows it. With a spread segment (`?:..params`) and a params struct whose `Display` renders empty, the route serializes as `/translations?`, which then shows up in the address bar and in copied links. The same happens with a named segment on an `Option` field: `#[route("/reset?:token")]` with `token: None` gives `/reset?`, even though the `None` pair itself is correctly elided.
As best I can tell the `?` is written unconditionally in the router macro's query writer (`packages/router-macro/src/query.rs`), before the field's rendered output is known, and the same code is on main. It looks like an oversight rather than a design choice, since the pair elision (skipping `None` pairs and their `&` separators) is already there.
Repro on 0.7.10, with `dioxus = { version = "=0.7.10", features = ["router"] }`:
```rust
use dioxus::prelude::*;
use dioxus::router::FromQuery;
use std::fmt::{self, Display, Formatter};
#[derive(Debug, Clone, PartialEq, Default)]
struct Params {
search: Option,
}
impl Display for Params {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self.search {
Some(search) => write!(f, "search={search}"),
None => Ok(()),
}
}
}
impl FromQuery for Params {
fn from_query(query: &str) -> Self {
let search = query
.split('&')
.filter_map(|segment| segment.split_once('='))
.find(|(key, _)| *key == "search")
.map(|(_, value)| value.to_owned())
.filter(|value| !value.is_empty());
Self { search }
}
}
#[derive(Debug, Clone, PartialEq, Routable)]
enum Route {
#[route("/translations?:..params")]
Translations { params: Params },
#[route("/reset?:token")]
Reset { token: Option },
}
#[component]
fn Translations(params: Params) -> Element {
rsx! {}
}
#[component]
fn Reset(token: Option) -> Element {
rsx! {}
}
fn main() {
let empty = Route::Translations { params: Params::default() };
let filled = Route::Translations {
params: Params { search: Some("zone".to_owned()) },
};
let reset_none = Route::Reset { token: None };
println!("{empty}"); // /translations?
println!("{filled}"); // /translations?search=zone
println!("{reset_none}"); // /reset?
}
```
Expected: `/translations` and `/reset`. Actual: `/translations?` and `/reset?`.
Cosmetic, but visible on every navigation.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in packages/router-macro/src/query.rs, where the query writer is described as emitting the separator before rendering the field output. Run the supplied reproduction covering the empty spread segment and the None option; done means both serialize without a trailing `?`, while non-empty queries still include it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- web-dev
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100