Skip to content

Inconsistent URL decoding and unexpected browser URL replacement in GoRouter (web) #180373

Closed

Description

@apinilabs-pascal

What package does this bug report belong to?

go_router

What target platforms are you seeing this bug on?

Web

Have you already upgraded your packages?

Yes

Dependency versions

pubspec.lock
 go_router:
    dependency: "direct main"
    description:
      name: go_router
      sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a
      url: "https://pub.dev"
    source: hosted
    version: "17.0.1"
  go_router_builder:
    dependency: "direct dev"
    description:
      name: go_router_builder
      sha256: "8f1dd8cc23850dba4223104231682a9410576720cb24693fb0eb925c49b2abe3"
      url: "https://pub.dev"
    source: hosted
    version: "4.1.3"

Steps to reproduce

  1. Define a route with a path parameter, e.g. /user/:id.
  2. Navigate internally with context.go('/user/${Uri.encodeComponent("foo bar")}').
  3. Refresh the page or enter /user/foo%20bar directly in the browser.
  4. Compare GoRouterState.pathParameters['id'] and the browser’s address bar value in both cases.

Expected results

  • Consistent decoding: Path parameters should be decoded (or not) in the same way regardless of navigation source. Developers should not need to special-case deep links vs in-app navigation.
  • Stable browser URL: On web, the address bar should not be rewritten unless the app explicitly performs a redirect. The URL the user enters should remain visible as-is.

Suggested Resolution

  • Decide on a single, consistent policy for URL decoding (either always decode once, or always leave encoded).
  • Ensure that on web, GoRouter does not rewrite the browser’s address bar unless a redirect is explicitly configured.
  • Consider exposing helpers in path_utils for safe, balanced encode/decode handling.

Actual results

  • Path parameters are decoded for deep links but remain encoded for in-app navigation.
  • Browser URL is silently replaced with a canonicalized/decoded version, even without redirects.

Impact

  • Developers cannot reliably extract parameters without risking double-decoding or mismatched values.
  • Users see the URL they entered change unexpectedly, which is confusing and can break copy/paste or bookmarking workflows.

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:go_router/go_router.dart';

void main() {
  usePathUrlStrategy();

  runApp(const MyApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => const HomePage()),
    GoRoute(
      path: '/item/:value',
      builder: (context, state) {
        final value = state.pathParameters['value'] ?? '';
        return DetailPage(value: value);
      },
    ),
  ],
);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'GoRouter Web Minimal',
      routerConfig: _router,
      theme: ThemeData(useMaterial3: true),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _controller = TextEditingController(text: 'foo bar');

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _goToDetail() {
    var raw = _controller.text.trim();
    raw = Uri.encodeComponent(raw);
    if (raw.isEmpty) {
      return;
    }
    // Encode to keep the URL valid on web.
    context.go('/item/${Uri.encodeComponent(raw)}');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 400),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: _controller,
                decoration: const InputDecoration(
                  labelText: 'Enter raw id',
                  border: OutlineInputBorder(),
                ),
                onSubmitted: (_) => _goToDetail(),
              ),
              const SizedBox(height: 12),
              ElevatedButton(
                onPressed: _goToDetail,
                child: const Text('Go to second page'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final String value;
  const DetailPage({super.key, required this.value});

  @override
  Widget build(BuildContext context) {
    final encoded = value;
    final decoded = Uri.decodeComponent(encoded);
    return Scaffold(
      appBar: AppBar(title: const Text('Second Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(
              'Encoded: $encoded',
              style: Theme.of(context).textTheme.titleLarge,
              textAlign: TextAlign.center,
            ),
            Text(
              'Decoded: $decoded',
              style: Theme.of(context).textTheme.titleLarge,
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.38.5, on Microsoft Windows [Version 10.0.26100.7462], locale en-CH)
[√] Windows Version (11 Pro 64-bit, 24H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.0.0-rc5)
[√] Chrome - develop for the web
[√] Connected device (2 available)
[√] Network resources

Activity

  1. darshankawar commented on Jan 5, 2026

    @darshankawar
    Member

    Seeing same behavior on latest sdk versions.

  2. added
    platform-webWeb applications specifically
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Jan 5, 2026
  3. added
    P2Important issues not at the top of the work list
    on Jan 14, 2026
  4. apinilabs-pascal commented on Jul 21, 2026

    @apinilabs-pascal
    ContributorAuthor

    any update on this?

  5. apinilabs-pascal commented on Jul 22, 2026

    @apinilabs-pascal
    ContributorAuthor

    same issue for query parameter by the way.

  6. github-actions commented on Oct 17, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  7. locked as resolved and limited conversation to collaborators on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listfound in release: 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.platform-webWeb applications specificallyteam-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions