Skip to content
Advertisement

Capture Request state at the time of an exception

I have a Slim Framework application with a custom errorHandler, and a small middleware stack. My middleware adds attributes to the Request object which I would like to have access to from my error handler in the event of an exception. For example:

$app->get('/endpoint', function($request $response, $args) {
    $myAttribute = $request->getAttribute('myAttribute'); //returns 'myValue'
    throw new Exception(); //any code that throws an error
})->add(function($request, $response, $next) {
    $request = $request->withAttribute('myAttribute', 'myValue');
    $response = $next($request, $response);
    return $response;
});

$app->getContainer['errorHandler'] = function($c) {
    return function($request, $response, $exception) {
        $myAttribute = $request->getAttribute('myAttribute'); //returns null
        return $response;
    }
};

The attribute does not exist within the Request object inside the error handler because the cloned Request from inside the route has not been returned after traversing through the middleware stack. Is it possible to access the Request and Response objects as they exist, at the time (in the location) the exception is thrown? I can’t explicitly pass them (for example, SlimException) because I’m trying to handle unexpected errors as well.

Advertisement

Answer

I’ve created two, somewhat hacky, solutions to capturing Request and Response state when exceptions are thrown. Both attempt to insert a try/catch as close to the center of the middleware stack as possible (without repeating code), and involve wrapping the original exception in a new exception class together with the modified endpoint parameters.

Middleware

This works as long as attention is paid to the order that middleware is added. Unfortunately it does require the innermost middleware is added to each route, or at the very least, “before” the middleware modifying the Request and/or Response objects. This won’t catch any changes to Request/Response made inside, nor after the route.

class WrappedException extends Exception {
    public $exception;
    public $request;
    public $response;

    public function __construct($exception, $request, $response) {
        $this->exception = $exception;
        $this->request = $request;
        $this->response = $response;
    }
}

$app->get('/endpoint', function($request $response, $args) {
    throw new Exception(); //any code that throws an error
})->add(function($request, $response, $next) {
    try {
        $response = $next($request, $response);
    } catch (Exception $exception) {
        throw new WrappedException($exception, $request, $response);
    }
    return $response;
});

$app->add(function($request, $response, $next) {
    $request = $request->withAttribute('myAttribute', 'myValue');
    $response = $next($request, $response);
    return $response;
});

$app->getContainer['errorHandler'] = function($c) {
    return function($request, $response, $exception) {
        if ($exception instanceof WrappedException) {
            //returns 'myValue'
            $myAttribute = $exception->request->getAttribute('myAttribute');
        }
        return $response;
    }
};

Route Class

This wraps all routes in a try block, and makes for a bit cleaner route code, but you must be sure to extend all routes from the RouteBase class.

class WrappedException extends Exception {
    public $exception;
    public $request;
    public $response;

    public function __construct($exception, $request = null, $response = null) {
        $this->exception = $exception;
        $this->request = $request;
        $this->response = $response;
    }
}

class RouteBase {
    public function __call($method, $arguments) {
        if (method_exists($this, $method)) {
            try {
                $this::$method(...$arguments);
            } catch (Exception $e) {
                throw new WrappedException($e, ...$arguments);
            }
        } else {
            throw new Exception('Route method not found.');
        }
    }
}

class RouteClass extends RouteBase {
    //PROTECTED -- must be callable by parent class, but not outside class
    protected function get(Request $request, Response $response, $args) {
        throw new Exception('A new exception!');
        $response->write('hey');
        return $response;
    }
}

$app->get('/endpoint', 'RouteClass:get');

$app->add(function($request, $response, $next) {
    $request = $request->withAttribute('myAttribute', 'myValue');
    $response = $next($request, $response);
    return $response;
});

$app->getContainer['errorHandler'] = function($c) {
    return function($request, $response, $exception) {
        if ($exception instanceof WrappedException) {
            //returns 'myValue'
            $myAttribute = $exception->request->getAttribute('myAttribute');
        }
        return $response;
    }
};
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement