DioxusLabs / DioxusLabs/dioxus

Router routes with query segments serialize a trailing `?` even when the query is empty

Open Beginner friendly
#5,792 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.