Solved: CakePHP 3.10: problem with handling 500 error within default template

if a 500 error occurs then the output is nested with the specified template at the position where this error occurs. this leads to the entire layout no longer being correct.


i want a 500 error to be mapped (like a 404) within the default layout. the cakephp docs don’t give me any hint if this is possible at all.

config/app.php

'Error' => [
        'exceptionRenderer' => App\Error\AppExceptionRenderer::class,
        'errorLevel' => E_ALL & ~E_USER_DEPRECATED,
        'log' => true,
        'trace' => true,
        'skipLog' => [
            'Cake\Http\Exception\NotFoundException',
            'Cake\Http\Exception\UnauthorizedException',
        ],
    ],

Application.php

public function middleware($middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        ->add(new ErrorHandlerMiddleware(null, Configure::read('Error')))
        ->add(new AssetMiddleware([
            'cacheTime' => Configure::read('Asset.cacheTime'),
        ]))
        ->add(new RoutingMiddleware($this, '_cake_routes_'));

    return $middlewareQueue;
}

Error/AppExceptionRenderer.php

class AppExceptionRenderer extends ExceptionRenderer
{
    /**
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function render(): ResponseInterface
    {
        $this->_findHighlights();

        return parent::render();
    }

    /**
     * @return void
     */
    protected function _findHighlights()
    {
        $Products = TableRegistry::getTableLocator()->get('Products');

        $mostPopularProducts = $Products->find('forHighlights')->find('mostPopular');
        $newestProducts = $Products->find('forHighlights')->find('newestWeddingOrPartnerRings');

        $this->controller->set('mostPopularProducts', $mostPopularProducts);
        $this->controller->set('newestProducts', $newestProducts);
    }
}

Controller/ErrorController.php

public function beforeRender(Event $event)
{
    parent::beforeRender($event);

    $this->viewBuilder()->setHelpers(['Captcha.Captcha']);
    $this->viewBuilder()->setTemplatePath('Error');
}

Template/Error/error500.ctp

use Cake\Core\Configure;
use Cake\Error\Debugger;

$this->layout = 'default';

if (Configure::read('debug')):
    $this->layout = 'dev_error';

    $this->assign('title', $message);
    $this->assign('templateName', 'error500.ctp');

    $this->start('file'); ?>
    <?php if (!empty($error->queryString)): ?>
        <p class="notice">
            <strong>SQL Query: </strong>
            <?= h($error->queryString) ?>
        </p>
    <?php endif; ?>
    <?php if (!empty($error->params)): ?>
        <strong>SQL Query Params: </strong>
        <?php Debugger::dump($error->params) ?>
    <?php endif; ?>
    <?php if ($error instanceof Error): ?>
        <strong>Error in: </strong>
        <?= sprintf('%s, line %s', str_replace(ROOT, 'ROOT', $error->getFile()), $error->getLine()) ?>
    <?php endif; ?>
    <?php echo $this->element('auto_table_warning');

    if (extension_loaded('xdebug')) :
        xdebug_print_function_stack();
    endif;

    $this->end();
endif; ?>
<h2><?= __d('cake', 'An Internal Error Has Occurred') ?></h2>

ProductsController.php

public function view()
    {
        $id = $this->_retrieveProductId();

        if (!$id) {
            throw new NotFoundException();
        }

        $product = $this->Products->get($id, [
            'finder' => 'withFullInfo',
        ]);

        if (empty($product)) {
            throw new NotFoundException();
        } elseif ($product->status != 1) {
            $breadCrumbs = $product->getBreadCrumb();

            return $this->redirect($this->_getCategoryUrl($breadCrumbs));
        } elseif (!$this->_validateCurrentUrl($product)) {
            return $this->redirect($product->makeProductUrl(), 301);
        }

        if ($this->Products->exists(['variant_id' => $product->variant_id])) {
            $variant = $this->Products->Variants->get($product->variant_id);
            $variantsproducts = $this->Products->find('all', [
                'contain' => [
                    'Categories',
                    'ProductImages',
                    'WomenWidths',
                ],
            ])->where([
                'variant_id' => $product->variant_id,
                'status' => 1,
            ])->order(['WomenWidths.width' => 'ASC']);

            $this->set(compact('variant', 'variantsproducts'));
        }

        $this->set(compact(
            'product',
            'cartItem',
            'cartId',
            'eventID',
        ));
    }

All those details should only be if you have debug enabled, otherwise it’ll just be a plain error page that gives away no secrets.

i have disabled debug mode in this case
just want to display a message within the shop layout
“An Internal Error …
Please contact us…”

The layout you’ve included here clearly has

<?php
if (Configure::read('debug')):
    // All the details here
endif; ?>
<h2><?= __d('cake', 'An Internal Error Has Occurred') ?></h2>

So if debug is disabled, you should get nothing but the “An Internal Error Has Occurred” message on the screen.

If you’re seeing more than that, then either you haven’t really disabled debug, or you are somehow echoing output through non-standard methods.

'debug' => filter_var(env('DEBUG', false), FILTER_VALIDATE_BOOLEAN),

then

./bin/cake cache clear_all

reload page

layout is broken

but i don’t want to get an blank screen with just this message.
i need to have it mapped into the layout

In your controller, try just debug(Configure::read('debug')); to see what the debug setting is. Your initialization there is taking the DEBUG setting from the environment, and we can’t have any way of knowing what that was.

I misspoke slightly, sorry, it’s your page template that has just the error message in it. The layout will be unaffected, and the error message should be placed correctly inside of it. (Note the $this->layout = 'default'; at the top of that template’s code.)

what controller you mean?
tried at “ErrorController → initialize” and on “ProductsController → initialize”
but no output with “debug”, doing it with “dd” results with “false”

Okay, it sounds like you might actually have debug disabled. The bits of output you’re seeing before the error message, where are those coming from? Your default layout? Or some other piece of code?

yes, debug mode is disabled.
i have updated my question, added “ProductsController” code where an 500 error is thrown.
in this case the 500 error is triggered 'cause i try to access images that are not present

but that’s just an example - i’m looking for a solution to display an 500 Error mapped inside the shop layout

i don’t know why cakephp is wrapping the “default” layout with the default layout itself.
this is my problem to understand what happens here when an 500 error is thrown

Okay, I see what you mean now. There’s an img tag being created here with data-single-src being set to a bunch of HTML. So, let’s look at that. Where is that bit of HTML coming from? What is the code that’s populating that data-single-src attribute?

no you don’t and you can’t know that.
i have an funtion that tries to load images and i had an issue not checking if images are available.
the error that was been thrown was “Call to a member function getThumbnailUrl() on null”

this throws an 500 error

but i would like to show only an error message mapped inside the shop layout

but there are more cases that can throw an 500 error
i’m looking for an solution to map 50x error within the layout

and this is where i stuck at

I can only go on what’s been shown to me. You threw a lot of data in right from the start, and the problem is not obvious from any of that. I’m doing my best to help, on my own time. Please don’t be dismissive of the free efforts of strangers to help you solve your problems.

Now, the reason I said that is because your “wrapping the “default” layout with the default layout” comment led me to look more closely at the HTML you provided in your very first screen shot. There is a figure tag there, which has an a tag, which has an img tag. That img tag has a data-single-src attribute, which appears to be a complete HTML page, specifically the actual error page, instead of just some little bit of data. This appears to be the “wrapping” thing you’re talking about. So, my question is where that HTML is coming from, and specifically where the content of the data-single-src attribute is coming from. As far as I can tell, neither of these pieces of code have been shown so far.

It gets as far as this:

So what do the getIndexImage and getThumbnailUrl functions look like?

Well, those look very normal. Like I said before, nothing obviously wrong here, as far as I can tell. What you describe as your desired outcome is 100% what it normally does for errors. What you’re seeing does not match that. Dinner time for me now, maybe someone else will spot something I’ve missed in the meantime…

If you have xdebug set up, put a breakpoint at the very top of your src/Template/Layout/default.ctp and let us know what the call stack looks like when it gets hit. If you haven’t got xdebug, you could get the same by putting debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); at the top of that file, but that’ll be harder to extract specifics from.

That’s an old bug resurfacing, so to speak, when errors occur while rendering, the output buffer is not being closed, hence you’ll get the output rendered up until the point of error in the response.

The fix in the 3.x branch only covered exceptions, not throwables like the fix in the 4.x branch, and nobody really noticed that it should’ve covered both, probably because 3.x needs to support PHP 5.6+, and throwables are only a thing in PHP 7+.

Long story short, the 3.x branch doesn’t receive any bug fixes anymore, only security fixes, so you’ll have to fix it on your end. For example in your src/AppView.php, overwrite View::_evaluate() so that it catches Throwable instead of Exception (assuming that you run your app on PHP 7+ only):

protected function _evaluate($viewFile, $dataForView)
{
    extract($dataForView);

    $bufferLevel = ob_get_level();
    ob_start();

    try {
        include func_get_arg(0);
    } catch (\Throwable $exception) {
        while (ob_get_level() > $bufferLevel) {
            ob_end_clean();
        }

        throw $exception;
    }

    return ob_get_clean();
}

cakephp/View.php at 3.10.4 · cakephp/cakephp · GitHub

Hi @ndm,
thank you for clarifying what caused the misbehavior.
now it works as needed