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

Open Beginner friendly
#5,792 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust
Domain
web-dev

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.

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"] }:

use dioxus::prelude::*;
use dioxus::router::FromQuery;
use std::fmt::{self, Display, Formatter};

#[derive(Debug, Clone, PartialEq, Default)]
struct Params {
    search: Option<String>,
}

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<String> },
}

#[component]
fn Translations(params: Params) -> Element {
    rsx! {}
}

#[component]
fn Reset(token: Option<String>) -> 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.

Dominant language
Rust
Stars
39.2k
Forks
1.9k
Avg merge
3d 12h
Merged PRs (30d)
6

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from DioxusLabs/dioxus

All issues in DioxusLabs/dioxus

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.