Custom routes return 404 error but correct content with HEAD request

I’m unable to debug this any further on my own now so I’m reaching out for help here now.
I want to provide a second version (slightly different copy) of a page on a different route of a page. I do that as advised via the configuration by setting a custom route.

However, when accessing these pages via curl with HEAD request (curl -i, curl -I) I get a 404 response despite it still returns the correct content of the page. This leads to big issues with external applications that fetch my content from Kirby.

Does anyone here have any idea what’s the issue and how to fix it?

c::set('routes', array(
array(
	'pattern' => 'archive/(:num)/copy',
	'action'  => function($no) {
		if ($issue = page('archive/' . $no)) {
			return new Response(
				snippet('copy', array('issue' => $issue), true)
			);
		} else {
			go('error');
		}
	}
)
));

Thanks a lot for any insight,
Anselm

Do you curl with the GET method? I believe the default router in kirby only works with GET.

Actually the external application accessed the page with POST, so yes that could be an issue. However I wonder what the issue really is since the content of the page is served but the header is wrong (404)?

You might take a look at setting up a custom router. This way you can define which method you’ld like to act upon.

1 Like

Thanks so much already, this seems to be the way to follow. However, I struggle to follow how I can use custom routers in Kirby. I found the developer guide but no mention where to put the code. I then found a forum post that mentions that it should go into a plugin. This seems to execute the code but it now shows me a 404 error for my page regardless of how I access it, so the custom router isn’t doing what it should.

Here’s the code:
Plugin:

<?php
$router = new Router();
$router->register( 'archive/(:num)/mail', array(
	'method' => 'GET|POST',
	'action' => function ( $no ) {
		if ( $issue = page( 'archive/' . $no ) ) {
			return new Response(
				snippet( 'copyMail', array( 'issue' => $issue ), true )
			);
		} else {
			// go( 'error' );
		}
	})
);

if( $route = $router->run() ) {
	$response = call( $route->action(), $route->arguments() );
}

Probably related to this issue: How to use a custom router?, with response::json() not working.

Maybe. It’s the topic where I got the information I have now—unfortunately, I couldn’t get it to work yet and feel like I’m still missing out on how to properly use the custom Router functionality. Is there any document that explains the implementation more in detail? The existing docs I found are only about how to configure the Router, not how to integrate it.

Unfortunately, no, there is no more detailed information that what is in the docs.

What is inside the “copymail” snippet?
Does it work without returning a snippet, e.g. returning a simple value?

Actually, it seems you’re right. It seems like I cannot use return new Response and/or the snippet() (containing full dynamic HTML of a page) in the custom router now. I can get it to work if instead of returning a new Response I use go(/) which is not what I want to achieve with the routing.

With the c::set( 'routes' routing returning a new Response was always possible, so I now would need a way to do that within the custom router. Unfortunately, I don’t understand much of the Toolkit API as I cannot find very much documentation on it.

Any help would be appreciated.

Basically I have come to the same conclusion as you in my question about returning json in a custom router.

It seems to me the “default routing” in kirby via c::set("routes") is not using the custom router logic internally (?) which lead to a lot of confusion for developers trying to use the custom router for advanced functionality (such as route filters e.g.): things that work perfectly in c::set("routes") suddenly don’t work anymore in custom routers and visa versa. :tired_face:

Yes, in my tests I had the same result, even returning a simple new Response() does not seem to work.

Is this a “bug”? Should I file a new issue to the kirby repo on github?

@lukasbestle promised to look into this tonight.

2 Likes

I can’t reproduce this at all, my example code is in the other topic.

Maybe what you are missing is actually printing the $response? Because that line is missing in all of your examples. But it is required as the router itself only does the routing, it does not do the response handling. It just returns the return value of the route.

I also can’t reproduce the “404 error when returning a response object from a c::set('routes') route” issue. Can you maybe post a reduced test case that I can reproduce directly in the starterkit?

Please note that the custom routers are only supposed to be used inside plugins. If you plan to use site-specific routes inside the site’s config.php, it is better to use the c::set('routes') option to avoid having to spin up two routers.

Actually, you’re true on that. Combining this with the hints from your code snippet in the other topic I now ended up with the following somehow working solution:

<?php

$router = new Router();
$router->register( array( array(
	'pattern' => 'archive/(:num)/mail',
	'method' => 'ALL',
	'action' => function( $no ) {
		if ( $issue = page( 'archive/' . $no ) ) {
			return snippet( 'copyMail', array( 'issue' => $issue ), true );
		} else {
			go( 'error' );
		}
	} ) )
);

if( $route = $router->run() ) {
	$response = call( $route->action(), $route->arguments() );
	die($response);
}

So many thanks already for figuring this out!

So you’re able to fetch a page content with a POST or HEAD request and get a status 200? I wasn’t able to do that. Try curl -I http://yourhost/ to test it. Note that curl -i etc might not work here. I doubt there’s a reduced test case for this but as I cannot specify a method in the c:set('routes') object parameter array, it’s relatively safe to assume that it doesn’t work by default.

I do but I wasn’t able to achieve the same result using just c:set('routes'). Anyway, the custom router isn’t used in production a lot. However, I’d love to have a more consistent solution, so if you have any idea on how to improve the situation, let me know!

One more thing to ask: In your code example you write die($response); to call it. I don’t like using die() here as it’ll exit the script there and therefore somehow missed the point of a plugin?

Of course you can!

c::set('routes', array(
	array(
		'pattern' => 'test',
		'method'  => 'POST',
		'action'  => function() {
			return new Response(snippet('some-snippet', true));
		}
	)
));

This works fine for me and visiting http://yoursite/test with a POST request returns HTTP 200.

PS: Where did you get it from that filters and methods are not supported for the c::set('routes') syntax? It uses a “custom router” internally (namely the one of the CMS) – so it supports the same options.

No, that’s required. Otherwise execution will continue outside of your plugin and Kirby will run the normal router. But that’s not what you want as it will mess up your output. You want your plugin to exit the program if the route matches (if it doesn’t, die() is never called!).

Actually, I have no idea anymore. I now tried it again and it seems to work as expected. However, before reaching out here I did so as well and it didn’t work well with POST and HEAD requests.

What did lead me into the custom router direction was the fact that I couldn’t get it to work and Custom routes return 404 error but correct content with HEAD request - #2 by bvdputte assuming that the default router does only work with GET. The resources I found about routing POST requests lead me to the Toolkit API and custom routers as well. I’m glad I was proven wrong now, it seems to work as expected with this code snippet in site/config/config.php:

c:set( 'routes', array(
	array(
		'pattern' => 'archive/(:num)/mail',
		'method' => 'ALL',
		'action' => function( $no ) {
			if ( $issue = page( 'archive/' . $no ) ) {
				return new Response(
					snippet( 'copyMail', array( 'issue' => $issue ), true )
				);
			}
		}
	),
) );

Good to know, it confused me too. How would you add a filter to a route in c::set("routes") then? thx!