Kohana v3

Kohana 3.1 Migration – Custom Error Pages

Kohana 3.1 has been released few days ago and it is time to upgrade all my Kohana based application / websites. To make it simple, I have upgraded first my portfolio site and so far, only the custom 404 page and page caching are affected.

UPDATE: support for base url other than “/”

Due to several issues about 404 pages using base url other than “/”, I have updated the post to reflect those enhancement/fix. It was due to the fact that Route::url() adds the base url whereas Route::get() doesn’t. For kohana setup using a non-root (“/”) base url, extra url segment will throw more errors on 404 pages.

Thanks to glt and the Kohana guys on the forum.

Here is the comment…

Custom Error Pages for Kohana 3.1

Since the documentation for Custom Error Pages for Kohana 3.1 is still incomplete or simply I don’t like, I have modified it and created something that fits my needs. I will separate them into components:

Note: The Kohana documentation may have been improved a lot but I haven’t checked it out lately.

  • .htaccess
  • bootstrap.php
  • Overridden Kohana_Exception class
  • Error controller
  • Error views

Setting up .htaccess

Mostly, the purpose of the .htaccess file is to create an apache rewrite rule and others such as caching static files, etc. While we can put these rules in the apache configuration, it is still convenient especially for share hosting to use the .htaccess file.

All we want to do is to setup the Kohana environment variable. Put something like this anywhere in the file:

# Set environment
SetEnv KOHANA_ENV "production"

It can be production, development, testing, or staging depending on whatever you server’s purpose is.

Setup bootstrap.php

So that we can trap errors, we need to tell Kohana to handle all errors as exception so that we can put our logic in the custom exception class. This is what your bootstrap.php should look like (other portions are not displayed):

Kohana::init(array(
	'base_url' 		=> '/',
	'index_file' 	=> FALSE,
	'errors'		=> TRUE,
	'profile'  		=> (Kohana::$environment == Kohana::DEVELOPMENT),
	'caching'    	=> (Kohana::$environment == Kohana::PRODUCTION)
));

Notice the errors key. It must be TRUE otherwise you will just get a blank page when an error/exception occurs. If you still get a blank page, maybe it is because your server disables displaying errors. To display errors, add this line on your index.php:

ini_set('display_errors', 'On');

# Error reporting may look like this but E_ALL is only what we need
error_reporting(E_ALL | E_STRICT);

Next is to create a router for errors. We can actually live without them but I’m just extending the suggested method written in the docs. The original router has only one extra parameter: message. In my version, I added origuri which is supposed to be the actual URI requested by the user. (Used for 404 page).

UPDATE: I have updated the router since the old version does not handle URI with dot “.” and other symbols.

/** 
 * Error router
 */
Route::set('error', 'error/<action>/<origuri>/<message>', array('action' => '[0-9]++', 'origuri' => '.+', 'message' => '.+'))
->defaults(array(
    'controller' => 'error',
	'action'	 => 'index'
));

Extended Kohana_Exception

Kohana already has an excellent exception handler Kohana_Kohana_Exception – the core file. The handler is set at Kohana::init() if errors is set to TRUE at init(). We will extend it thanks to the holy Cascading File System so that we can create custom error pages.

Create a file application/classes/kohana/exception.php with this content:

<?php defined('SYSPATH') or die('No direct script access.');

/** 
 * Custom exception handler for typical 404/500 error
 * 
 * @author Lysender
 *
 */
class Kohana_Exception extends Kohana_Kohana_Exception
{
	public static function handler(Exception $e)
	{
		// Throw errors when in development mode
		if (Kohana::$environment === Kohana::DEVELOPMENT)
		{
			parent::handler($e);
		}
		else
		{
			Kohana::$log->add(Log::ERROR, Kohana_Exception::text($e));
			
			$attributes = array(
				'action'	=> 500,
				'origuri'	=> rawurlencode(Arr::get($_SERVER, 'REQUEST_URI')),
				'message'	=> rawurlencode($e->getMessage())
			);
			
			if ($e instanceof Http_Exception)
			{
				$attributes['action'] = $e->getCode();
			}
			
			// Error sub request
			echo Request::factory(Route::get('error')->uri($attributes))
				->execute()
				->send_headers()
				->body();
		}
	}
}

What we do here is that if we are in DEVELOPMENT environment, all errors are displayed in the regular error message and stack trace. Otherwise, the exception is just logged and a custom error page is displayed via a sub request.

We retrieve the REQUEST_URI since I cannot effectively retrieve the requested URI using the request object. It adds the default action index which is I don’t like so the $_SERVER variable here is just a work around.

The Error controller

The controller that handles the error pages is just an orginary controller. In my case, it extends my template controller for my website so that it will have the same elements such as header and footer. However, we need to restrict users from viewing the page directly so that only HMVC or sub requests are allowed. It is funny that if we access the custom /error/404 page, the page will say it is not found. Nevermind.

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Error extends Controller_Site
{
	/** 
	 * @var string
	 */
	protected $_requested_page;
	
	/** 
	 * @var string
	 */
	protected $_message;
	
	/**
	 * Pre determine error display logic
	 */
	public function before()
	{
		parent::before();
		
		// Sub requests only!
		if ( ! $this->request->is_initial())
		{
			if ($message = rawurldecode($this->request->param('message')))
			{
				$this->_message = $message;
			}
			
			if ($requested_page = rawurldecode($this->request->param('origuri')))
			{
				$this->_requested_page = $requested_page;
			}
		}
		else
		{
			// This one was directly requested, don't allow
			$this->request->action(404);
			
			// Set the requested page accordingly
			$this->_requested_page = Arr::get($_SERVER, 'REQUEST_URI');
		}
		
		$this->response->status((int) $this->request->action());
	}
	
	/** 
	 * Serves HTTP 404 error page
	 */
	public function action_404()
	{
		$this->template->description = 'The requested page not found';
		$this->template->keywords = 'not found, 404';
		$this->template->title = 'Page not found';
		
		$this->view = View::factory('error/404')
			->set('error_message', $this->_message)
			->set('requested_page', $this->_requested_page);
	}

	/** 
	 * Serves HTTP 500 error page
	 */
	public function action_500()
	{
		$this->template->description = 'Internal server error occured';
		$this->template->keywords = 'server error, 500, internal error, error';
		$this->template->title = 'Internal server error occured';

		$this->view = View::factory('error/500');
	}
}

I have only two custom page, the 404 page and the 500 page. You can create your own such as 503 Service Temporarily Unvailable error page, etc. For the 404 page, the requested page is passed to the view so that it can be displayed in the view as a link. A link to a page that does not exists!

Error views

Our error views are located at application/views/error. These are the 404.php and 500.php files. These are the contents:

404.php

<h1><strong>404 Error:</strong> Page not found</h1>

<p>The requested page <?php echo HTML::anchor($requested_page, $requested_page) ?> is not found.</p>

<p>It is either not existing, moved or deleted. 
Make sure the URL is correct. </p>

<p>To go back to the previous page, click the Back button.</p>

<p><a href="<?php echo URL::site('/', true) ?>">If you wanted to go to the main page instead, click here.</a></p>

500.php

<h1><strong>500 Error:</strong> Internal server error</h1>

<p>Something went wrong while we are processing your request. You can try the following:</p>

<ul>
	<li>Reload / refresh the page.</li>
	<li>Go back to the previous page.</li>
</ul>

<p>This incident is logged and we are already notified about this problem.  
We will trace the cause of this problem.</p>

<p>For the mean time, you may want to go to the main page.</p>

<p><a href="<?php echo URL::site('/', true) ?>">If you wanted to go to the main page, click here.</a></p>

Live demo here: http://www.lysender.com/bad-url-is-bad.

50 thoughts on “Kohana 3.1 Migration – Custom Error Pages”

  1. Hello, thanks for the article. Everything worked great when I used application folder. Then i decided to move all exception-related files into a “modules/errors” folder and found out that it handles only 404 errors, not 500. For example, in case of typos or wrong php syntax I receive standart Konaha_Exception message, even in production O_o.

    Do you have any ideas about that? Any help will be appreciated. Thank you

  2. @Gabriel: Mine Request::initial() !== Request::current()

    Yours: ! Request::curent()->is_initial()

    That’s more elegant I think. Thanks for sharing, I haven’t really dig deeper with the 3.1 since I’m waiting for Sprig to be updated to it. Thanks for the visit.

  3. @Dmitrij – looks like I have a bug to fix and a need to update this post. Well, there was an update on the official docs on how to create a custom exception/error handler. I’ll try to follow that and update this post.

  4. @Dmitrij – I have fixed the problem. It was caused by my router which does not handle characters like “.”, here is my updated route (which is also reflected to this post):

    Route::set('error', 'error/(/(/))', array(
    'action' => '[0-9]++',
    'origuri' => '.+',
    'message' => '.+'
    ))
    ->defaults(array(
    'controller' => 'error',
    'action' => 'index'
    ));

  5. Lysender,

    Your code samples have helped me out quite a bit, but I am running into some issues.

    I tried both of your Routes, the one from March 11th and the original one from up above. But both throw me the following error:

    Fatal error: Unsupported operand types in C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\request.php on line 791

    Call Stack:
    3.0058 1039176 1. Kohana_Exception::handler(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\application\classes\kohana\exception.php:0
    56.6586 1095376 2. Kohana_Request->execute() C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\application\classes\kohana\exception.php:37
    56.6586 1095376 3. Kohana_Request_Client_External->execute(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\request.php:999
    56.6586 1095376 4. Kohana_Request->uri(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\request\client\external.php:80
    65.7893 1095656 5. Kohana_Core::shutdown_handler() C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\core.php:0
    65.7894 1098056 6. Kohana_Exception::handler(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\core.php:1033
    67.5778 1101696 7. Kohana_Request->execute() C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\application\classes\kohana\exception.php:37
    67.5778 1101696 8. Kohana_Request_Client_External->execute(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\request.php:999
    67.5779 1101696 9. Kohana_Request->uri(???) C:\Clients\Aquent\Bigfoot\BigfootTickets\www.bigfoottickets.com\system\classes\kohana\request\client\external.php:80

    It seems that it is attempting to add array() to null at that location and that’s causing all kinds of trouble. I have KO 3.1.1.1 installed right now.

    I am throwing
    throw new Http_Exception_404(‘Unable to find production #’ . $ProductionID);
    to get this error.

    Any suggestions?

  6. Sorry, I found my own problem. It is due to a bug in the KO 3.1.1.1 Request class mishandling external routes. It thought there was an external route because my base_url was set in the bootstrapper to include the full url, which I had done to make https links work. It isn’t really needed though.

    You can fix the Request object as explained here: http://dev.kohanaframework.org/issues/3775

    Then I had a problem in the exception class where it calls “Route::url(‘error’, $attributes);” that wasn’t working if your base_url is set to anything in the bootstrapper. If it is set to the name of your index file it builds the url like “/index.php/error/404/Some+file/Some+message” and then the route won’t match. Now I build the URL manually so it can find the internal route correctly.

  7. @Chris Chubb – Glad the you’ve found the problem. I have updated all my sites to KO 3.1.2 immediately (although cached pages are not yet cleared) because of that bug but honestly I don’t experience the blog in relation to custom 404.

    And oh, I haven’t tried setting the base url other than root or a sub directory and also that HTTPS stuff. Perhaps you can discuss more on that on the forum.

  8. @lysender,
    Hi, i’ve got the same problem as Dmitrij:
    Fatal error: Exception thrown without a stack frame in Unknown on line 0.

    I followed all your instructions, except template controller. I’ve got my own one. Could you help me, please?

  9. Problem is resolved πŸ™‚ You can remove my previous comment. I’m sorry for your attention.

  10. @lysender and @sergy,
    Hi, i’ve got the same problem as Dmitrij and Sergy:

    Fatal error: Exception thrown without a stack frame in Unknown on line 0.

    I followed all your instructions, except template controller. I’ve got my own one. I fail to solve this. My KO version is 3.1.2. Could you help me, please?

  11. @Wariston – It means that some part of your Exception handler is throwing an Exception. Perhaps a typo, or some tricky errors.

    What I do so solve that was to debug extended Kohana_Exception class, in the else part where I remove piece by piece some code until I found the bug. In your case I don’t know what it is.

    You can also wrap your Exception handler with try and catch block like this:

    
    class Kohana_Exception extends Kohana_Kohana_Exception
    {
      public static function handler(Exception $e)
      {
        // Throw errors when in development mode
        if (Kohana::$environment === Kohana::DEVELOPMENT)
        {
          parent::handler($e);
        }
        else
        {
          try
          {
            Kohana::$log->add(Log::ERROR, Kohana_Exception::text($e));
    
            $attributes = array(
              'action'	=> 500,
              'origuri'	=> rawurlencode(Arr::get($_SERVER, 'REQUEST_URI')),
              'message'	=> rawurlencode($e->getMessage())
            );
    
            if ($e instanceof Http_Exception)
            {
              $attributes['action'] = $e->getCode();
            }
    
            // Error sub request
            echo Request::factory(Route::url('error', $attributes))
              ->execute()
              ->send_headers()
              ->body();
          }
          catch (Exception $e)
          {
            var_dump($e->getMessage());
            var_dump($e->getTraceAsString());
          }
        }
      }
    }
    
  12. Hi

    I’m getting the same error as other guys.
    Fatal error: Exception thrown without a stack frame in Unknown on line 0

    Wrapping my Exception handler resulted in
    The requested view template could not be found

    I followed all your guidesa and I do have controller and views created. What else could be causing this problem? Any ideas?

  13. Hello,

    Something is wrong with this code.
    You try to load view error/404, and You named view 400.php – shouldn’t it be 404.php?

    The exception part:
    // Throw errors when in development mode
    if (Kohana::$environment === Kohana::DEVELOPMENT)
    {
    parent::handler($e);
    }
    In my case it always redirect me to parent handler. I’ve setup var in .htaccess. But
    Kohana::$environment value is set to 4.
    Kohana ver is 3.1.2.

  14. Hi P,

    Funny! Yes there was a typo. Got to fix it very soon. And yes, it must be 404.php.

  15. This method does not work if ‘base_url’ => ‘http://localhost/’. Why?

    Any workaround?

    Thank you!

  16. Hi there,

    Much thanks for this tutorial – it makes things clear!

    However i can’t get it working properly. I wrapped exception handler, and here is what i get for adress http://localhost/myweb.pl/www/secondweb.pl/bad-url.html:
    “Unable to find a route to match the URI: myweb.pl/www/secondweb.pl/error/404/…”

    My error route settings are as in tutorial, placed before default route settings.

    .htacces and bootstrap.php point to this base url: “/myweb.pl/www/secondweb.pl/”

    What might be the issue here?

    Thanks in advance
    glt

  17. Hi glt,

    I’m not really sure what happens. Based on your error, no route matches on your 404 page internal request. Post the full error message. Also, check if your error controller is correct.

  18. Hi,
    thanks for your response!

    Ok, so i’ve done some reasearch and here’s what i got:

    1. after calling error controller directly, for example: http://localhost/myweb.pl/www/secondweb.pl/error/404/bad-url.html – i get nice error page πŸ™‚

    2. when i type: http://localhost/myweb.pl/www/secondweb.pl/bad-url.html – i get this error, which i pasted not all before. So the full message goes like that:
    “Unable to find a route to match the URI: myweb.pl/www/secondweb.pl/error/404/%2Fmyweb.pl%2Fwww%2Fsecondweb.pl%2Fbad-url.html/Unable%20to%20find%20a%20route%20to%20match%20the%20URI%3A%20bad-url.html”

    So, now I see that base_url is put twice in URI! But how to fix this?

    Regards
    glt

  19. Hi glt,

    You may need to tweak your route but I’m not sure. I have only done experiments where ther base url is “/” and nothing more, that’s why my article may not work on other base urls.

    Can’t help that much as of now.

  20. Got it!

    Just in case someone has similar problem use Route::get('error')->uri($attributes) instead of Route::url('error', $attributes)

    Thanks for your time lysender!

    Regards
    glt

  21. Thank you for this, Lysender — for both the 3.0 and 3.1 versions (hope they don’t change it again for a while)

  22. @buckthorn,

    I think after six months or so they will but I hope that would not be a huge revamp like in 3.0 to 3.1.

  23. Problem:

    Route returns the path including any set set base directory.

    Request then treats the base directory as a controller name, which of course causes an error.

    So the Request::factory(Route::url(‘error’, $attributes)) structure is wrong.

    The route handed to Request has to be built by hand.

    – Henrik

  24. This works for me. Manually creating the route avoids the base directory issue, and placing other vars into a session (to get picked up on the other side) avoids url formatting issues.

    – Henrik


    $session = session::instance();
    $session->set('exception',$attributes);
    // $route = Route::url('error',$attributes); // problem with base
    $route = "/error/{$attributes['action']}";
    echo Request::factory($route)
    ->execute()
    ->send_headers()
    ->body();

  25. I finally twigged to the get(‘error’)->uri($attributes), so now I have (different environment, different details, but you get the gist):

    $session = session::instance();
    $session->set(‘exception’,$attributes);
    $routeargs = array(‘controller’=>’error’,’id’=>$attributes[‘action’]);
    $route = Route::get(‘special’)->uri($routeargs);
    echo Request::factory($route)
    ->execute()
    ->send_headers()
    ->body();
    – H

  26. Hi Henrik,

    I have not made any updates on my blog and yes, I have not taken consideration for non-root installation of kohana eg: application is under a sub-directory.

    There are suggestions on the comments though but I have not reviewed them nor tested but they say it works.

  27. Hi, thanks for this great tutorial.
    This help me to make my custom error pages in Kohana 3.1.

    Unfortunately, i currently doing an update in Kohana 3.2 and this doesn’t work.
    It seems the Request class has changed in this version.

    Will you make an update for this article ?

    Thanks ^^

  28. I got this working in Kohana 3.2 by add the following code to the error controller:


    public function __construct(Request $request, Response $response) {
    parent::__construct($request, $response);

    // Assign the request to the controller
    $this->request = $request;

    // Assign a response to the controller
    $this->response = $response;
    }

    And, in action_404 and action_500 I had to add:


    echo $this->view;

  29. Also, it is not recommended to display the url that the user incorrectly typed in. This allows session hijacking. Think of sql injection.. You can inject javascript code, etc.

  30. Thanks Chris Monahan for the advice. I will try to find ways of replication some session hijacking, sql injection or XSS but I think there is more threat in XSS here.

  31. Looks a LOT more comprehensive than the Kohana’s docs – Thank you for posting, I haven’t tried it yet, but I am sure it will work like as it should once I copy it over into my setup.

  32. Hi, I am using Kohana 3.2 and have had many problems trying to get this custom error functionality. Unfortunately, as I am currently working on the project locally, I have had to set my base_url to /project_name/. Now, when I go to localhost/projectname/error/ANYTHING I get the right error. However, when I go to localhost/projectname/ANYTHING – I get “Unable to find a route to match the URI”. Has anyone else experienced this problem too? There’s obviously an issue with the routes set in the bootstrap but I cannot quite work out what?

Leave a reply

Your email address will not be published. Required fields are marked *