5

Per this code example, it's clear that the API is expecting to return results based on an existing element...

'transformer' => function(EntryModel $entry) {
    ...
}

And when the criteria finds a matching entry, everything works great!

But when there is no matching criteria results, the API seems to simply return a generic 404 error.

How can I make the API return a message (in JSON format) instead of a 404?


EDIT:

The solution provided by @megatrond is helpful, but doesn't really nail the problem here. I'm actually using that workaround for a different problem I encountered... I needed to guarantee that the user is authorized to ping my endpoint. In implementing that solution, I created a unique access key, included it in the URL, and checked against it before potentially returning a JSON-formatted error message. Under those circumstances, I was able to run a useless query for the sake of allowing me to return JSON.

However, for the issue in this thread... The criteria will either return some results, or no results, and it's not possible to tell before allowing the query to run.

Consider this scenario...

Someone uses my endpoint to target the entry with id 42. Assuming entry #42 exists, the endpoint will successfully return data for it. But, if entry #42 has been deleted or disabled, the endpoint doesn't give me the opportunity to return an error message. Instead, it just kicks out a "404 Not Found" error. :(

Ideally, I'm looking for a fallback for when the criteria returns zero results.

5

Currently, Element API is hard coded to throw a 404 exception for single element endpoints, if the element criteria returns no results. There's no way to prevent this exception, add "fallback" criteria or to display an error message.

Note that the 404 exception will only be thrown if the first parameter is set to true, though. If it isn't, Element API will just return an empty dataset.

Obviously, omitting the first parameter is not a real solution to your issue. You still have no way to set a custom error message, and it would also mean that all single element endpoints would return arrays, as well as paging metadata etc, instead of a "flat" JSON object.

On that note, my opinion is that the canonical answer to this question should probably be to fork the Element API plugin and/or add a feature request.

However, if you don't want to go down that road, here's a possible workaround. It's not pretty :)

Basically, what you'll need to do is to catch the HttpException, in order to return a custom payload.

To do this, you'll need to write a custom plugin. Important: make sure that the string returned by your plugin's getName() method comes before "Element API", alphabetically. The reason for this is that you need to make sure that Craft loads your plugin before the Element API plugin (Craft loads plugins alphabetically, by name). I'll pretend you opt for the name "Awesome Plugin", which would work nicely.

Your plugin will need a controller, so create one called "AwesomeController" or the like. This controller should have a method called actionGetElements, taking the same arguments as the ElementApiController's actionGetElements method.

The only purpose of your controller is to "wrap" the ElementApiController, in order to catch the 404 HttpException fired by Element API, and echo a custom error message. Here's how that could look:

<?php
namespace Craft;

class AwesomeController extends BaseController
{

    protected $allowAnonymous = true;

    public function actionGetElements($configFactory = null, array $config = null)
    {
        try {

            // Fire up the Element API controller
            craft()->runController('elementApi/getElements');

        } catch (HttpException $e) {

            switch ($e->statusCode) {

                case 404 :

                    // It's a 404. Output a custom error message
                    JsonHelper::sendJsonHeaders();
                    echo JsonHelper::encode(array(
                        'error' => 'No element found',
                    ));
                    // End the request
                    craft()->end();

            }
        }
    }

}

Obviously, the controller will do nothing by itself – the final thing you need to do is to "hijack" the Element API routes; making them point to your controller instead of the ElementApiController. To achieve this, you'll need to create a method called registerSiteRoutes() in your plugin's primary class. Inside this method, you'll basically want to pull all the routes from the Element API plugin, changing their action value to awesomePlugin/getElements.

This is why it's important to make sure that your plugin is loaded by Craft before the Element API plugin: when an existing route is added to Craft, it'll be ignored. This means that for every endpoint in /config/elementapi.php, you can trick Craft into loading your custom controller instead of ElementApiController!:

Here's how that registerSiteRoutes() method could look:

public function registerSiteRoutes()
{

    $routes = [];

    // Get the Element API plugin
    $elementApiPlugin = craft()->plugins->getPlugin('elementApi');

    if (!$elementApiPlugin) {
        return $routes;
    }

    // Get the Element API routes
    $elementApiRoutes = $elementApiPlugin->registerSiteRoutes();

    // Set action property
    foreach ($elementApiRoutes as $endpoint => $config) {
        $config['action'] = 'awesomePlugin/getElements';
        $routes[$endpoint] = $config;
    }

    return $routes;

}

All in all – not exactly an elegant solution; but if the 404's are a dealbreaker to you, its at least a working workaround that doesn't require you to tamper with the Element API source, and the performance footprint should be minimal, too (no redundant database queries).

  • Great answer, thank you! It still seems like a lot of workaround code to solve a (theoretically) simple problem. I'm going to submit a formal feature request... I feel strongly that the plugin should natively handle the situation more gracefully. – Lindsey D Jan 14 '16 at 17:51
  • Per your suggestion, I've added a formal feature request... Please vote if you agree! Element API plugin: Better handling of NULL criteria results – Lindsey D Jan 14 '16 at 18:05
  • I completely agree and voted for your request :) – Mats Mikkel Rummelhoff Jan 14 '16 at 18:13
  • 2
    +1 for the ridiculous workaround, though. :) – Brad Bell Jan 14 '16 at 18:41
  • 1
    @BradBell Where there's a will, there's a ridiculous way :) – Mats Mikkel Rummelhoff Jan 14 '16 at 19:11
1

This is definately a sub-optimal workaround, but it might work:

Have your criteria in the api config fetch something you know is there, and there is only one of - your homepage or a global set (maybe a global set that's called errorMessages?)

Then do the normal

$criteria = craft()->elements->getCriteria(ElementType::Entry);
$criteria->section = 'sectionHandle';
// maybe some more criteria in here
$entries = $criteria->find();

And return your desired message if $entries is null

  • Thanks megatrond... This is somewhat helpful, though doesn't exactly resolve the problem. I edited my question to elaborate the use-case. I think your solution could potentially be used to achieve the desired effect... but as you said, it's a "sub-optimal workaround". At that rate, I'd wonder why I wasn't just using a JSON-formatted template in the first place? – Lindsey D Jan 13 '16 at 19:16
  • 2
    I agree with you, and so I forked the ElementApi and added a config param called handleNotFound, which whould be a function, and return whatever you'd like if an element is not found. It is only used when first is enabled. If no handler is provided, a 404 will be thrown. – megatrond Jan 14 '16 at 8:23
  • Very interesting... thanks for the fork link! – Lindsey D Jan 14 '16 at 8:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.