1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 08:12:40 +01:00

Improve top-level exception handling

Summary:
Fixes T6692. Addresses two main issues:

  - The write guard would sometimes not get disposed of on exception pathways, generating an unnecessary secondary error which was just a symptom of the original root error.
    - This was generally confusing and reduced the quality of reports we received because users would report the symptomatic error sometimes instead of the real error.
    - Instead, reflow the handling so that we always dispose of the write guard if we create one.
  - If we missed the Controller-level error page generation (normally, a nice page with full CSS, etc), we'd jump straight to Startup-level error page generation (very basic plain text).
    - A large class of errors occur too early or too late to be handled by Controller-level pages, but many of these errors are not fundamental, and the plain text page is excessively severe.
    - Provide a mid-level simple HTML error page for errors which can't get full CSS, but also aren't so fundamental that we have no recourse but plain text.

Test Plan:
Mid-level errors now produce an intentional-looking error page:

{F259885}

Verified that setup errors still render properly.

@chad, feel free to tweak the exception page -- I just did a rough pass on it. Like the setup error stuff, it doesn't have Celerity, so we can't use `{$colors}` and no other CSS will be loaded.

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley, chad

Maniphest Tasks: T6692

Differential Revision: https://secure.phabricator.com/D11126
This commit is contained in:
epriestley 2015-01-02 10:49:27 -08:00
parent 2660b944be
commit 08126d3904
10 changed files with 411 additions and 214 deletions

View file

@ -45,6 +45,7 @@ return array(
'rsrc/css/application/config/config-template.css' => '25d446d6', 'rsrc/css/application/config/config-template.css' => '25d446d6',
'rsrc/css/application/config/config-welcome.css' => 'b0d16200', 'rsrc/css/application/config/config-welcome.css' => 'b0d16200',
'rsrc/css/application/config/setup-issue.css' => '8f852bc0', 'rsrc/css/application/config/setup-issue.css' => '8f852bc0',
'rsrc/css/application/config/unhandled-exception.css' => '38f08073',
'rsrc/css/application/conpherence/menu.css' => 'e1e0fdf1', 'rsrc/css/application/conpherence/menu.css' => 'e1e0fdf1',
'rsrc/css/application/conpherence/message-pane.css' => '042886d1', 'rsrc/css/application/conpherence/message-pane.css' => '042886d1',
'rsrc/css/application/conpherence/notification.css' => '04a6e10a', 'rsrc/css/application/conpherence/notification.css' => '04a6e10a',
@ -816,6 +817,7 @@ return array(
'sprite-tokens-css' => '1706b943', 'sprite-tokens-css' => '1706b943',
'syntax-highlighting-css' => '56c1ba38', 'syntax-highlighting-css' => '56c1ba38',
'tokens-css' => '3d0f239e', 'tokens-css' => '3d0f239e',
'unhandled-exception-css' => '38f08073',
), ),
'requires' => array( 'requires' => array(
'00861799' => array( '00861799' => array(

View file

@ -161,12 +161,14 @@ phutil_register_library_map(array(
'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php',
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTableView' => 'view/control/AphrontTableView.php',
'AphrontTagView' => 'view/AphrontTagView.php', 'AphrontTagView' => 'view/AphrontTagView.php',
'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php', 'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php',
'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php', 'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
'AphrontURIMapper' => 'aphront/AphrontURIMapper.php', 'AphrontURIMapper' => 'aphront/AphrontURIMapper.php',
'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php', 'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php',
'AphrontView' => 'view/AphrontView.php', 'AphrontView' => 'view/AphrontView.php',
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', 'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
@ -3217,11 +3219,13 @@ phutil_register_library_map(array(
'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontRequestTestCase' => 'PhabricatorTestCase',
'AphrontSideNavFilterView' => 'AphrontView', 'AphrontSideNavFilterView' => 'AphrontView',
'AphrontStackTraceView' => 'AphrontView', 'AphrontStackTraceView' => 'AphrontView',
'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
'AphrontTableView' => 'AphrontView', 'AphrontTableView' => 'AphrontView',
'AphrontTagView' => 'AphrontView', 'AphrontTagView' => 'AphrontView',
'AphrontTokenizerTemplateView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView',
'AphrontTwoColumnView' => 'AphrontView', 'AphrontTwoColumnView' => 'AphrontView',
'AphrontTypeaheadTemplateView' => 'AphrontView', 'AphrontTypeaheadTemplateView' => 'AphrontView',
'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
'AphrontUsageException' => 'AphrontException', 'AphrontUsageException' => 'AphrontException',
'AphrontView' => array( 'AphrontView' => array(
'Phobject', 'Phobject',
@ -4669,7 +4673,7 @@ phutil_register_library_map(array(
'PhabricatorMarkupInterface', 'PhabricatorMarkupInterface',
), ),
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource', 'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigResponse' => 'AphrontHTMLResponse', 'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject', 'PhabricatorConfigSchemaQuery' => 'Phobject',
'PhabricatorConfigSchemaSpec' => 'Phobject', 'PhabricatorConfigSchemaSpec' => 'Phobject',
'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',

View file

@ -15,18 +15,20 @@ final class PhabricatorCelerityTestCase extends PhabricatorTestCase {
$new_map = id(new CelerityResourceMapGenerator($resources)) $new_map = id(new CelerityResourceMapGenerator($resources))
->generate(); ->generate();
$this->assertEqual( // Don't actually compare these values with assertEqual(), since the diff
$new_map->getNameMap(), // isn't helpful and is often enormously huge.
$old_map->getNameMap());
$this->assertEqual( $maps_are_identical =
$new_map->getSymbolMap(), ($new_map->getNameMap() === $old_map->getNameMap()) &&
$old_map->getSymbolMap()); ($new_map->getSymbolMap() === $old_map->getSymbolMap()) &&
$this->assertEqual( ($new_map->getRequiresMap() === $old_map->getRequiresMap()) &&
$new_map->getRequiresMap(), ($new_map->getPackageMap() === $old_map->getPackageMap());
$old_map->getRequiresMap());
$this->assertEqual( $this->assertTrue(
$new_map->getPackageMap(), $maps_are_identical,
$old_map->getPackageMap()); pht(
'When this test fails, it means the Celerity resource map is out '.
'of date. Run `bin/celerity map` to rebuild it.'));
} }
} }

View file

@ -54,6 +54,192 @@ abstract class AphrontApplicationConfiguration {
public function willBuildRequest() {} public function willBuildRequest() {}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
PhabricatorEnv::initializeWebEnvironment();
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setGlobal('log.access', $access_log);
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
PhabricatorStartup::endOutputCapture();
$sink->writeResponse($response);
return;
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
$processing_exception = null;
try {
$response = $application->processRequest($request, $access_log, $sink);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$access_log->write();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
// Add points to the rate limits for this request.
if (isset($_SERVER['REMOTE_ADDR'])) {
$user_ip = $_SERVER['REMOTE_ADDR'];
// The base score for a request allows users to make 30 requests per
// minute.
$score = (1000 / 30);
// If the user was logged in, let them make more requests.
if ($request->getUser() && $request->getUser()->getPHID()) {
$score = $score / 5;
}
PhabricatorStartup::addRateLimitScore($user_ip, $score);
}
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$access_log->setData(
array(
'C' => get_class($controller),
));
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
}
} catch (Exception $ex) {
$original_exception = $ex;
$response = $this->handleException($ex);
}
try {
$response = $controller->didProcessRequest($response);
$response = $this->willSendResponse($response, $controller);
$response->setRequest($request);
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
echo phutil_tag(
'div',
array('style' =>
'background: #eeddff;'.
'white-space: pre-wrap;'.
'z-index: 200000;'.
'position: relative;'.
'padding: 8px;'.
'font-family: monospace',
),
$unexpected_output);
}
}
$sink->writeResponse($response);
} catch (Exception $ex) {
if ($original_exception) {
throw $original_exception;
}
throw $ex;
}
return $response;
}
/* -( URI Routing )-------------------------------------------------------- */ /* -( URI Routing )-------------------------------------------------------- */

View file

@ -0,0 +1,63 @@
<?php
abstract class AphrontStandaloneHTMLResponse
extends AphrontHTMLResponse {
abstract protected function getResources();
abstract protected function getResponseTitle();
abstract protected function getResponseBodyClass();
abstract protected function getResponseBody();
abstract protected function buildPlainTextResponseString();
final public function buildResponseString() {
// Check to make sure we aren't requesting this via Ajax or Conduit.
if (isset($_REQUEST['__ajax__']) || isset($_REQUEST['__conduit__'])) {
return (string)hsprintf('%s', $this->buildPlainTextResponseString());
}
$title = $this->getResponseTitle();
$resources = $this->buildResources();
$body_class = $this->getResponseBodyClass();
$body = $this->getResponseBody();
return (string)hsprintf(
<<<EOTEMPLATE
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>%s</title>
%s
</head>
%s
</html>
EOTEMPLATE
,
$title,
$resources,
phutil_tag(
'body',
array(
'class' => $body_class,
),
$body));
}
private function buildResources() {
$paths = $this->getResources();
$webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/';
$resources = array();
foreach ($paths as $path) {
$resources[] = phutil_tag(
'style',
array('type' => 'text/css'),
phutil_safe_html(Filesystem::readFile($webroot.'/rsrc/'.$path)));
}
return phutil_implode_html("\n", $resources);
}
}

View file

@ -0,0 +1,74 @@
<?php
final class AphrontUnhandledExceptionResponse
extends AphrontStandaloneHTMLResponse {
private $exception;
public function setException(Exception $exception) {
$this->exception = $exception;
return $this;
}
public function getHTTPResponseCode() {
return 500;
}
protected function getResources() {
return array(
'css/application/config/config-template.css',
'css/application/config/unhandled-exception.css',
);
}
protected function getResponseTitle() {
return pht('Unhandled Exception');
}
protected function getResponseBodyClass() {
return 'unhandled-exception';
}
protected function getResponseBody() {
$ex = $this->exception;
if ($ex instanceof AphrontUsageException) {
$title = $ex->getTitle();
} else {
$title = get_class($ex);
}
$body = $ex->getMessage();
$body = phutil_escape_html_newlines($body);
return phutil_tag(
'div',
array(
'class' => 'unhandled-exception-detail',
),
array(
phutil_tag(
'h1',
array(
'class' => 'unhandled-exception-title',
),
$title),
phutil_tag(
'div',
array(
'class' => 'unhandled-exception-body',
),
$body),
));
}
protected function buildPlainTextResponseString() {
$ex = $this->exception;
return pht(
'%s: %s',
get_class($ex),
$ex->getMessage());
}
}

View file

@ -1,6 +1,6 @@
<?php <?php
final class PhabricatorConfigResponse extends AphrontHTMLResponse { final class PhabricatorConfigResponse extends AphrontStandaloneHTMLResponse {
private $view; private $view;
@ -9,51 +9,33 @@ final class PhabricatorConfigResponse extends AphrontHTMLResponse {
return $this; return $this;
} }
public function buildResponseString() { public function getHTTPResponseCode() {
// Check to make sure we aren't requesting this via ajax or conduit return 500;
if (isset($_REQUEST['__ajax__']) || isset($_REQUEST['__conduit__'])) {
// We don't want to flood the console with html, just return a simple
// message for now.
return pht(
'This install has a fatal setup error, access the internet web '.
'version to view details and resolve it.');
}
$resources = $this->buildResources();
$view = $this->view->render();
return hsprintf(
'<!DOCTYPE html>'.
'<html>'.
'<head>'.
'<meta charset="UTF-8" />'.
'<title>Phabricator Setup</title>'.
'%s'.
'</head>'.
'<body class="setup-fatal">%s</body>'.
'</html>',
$resources,
$view);
} }
private function buildResources() { protected function getResources() {
$css = array( return array(
'application/config/config-template.css', 'css/application/config/config-template.css',
'application/config/setup-issue.css', 'css/application/config/setup-issue.css',
); );
$webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/';
$resources = array();
foreach ($css as $path) {
$resources[] = phutil_tag(
'style',
array('type' => 'text/css'),
phutil_safe_html(Filesystem::readFile($webroot.'/rsrc/css/'.$path)));
}
return phutil_implode_html("\n", $resources);
} }
protected function getResponseTitle() {
return pht('Phabricator Setup Error');
}
protected function getResponseBodyClass() {
return 'setup-fatal';
}
protected function getResponseBody() {
return $this->view->render();
}
protected function buildPlainTextResponseString() {
return pht(
'This install has a fatal setup error, access the internet web '.
'version to view details and resolve it.');
}
} }

View file

@ -122,6 +122,10 @@ final class PhabricatorStartup {
self::setupPHP(); self::setupPHP();
self::verifyPHP(); self::verifyPHP();
// If we've made it this far, the environment isn't completely broken so
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
if (isset($_SERVER['REMOTE_ADDR'])) { if (isset($_SERVER['REMOTE_ADDR'])) {
self::rateLimitRequest($_SERVER['REMOTE_ADDR']); self::rateLimitRequest($_SERVER['REMOTE_ADDR']);
} }

View file

@ -11,174 +11,27 @@ if (file_exists($preamble_path)) {
PhabricatorStartup::didStartup(); PhabricatorStartup::didStartup();
$show_unexpected_traces = false;
try { try {
PhabricatorStartup::loadCoreLibraries(); PhabricatorStartup::loadCoreLibraries();
PhabricatorEnv::initializeWebEnvironment();
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
$show_unexpected_traces = PhabricatorEnv::getEnvConfig(
'phabricator.developer-mode');
// This is the earliest we can get away with this, we need env config first.
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setGlobal('log.access', $access_log);
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$sink = new AphrontPHPHTTPSink(); $sink = new AphrontPHPHTTPSink();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
PhabricatorStartup::endOutputCapture();
$sink->writeResponse($response);
return;
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
// Until an administrator sets "phabricator.base-uri", assume it is the same
// as the request URI. This will work fine in most cases, it just breaks down
// when daemons need to do things.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
$application->setRequest($request);
list($controller, $uri_data) = $application->buildController();
$request->setURIMap($uri_data);
$controller->setRequest($request);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
'C' => get_class($controller),
));
// If execution throws an exception and then trying to render that exception
// throws another exception, we want to show the original exception, as it is
// likely the root cause of the rendering exception.
$original_exception = null;
try { try {
$response = $controller->willBeginExecution(); AphrontApplicationConfiguration::runHTTPRequest($sink);
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
}
} catch (Exception $ex) { } catch (Exception $ex) {
$original_exception = $ex; try {
$response = $application->handleException($ex); $response = new AphrontUnhandledExceptionResponse();
} $response->setException($ex);
try { PhabricatorStartup::endOutputCapture();
$response = $controller->didProcessRequest($response); $sink->writeResponse($response);
$response = $application->willSendResponse($response, $controller); } catch (Exception $response_exception) {
$response->setRequest($request); // If we hit a rendering exception, ignore it and throw the original
// exception. It is generally more interesting and more likely to be
$unexpected_output = PhabricatorStartup::endOutputCapture(); // the root cause.
if ($unexpected_output) { throw $ex;
$unexpected_output = "Unexpected output:\n\n{$unexpected_output}";
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
echo phutil_tag(
'div',
array('style' =>
'background: #eeddff;'.
'white-space: pre-wrap;'.
'z-index: 200000;'.
'position: relative;'.
'padding: 8px;'.
'font-family: monospace',
),
$unexpected_output);
}
} }
$sink->writeResponse($response);
} catch (Exception $ex) {
$write_guard->dispose();
$access_log->write();
if ($original_exception) {
$ex = new PhutilAggregateException(
'Multiple exceptions during processing and rendering.',
array(
$original_exception,
$ex,
));
}
PhabricatorStartup::didEncounterFatalException(
'Rendering Exception',
$ex,
$show_unexpected_traces);
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response->getHTTPResponseCode(),
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
// Add points to the rate limits for this request.
if (isset($_SERVER['REMOTE_ADDR'])) {
$user_ip = $_SERVER['REMOTE_ADDR'];
// The base score for a request allows users to make 30 requests per
// minute.
$score = (1000 / 30);
// If the user was logged in, let them make more requests.
if ($request->getUser() && $request->getUser()->getPHID()) {
$score = $score / 5;
}
PhabricatorStartup::addRateLimitScore($user_ip, $score);
} }
} catch (Exception $ex) { } catch (Exception $ex) {
PhabricatorStartup::didEncounterFatalException( PhabricatorStartup::didEncounterFatalException('Core Exception', $ex, false);
'Core Exception',
$ex,
$show_unexpected_traces);
} }

View file

@ -0,0 +1,27 @@
/**
* @provides unhandled-exception-css
*/
.unhandled-exception {
background: #222228;
}
.unhandled-exception-detail {
max-width: 760px;
margin: 16px auto;
background: #f7f7f7;
border: 2px solid #ffffff;
}
.unhandled-exception-detail .unhandled-exception-title {
font-size: 15px;
font-weight: bold;
margin: 0;
padding: 16px;
background: #DFE0E2;
}
.unhandled-exception-detail .unhandled-exception-body {
padding: 16px;
color: #4B4D51;
}