inertia-laravel icon indicating copy to clipboard operation
inertia-laravel copied to clipboard

Custom `rootView` and shared data set in middleware not taken into account for errors (eg. 404)

Open rgehan opened this issue 5 years ago • 30 comments

The new HandleInertiaRequests middleware exposes a rootView property, allowing to set a custom root view.

It works fine for regular Inertia::render in controllers, but doesn't seem to be taken into account when returning custom error views, as the doc suggests.

In my case, this happens for a 404 error. I imagine Laravel handles such errors differently: the request might not go through the middleware stack.

I just realized that it wouldn't send the shared data to the error page either, nor would it version.

Here's a stack trace:

[2020-10-29 15:26:42] local.ERROR: View [app] not found. {"exception":"[object] (InvalidArgumentException(code: 0): View [app] not found. at /<project>/vendor/laravel/framework/src/Illuminate/View/FileViewFinder.php:137)
[stacktrace]
#0 /<project>/vendor/laravel/framework/src/Illuminate/View/FileViewFinder.php(79): Illuminate\\View\\FileViewFinder->findInPaths('app', Array)
#1 /<project>/vendor/laravel/framework/src/Illuminate/View/Factory.php(138): Illuminate\\View\\FileViewFinder->find('app')
#2 /<project>/vendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php(85): Illuminate\\View\\Factory->make('app', Array)
#3 /<project>/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(261): Illuminate\\Routing\\ResponseFactory->view('app', Array)
#4 /<project>/vendor/inertiajs/inertia-laravel/src/Response.php(93): Illuminate\\Support\\Facades\\Facade::__callStatic('view', Array)
#5 /<project>/app/Exceptions/Handler.php(51): Inertia\\Response->toResponse(Object(Illuminate\\Http\\Request))
#6 /<project>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(416): App\\Exceptions\\Handler->render(Object(Illuminate\\Http\\Request), Object(InvalidArgumentException))
#7 /<project>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(114): Illuminate\\Foundation\\Http\\Kernel->renderException(Object(Illuminate\\Http\\Request), Object(InvalidArgumentException))
#8 /<project>/public/index.php(70): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#9 /Users/<user>/.composer/vendor/laravel/valet/server.php(191): require('/Users/<user>/d...')
#10 {main}
"}

// app/Http/Middleware/HandleInertiaRequests.php

class HandleInertiaRequests extends Middleware
{
    protected $rootView = 'layouts.dashboard';

    // ...
}
// app/Exceptions/Handler.php

class Handler extends ExceptionHandler
{
    // ...

    public function render($request, Throwable $e)
    {
        $response = parent::render($request, $e);

        if (in_array($response->status(), [403, 404, 500, 503])) {
            return Inertia::render('public/Error', ['status' => $response->status()])
                ->toResponse($request)
                ->setStatusCode($response->status());
        }

        return $response;
    }
}

rgehan avatar Oct 29 '20 15:10 rgehan

Hi,

Thanks for this! You're absolutely right, it doesn't go through the Middleware stack, so the root view never gets changed.

For now, you can work around this by using Inertia::setRootView('customView') directly before the Inertia::render() call, but to fix this I think it might be a good idea to make the Inertia::setRootView method chainable, so that this can all be done in a single expression.. Something like:

Inertia::setRootView('customView')->render('Error', [
    'key' => 'value'
]]);

@reinink what do you think?

claudiodekker avatar Oct 30 '20 10:10 claudiodekker

The thing is, that isn't the only thing that's broken.

After working around that issue, I've realized that shared data wasn't set either (neither versioning)

So we'd have to duplicate both setRootView and shared data logic in the error handler too :/

For now, I've just reverted to the old way of setting the root view, and the shared data.

// In a service provider

Inertia::setRootView('layouts.dashboard');

Inertia::share([
  // ...
]);

rgehan avatar Oct 30 '20 10:10 rgehan

Hmm, this is an unfortunate side affect to moving to the middleware approach to configuring Inertia. 😕

As sad as it is to say, this makes me think that we need to go back to the service provider approach. However, I think we can maybe improve that experience by providing an Inertia specific service provider. Something like this:

<?php

namespace App\Providers;

use Inertia\ServiceProvider;
use Illuminate\Http\Request;

class InertiaServiceProvider extends ServiceProvider
{
    /**
     * The root template that's loaded on the first page visit.
     *
     * @see https://inertiajs.com/server-side-setup#root-template
     * @var string
     */
    protected $rootView = 'app';

    /**
     * Determines the current asset version.
     *
     * @see https://inertiajs.com/asset-versioning
     *
     * @return string|null
     */
    public function version(Request $request)
    {
        return parent::version($request);
    }

    /**
     * Defines the props that are shared by default.
     *
     * @see https://inertiajs.com/shared-data
     *
     * @return array
     */
    public function share(Request $request)
    {
        return array_merge(parent::share($request), [
            // your data
        ]);
    }
}

reinink avatar Oct 30 '20 10:10 reinink

What was the reasoning with moving from Inertia:: calls in a SP, to a middleware? What problems did it solve?

IMO, the service provider approach was fine, it just lacked a default "skeleton" that users could customize.

With your suggestion, we have the best of both worlds :)

rgehan avatar Oct 30 '20 11:10 rgehan

It was argued that it was more proper to do this work in a middleware, since otherwise you're doing Inertia configuration while running console commands.

I personally don't think it matters, as long as this work is all done using lazily evaluated callbacks. And really, this approach is no different than what Taylor does with Fortify (and many other first class Laravel packages):

use Laravel\Fortify\Fortify;

Fortify::loginView(function () {
    return view('auth.login');
});

reinink avatar Oct 30 '20 11:10 reinink

It was argued that it was more proper to do this work in a middleware

Naive question, but can't we just use app()->runningInConsole() to shunt the provider when necessary?

I can see where this is coming from though, the argument makes sense. If it had worked for errors, it would have been a perfectly valid solution.

rgehan avatar Oct 30 '20 11:10 rgehan

Naive question, but can't we just use app()->runningInConsole() to shunt the provider when necessary?

Probably!

reinink avatar Oct 30 '20 11:10 reinink

I do like @claudiodekker proposal. For that one error page be able to override the root element ... done. I wouldnt know why anyone would like to share props to an error page

herpaderpaldent avatar Oct 30 '20 12:10 herpaderpaldent

Naive question, but can't we just use app()->runningInConsole() to shunt the provider when necessary?

Probably!

Definitely, if we're extending the 'base' service provider (like we're doing with the 'base' middleware now), we can hide all of that logic within the Inertia package 👍

I do like @claudiodekker proposal. For that one error page be able to override the root element ... done. I wouldnt know why anyone would like to share props to an error page

Because error pages might extend layouts, which might use certain shared props (e.g. the user) ;)

claudiodekker avatar Oct 30 '20 12:10 claudiodekker

I wouldnt know why anyone would like to share props to an error page

Maybe they want to share their app name with the error page? I don't think that just because we can't think of a use-case, that this isn't something we should support.

We also need to consider the version. If you don't set the version, and then you create an Inertia link on your error page, you'll immediately get a full page reload when you click to go elsewhere, which isn't a great experience.

reinink avatar Oct 30 '20 12:10 reinink

You've perfectly described my use case:

  • I have a custom layout, which isn't app, hence the setRootView
  • That layout relies on some globals that are available everywhere else (the user being one of them)
  • The error page uses that layout, and still needs the user prop

That definitely needs to be supported, especially since it used to work perfectly fine.

rgehan avatar Oct 30 '20 12:10 rgehan

Ok, got it. Anywhere, i hope the solution in the end, will be working with packages too. To original implementation did, middleware did so too ...

A way could be: not to support sharing props with middleware way of things. Anyone wanting to such a thing might need to share and set rootview via the service provider

herpaderpaldent avatar Oct 30 '20 12:10 herpaderpaldent

@herpaderpaldent It'd work even better with packages, because you could let your package auto-register a service provider with Laravel, or am I missing something?

claudiodekker avatar Oct 30 '20 13:10 claudiodekker

If I'm not mistaken, packages also have the ability to register middlewares (pushMiddleware I think?), so in both cases it should work.

rgehan avatar Oct 30 '20 13:10 rgehan

Yeyea thats what i mean rightnow middleware and via Inertia::share() works just fine from a package. An inertia serviceprovider from the inertia package makes me thinking

herpaderpaldent avatar Oct 30 '20 14:10 herpaderpaldent

The following workaround also works for sharing data but it seems not very nice to "misuse" a middleware this way.

Code in Exception Handler:

Inertia::share((new HandleInertiaRequests)->share($request));

mrmonat avatar Oct 31 '20 18:10 mrmonat

I have discovered through trouble shooting that if you have the middleware active on a root the middleware overwrites you setting the rootView in controller or in my case the parent controller.

I think that there should be some sort of check to see if there is a root view set and if not set a changeable default.

jny986 avatar Nov 18 '20 08:11 jny986

👀

emargareten avatar Feb 17 '21 11:02 emargareten

I also ran into this problem recently. The only working solution i was able to find add is to add next two lines in custom Exception Handler before calling render:

Inertia::setRootView('my.custom.layout'); Inertia::share((new HandleInertiaRequests)->share($request));

I hope the issue will be solved soon...

dmanva avatar Feb 19 '21 17:02 dmanva

Related discussion: https://github.com/inertiajs/inertia/discussions/559

reinink avatar Mar 22 '21 13:03 reinink

Ran into the same issue, I think I also prefer moving the logic back to a service provider and bailing early in the console.

jelleroorda avatar Oct 10 '21 09:10 jelleroorda

Welcome to 2022, Like to confirm that, i too face the same problem.

anburocky3 avatar Feb 04 '22 18:02 anburocky3

Welcome to 2022, Like to confirm that, i too face the same problem.

IamSAL avatar Feb 07 '22 15:02 IamSAL

any update on this?

JenuelDev avatar May 24 '22 02:05 JenuelDev

any updates on any of the issues?

laserhybiz avatar May 24 '22 06:05 laserhybiz

Same issue here, @mrmonat's solution partially fix the issue as props explicitly set in "HandleInertiaRequests" middleware are passed to the view but not props like user, jetstream, ...

lucasdcrk avatar May 25 '22 12:05 lucasdcrk

I have the same issue and am quite surprised this hasn't been addressed yet.

sebastianfeistl avatar Nov 08 '23 19:11 sebastianfeistl

Currently facing the same issue, and would like to see this addressed as it has been discussed 3 years now and hasn't been solved yet.

medabkari avatar Nov 25 '23 07:11 medabkari

Still just a temporary fix, but this helped me add all the Jetstream/user values to the view:

use App\Http\Middleware\HandleInertiaRequests;
use Laravel\Jetstream\Http\Middleware\ShareInertiaData;

(new ShareInertiaData)->handle($request, fn () => null);
Inertia::share((new HandleInertiaRequests)->share($request));
return Inertia::render('public/Error', ['status' => $response->getStatusCode()])
    ->toResponse($request)
    ->setStatusCode($response->getStatusCode());

fritz-c avatar Mar 07 '24 22:03 fritz-c