Fixing asterisk keys in Laravel URI queries

Adding a literal * query parameter could overwrite the existing ones. The same bug as Request::merge(), with a smaller fix.

I fixed another asterisk-key bug in Laravel. If you added ['*' => 'all'] to a URI with withQuery(), every existing query parameter could become all. The * parameter itself wasn't added.

With no query parameters to begin with, the new key simply disappeared. This is the URI equivalent of the request-merging bug from my first post.

Adding one parameter changed the others

Suppose you're building a URL with a language and a page number. This controller method returns its query parameters so the result is easy to inspect. The class imports Illuminate\Support\Uri and Illuminate\Http\JsonResponse:

// app/Http/Controllers/SearchController.php
public function preview(): JsonResponse
{
    $uri = Uri::of('https://example.com/search?lang=en&page=2')
        ->withQuery(['*' => 'all']);

    return response()->json($uri->query()->all());
}

You'd expect:

{
    "lang": "en",
    "page": "2",
    "*": "all"
}

Before the fix, you got:

{
    "lang": "all",
    "page": "all"
}

You're adding one parameter, but the language and page number are both overwritten. A nested key such as filter.* had the same effect on the parameters inside filter.

Two calls to the same helper

Inside src/Illuminate/Support/Uri.php, withQuery() has two branches. By default it merges into the current query. With merge: false, it starts with an empty array and builds a replacement.

Both branches used data_set(). These are the two affected loops before the fix, with the surrounding method omitted:

// Merge into the existing query.
foreach ($query as $key => $value) {
    data_set($mergedQuery, $key, $value);
}

// Build a replacement query.
foreach ($query as $key => $value) {
    data_set($newQuery, $key, $value);
}

data_set() interprets * as a wildcard. In the first loop, that overwrote existing values. In the second, the wildcard had nothing to update, so the key wasn't stored.

The patched method

PR #61312 replaces those two calls with Arr::set(). Here is the complete method after the fix:

// src/Illuminate/Support/Uri.php
public function withQuery(array $query, bool $merge = true): static
{
    foreach ($query as $key => $value) {
        if ($value instanceof UrlRoutable) {
            $query[$key] = $value->getRouteKey();
        }
    }

    if ($merge) {
        $mergedQuery = $this->query()->all();

        foreach ($query as $key => $value) {
            Arr::set($mergedQuery, $key, $value);
        }

        $newQuery = $mergedQuery;
    } else {
        $newQuery = [];

        foreach ($query as $key => $value) {
            Arr::set($newQuery, $key, $value);
        }
    }

    return new static($this->uri->withQuery(Arr::query($newQuery) ?: null));
}

Arr::set() keeps asterisks literal while retaining dot notation. A key such as filter.name still updates name inside filter; filter.* now adds a key named * there.

The method returns a new URI instance. The original URI stays unchanged.

How this differs from the request fix

Both bugs came from using a wildcard-aware helper to store literal keys. But the surrounding code made one fix slightly more involved:

Request::merge() Uri::withQuery()
Builds Request input URI query parameters
Iterates with A reducer callback foreach loops
Fix Change the helper and return the full input Change the helper in both loops

In the first post, returning Arr::set() directly from the reducer would have passed a nested array into the next iteration for dotted keys. The callback had to return $requestInput explicitly.

Here, the loops ignore the helper's return value. Arr::set() updates each array by reference, and the method uses that array afterward. No extra return statement is needed inside either loop.

Replacement queries and missing parameters

Fixing only the default merge branch would have left merge: false broken. The regression tests check that replacing the query with ['*' => 'all'] actually keeps that key.

They also cover an empty query, nested asterisks, and withQueryIfMissing(). That last method delegates to withQuery(), so it benefits from the same fix. Existing dot-notation tests continue to cover ordinary nested updates.

My PR #61312 was merged into 13.x on August 24, 2026, a few minutes after the request-merging fix.

Thanks for reading.
Hristijan