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.
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.
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
Hi there,
I think your question has been answered already in one of the kohana forum thread and I also believe you are the one who posted that.
Thanks for visiting btw!
No, i didn’t. Thanks for your help π
to check for a sub-request “native” method could be used
! Request::curent()->is_initial()
@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.
Hi
link http://www.lysender.com/bad-url-is-bad.html
get error “Fatal error: Exception thrown without a stack frame in Unknown on line 0”
@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.
@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'
));
Π‘an make a working archive??
thank you
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?
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.
@Dimitrij – I have fixed the problem and this post was updated to reflect those changes. I’m sorry if I can’t provide you an archive for the code since my portfolio site’s code is not for public viewing.
But I have another pet project http://ff2fb.lysender.com/ which uses the same custom 404. You can view the source code on github: https://github.com/lysender/ff2fb
@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.
@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?
Problem is resolved π You can remove my previous comment. I’m sorry for your attention.
@sergy – glad that you’ve resolved it yourself.
@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?
@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:
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?
Ah I got it π
Your
before()
was colliding with mine in the controller.All good now π
@6bytes – That was a quick find π
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.
Hi P,
Funny! Yes there was a typo. Got to fix it very soon. And yes, it must be 404.php.
Sorry, I am still a beginner… couldn’t get this to work (route not found) until I found this…
I have found that when your $base_url is a directory, you have to remove it from the Route::url to have it work.
Is this worth adding to this excellent tutorial?
@John Rushworth
Will look into that perhaps this weekend.
This method does not work if ‘base_url’ => ‘http://localhost/’. Why?
Any workaround?
Thank you!
@Bogdan – Just use “/” as
base_url
.Ok, thank you!
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
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.
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
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.
Got it!
Just in case someone has similar problem use
Route::get('error')->uri($attributes)
instead ofRoute::url('error', $attributes)
Thanks for your time lysender!
Regards
glt
Thank you for this, Lysender — for both the 3.0 and 3.1 versions (hope they don’t change it again for a while)
@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.
Thanks, man, very useful )
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
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();
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
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.
Hi Kenrick,
I have updated the post. Thanks for dropping by.
Excellent article mate. Thanks much for this post, was very helpful!
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 ^^
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;
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.
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.
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.
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?