inertia icon indicating copy to clipboard operation
inertia copied to clipboard

Custom errors pages lose the application context and is not sending shared data

Open lighningwolf opened this issue 2 years ago • 10 comments

Versions:

"@inertiajs/inertia": "^0.10.1"
"@inertiajs/inertia-vue3": "^0.5.2"
"inertiajs/inertia-laravel": "^0.4.4"

Describe the problem:

When using custom error pages (i.e 404, 500 pages) Inertia doesn't send the SharedData to the client, and in the ExceptionHandler things like: auth()->user(), tenant() (from Tenancy for Laravel are nullwhile I expect user to be authenticated and on a specific tenant at the moment of the error.

My error pages are using the Layout of the app this is why I need my exception handler to send me this data.

What I've done so far

Followed the inertia js documentation about custom errors. My environment is local so I removed the check for this specific environment during the development.

It works well until I add my Layout to the page in the Vue component which was designed to receive som mandatory shared data.

lighningwolf avatar Nov 17 '21 16:11 lighningwolf

Hey @lighningwolf

I copied the sample code from the documentation to Laravel's App\Exceptions\Handler.php. When I trigger an error (e.g. abort(500)) and inspect the response on the browser's network tab, I do get a proper Inertia response including the shared properties:

{
    "component": "Error",
    "props": {
        "errors": {},
        "auth": {
            "user": {
                "id": 1,
               ...
            }
        },
        "test": "this is another shared property form App\\Http\\Middleware\\HandleInertiaRequests",
        "status": 500
    },
    "url": "/profile",
    "version": "a3dc1f89dc0f51e87d1a0c4e18c8e9f1"
}

I am using Laravel v8.69. So it seems to work out of the box. Maybe you can simply add a "test" property to the shared data, like I did? Or try with a fresh Laravel/Inertia install and narrow down the error there?

This is also more likely an issue for Inertia's Laravel adapter..

ajnsn avatar Nov 18 '21 07:11 ajnsn

I had the same issue as @lighningwolf. My Error.vue page uses a template that has required shared data however that data was not passed to Vue. ie: HandleInertiaRequests is not used. I had to pass the data manually like so:

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

    if (!app()->environment(['local', 'testing']) && in_array($response->status(), [500, 503, 404, 403])) {
        return Inertia::render('Error', ['status' => $response->status()])
            ->with([
                'user' => fn () => $request->user()
                    ? $request->user()->only('id', 'name', 'email')
                    : null,
            ])
            ->toResponse($request)
            ->setStatusCode($response->status());
    } else if ($response->status() === 419) {
        return back()->with([
            'message' => 'The page expired, please try again.',
        ]);
    }

    return $response;
}

devinfd avatar Nov 29 '21 18:11 devinfd

Hey @devinfd can you simply add a string as "test" to your share method, like i did above. Does it come through? What error / status code is your issue about?

Please also inspect your Inertia response wihtin the Browser Dev tools, look for the prop. Please share your HandleInertiaRequests middleware or maybe you share your repo to reproduce. Thank you!

ajnsn avatar Nov 29 '21 22:11 ajnsn

My HandleInertiaRequests:

class HandleInertiaRequests extends Middleware
{
    /**
     * 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
     * @param  \Illuminate\Http\Request  $request
     * @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
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function share(Request $request)
    {
        return array_merge(parent::share($request), [
            'appName' => config('app.name'),
            'test' => 'foo',
            'auth.user' => fn () => $request->user()
                ? $request->user()->only('id', 'name', 'email')
                : null
        ]);
    }
}

A "test" property in HandleInertiaRequests is not shared with Error.vue

All I wanted to do was create a file/view to handle 404 errors.

devinfd avatar Nov 29 '21 23:11 devinfd

Hey @devinfd Thank you! The 404 status code was the missing piece. Laravel does not apply route middleware groups from the $middlewareGroups-array inside Kernel.php if the route is not hit. Only the global HTTP middleware stack from the $middleware-array is applied.

You now think that could simply move Inertia's middelware to the global stack but this will not work as e.g. the Session store not set there and the middleware will throw an error.

It does work with all other errors, e.g. from your route-controllers though. So your solution with the manual props is possibly the easiest one.

Note that this is not a bug in Inertia but how Laravel works in general.

ajnsn avatar Nov 30 '21 06:11 ajnsn

Hi, I'm really sorry for the lack of response I have to admit I totally forgot about this issue I've had to move on other subjects despite this one was not solved.

@ajnsn I get what you said about the middleware I have to admit it was logical yet I didn't thought about it. What I don't get is what you mean about the status code, my Exception/Handler is sending the code (same way as what @devinfd shows in his code) so I don't understand what is the real missing part to fetch my inertia data, do I really need to duplicate my sharemethod from HandleInertiaRequests ? Because now I have:

Handler@render

/**
     * Prepare exception for rendering.
     *
     * @param $request
     * @param Throwable $e
     * @return JsonResponse|Response|\Symfony\Component\HttpFoundation\Response
     * @throws Throwable
     */
    public function render($request, Throwable $e): JsonResponse|Response|\Symfony\Component\HttpFoundation\Response
    {
        $response = parent::render($request, $e);

        $user = $request->user()
            ? User::where('id', $request->user()->id)
                ->with([
                    'roles' => fn($query) => $query->select('id', 'name')->with([
                        'permissions' => fn($query) => $query->select('name')
                    ]),
                ])
                ->select('id', 'email', 'name')
                ->sole()
            : null;
        $companies = [];
        if($user){
            $companies = $user->hasRole('admin') ? Company::all() : Collaborator::find($user->id)->companies;
        }

        if (!app()->environment(['local', 'testing']) && in_array($response->status(), [500, 503, 404, 403])) {
            return Inertia::render('Errors/Index', ['status' => $response->status()])
                ->with([
                    'flash' => [
                        'success' => session()->get('success'),
                        'error' => session()->get('error'),
                    ],
                    'route' => Route::currentRouteName(),
                    'tenant' => tenant(),
                    'companies' => $companies ?? [],
                    'auth.user' => $user
                ])
                ->toResponse($request)
                ->setStatusCode($response->status());
        }

        if ($response->status() === 419) {
            return back()->with([
                'message' => 'The page expired, please try again.',
            ]);
        }

        return $response;
    }
}

The with has the exact same attributes as what my share method returns in HandleInertiaRequests yet when I go to a wrong URL (which leads to a 404 exception) I have just an object full of null values

{
auth:{
  user:null
  }
companies:Array[0]
flash:{
  error:null
  success:null
},
route:null
tenant:null
}

What am I missing here ?

lighningwolf avatar Nov 30 '21 15:11 lighningwolf

Hey @lighningwolf Laravel's request lifecylce is described here. Long story, short: Some middleware's, like the Session (\Illuminate\Session\Middleware\StartSession::class) middleware, are only attached to middlware group web, look at your App\Http\Kernel.php. You also attach your HandleInertiaRequests middleware to this group as described in the docs.

If you now hit a unknown route (404), Laravel will only process some basic middleware and does not process the web middleware group. Therefore also some other things wont work, even if you pass them in your Exception handler, e.g.

  • session() - as there is no Session middleware available
  • Route::currentRouteName()- as there is no route found.

I cannot say anything about your tenant() method but the same most likely applies.

Hope this helps!

ajnsn avatar Nov 30 '21 16:11 ajnsn

@ajnsn thanks for your really quick answer, as you suggested I moved the HandleInertiaRequests::class from the webgroup to the $middleware array (with the StartSession) now if I go to an unknown route (i'm sticking with 404) I have nothing on screen but i i dd before the return in the handler I see the dump on screen yet my return Inertia::render()->asResponse..... does not render the requested error page, I might be abusing your time and I'm sorry but did you have any idea why ? English is not my primary language I might have missed something from what you said

lighningwolf avatar Nov 30 '21 16:11 lighningwolf

Hey @lighningwolf oh, I did not directly suggest to do that, sorry if that was unclear. Moving middleware from the web middleware group to the global middleware stack could have side effects.

You propably would encounter all these things if you use just use Laravel Blade, without Inertia as these are basic Laravel things.

Anyway here's a quick find about these issue and possible solution - but haven't tried that and it's from 2017 - so test carefully.

Edit: Fallback routes could also be an easy solution for the 404 issue. Just return Inertia::render inside the fallback-callback could work?

ajnsn avatar Nov 30 '21 18:11 ajnsn

Try this solution.

namespace App\Exceptions;

use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpKernel\Exception\HttpException;

class Handler extends \Illuminate\Foundation\Exceptions\Handler
{
    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        $this->renderable(function (HttpException $e, Request $request) {
            $code = $e->getStatusCode();

            $middleware = new HandleInertiaRequests;

            $inertia = Inertia::getFacadeRoot();

            $inertia->version($middleware->version($request));

            foreach ($middleware->share($request) as $key => $value) {
                $inertia->share($key, $value);
            }

            return $inertia->render('errors/' . $code)
                ->toResponse($request)
                ->setStatusCode($code);
        });
    }
}

Also add a fallback route to handle 404 error.

Route::fallback(fn () => inertia('errors/404'));

den1n avatar Aug 09 '22 07:08 den1n

Just add a fallback route for 404 error and follow the Error Handling guide https://inertiajs.com/error-handling

Route::fallback(fn () => abort(404));

https://laravel.com/docs/10.x/routing#fallback-routes

tuanpt-0634 avatar Jul 13 '23 15:07 tuanpt-0634

Hey! Thanks so much for your interest in Inertia.js and for sharing this issue/suggestion.

In an attempt to get on top of the issues and pull requests on this project I am going through all the older issues and PRs and closing them, as there's a decent chance that they have since been resolved or are simply not relevant any longer. My hope is that with a "clean slate" me and the other project maintainers will be able to better keep on top of issues and PRs moving forward.

Of course there's a chance that this issue is still relevant, and if that's the case feel free to simply submit a new issue. The only thing I ask is that you please include a super minimal reproduction of the issue as a Git repo. This makes it much easier for us to reproduce things on our end and ultimately fix it.

Really not trying to be dismissive here, I just need to find a way to get this project back into a state that I am able to maintain it. Hope that makes sense! ❤️

reinink avatar Jul 28 '23 01:07 reinink