diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index 7c5c571b64..5eeba52221 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,663 +1,681 @@
 <?php
 
 /**
  * @task routing URI Routing
  * @task response Response Handling
  * @task exception Exception Handling
  */
 abstract class AphrontApplicationConfiguration extends Phobject {
 
   private $request;
   private $host;
   private $path;
   private $console;
 
   abstract public function buildRequest();
   abstract public function build404Controller();
   abstract public function buildRedirectController($uri, $external);
 
   final public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   final public function getRequest() {
     return $this->request;
   }
 
   final public function getConsole() {
     return $this->console;
   }
 
   final public function setConsole($console) {
     $this->console = $console;
     return $this;
   }
 
   final public function setHost($host) {
     $this->host = $host;
     return $this;
   }
 
   final public function getHost() {
     return $this->host;
   }
 
   final public function setPath($path) {
     $this->path = $path;
     return $this;
   }
 
   final public function getPath() {
     return $this->path;
   }
 
   public function willBuildRequest() {}
 
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public static function runHTTPRequest(AphrontHTTPSink $sink) {
     PhabricatorStartup::beginStartupPhase('multimeter');
     $multimeter = MultimeterControl::newInstance();
     $multimeter->setEventContext('<http-init>');
     $multimeter->setEventViewer('<none>');
 
     // Build a no-op write guard for the setup phase. We'll replace this with a
     // real write guard later on, but we need to survive setup and build a
     // request object first.
     $write_guard = new AphrontWriteGuard('id');
 
     PhabricatorStartup::beginStartupPhase('preflight');
 
     $response = PhabricatorSetupCheck::willPreflightRequest();
     if ($response) {
-      PhabricatorStartup::endOutputCapture();
-      $sink->writeResponse($response);
-      return;
+      return self::writeResponse($sink, $response);
     }
 
     PhabricatorStartup::beginStartupPhase('env.init');
-    PhabricatorEnv::initializeWebEnvironment();
+
+    try {
+      PhabricatorEnv::initializeWebEnvironment();
+      $database_exception = null;
+    } catch (AphrontInvalidCredentialsQueryException $ex) {
+      $database_exception = $ex;
+    } catch (AphrontConnectionQueryException $ex) {
+      $database_exception = $ex;
+    }
+
+    if ($database_exception) {
+      $issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
+        $database_exception);
+      $response = PhabricatorSetupCheck::newIssueResponse($issue);
+      return self::writeResponse($sink, $response);
+    }
 
     $multimeter->setSampleRate(
       PhabricatorEnv::getEnvConfig('debug.sample-rate'));
 
     $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.
     PhabricatorStartup::beginStartupPhase('log.access');
     PhabricatorAccessLog::init();
     $access_log = PhabricatorAccessLog::getLog();
     PhabricatorStartup::setAccessLog($access_log);
     $access_log->setData(
       array(
         'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
         'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
         'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
       ));
 
     DarkConsoleXHProfPluginAPI::hookProfiler();
 
     // We just activated the profiler, so we don't need to keep track of
     // startup phases anymore: it can take over from here.
     PhabricatorStartup::beginStartupPhase('startup.done');
 
     DarkConsoleErrorLogPluginAPI::registerErrorHandler();
 
     $response = PhabricatorSetupCheck::willProcessRequest();
     if ($response) {
-      PhabricatorStartup::endOutputCapture();
-      $sink->writeResponse($response);
-      return;
+      return self::writeResponse($sink, $response);
     }
 
     $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();
 
     // Now that we have a request, convert the write guard into one which
     // actually checks CSRF tokens.
     $write_guard->dispose();
     $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
 
     // 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(),
       ));
 
     $processing_exception = null;
     try {
       $response = $application->processRequest(
         $request,
         $access_log,
         $sink,
         $multimeter);
       $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(),
       ));
 
     $multimeter->newEvent(
       MultimeterEvent::TYPE_REQUEST_TIME,
       $multimeter->getEventContext(),
       PhabricatorStartup::getMicrosecondsSinceStart());
 
     $access_log->write();
 
     $multimeter->saveEvents();
 
     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,
     MultimeterControl $multimeter) {
 
     $this->setRequest($request);
 
     list($controller, $uri_data) = $this->buildController();
 
     $controller_class = get_class($controller);
     $access_log->setData(
       array(
         'C' => $controller_class,
       ));
     $multimeter->setEventContext('web.'.$controller_class);
 
     $request->setController($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(),
           ));
         $multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
       }
 
       if (!$response) {
         $controller->willProcessRequest($uri_data);
         $response = $controller->handleRequest($request);
         $this->validateControllerResponse($controller, $response);
       }
     } catch (Exception $ex) {
       $original_exception = $ex;
       $response = $this->handleException($ex);
     }
 
     try {
       $response = $this->produceResponse($request, $response);
       $response = $controller->willSendResponse($response);
       $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);
+      self::writeResponse($sink, $response);
     } catch (Exception $ex) {
       if ($original_exception) {
         throw $original_exception;
       }
       throw $ex;
     }
 
     return $response;
   }
 
+  private static function writeResponse(
+    AphrontHTTPSink $sink,
+    AphrontResponse $response) {
+
+    $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);
+  }
+
 
 /* -(  URI Routing  )-------------------------------------------------------- */
 
 
   /**
    * Build a controller to respond to the request.
    *
    * @return pair<AphrontController,dict> Controller and dictionary of request
    *                                      parameters.
    * @task routing
    */
   final private function buildController() {
     $request = $this->getRequest();
 
     // If we're configured to operate in cluster mode, reject requests which
     // were not received on a cluster interface.
     //
     // For example, a host may have an internal address like "170.0.0.1", and
     // also have a public address like "51.23.95.16". Assuming the cluster
     // is configured on a range like "170.0.0.0/16", we want to reject the
     // requests received on the public interface.
     //
     // Ideally, nodes in a cluster should only be listening on internal
     // interfaces, but they may be configured in such a way that they also
     // listen on external interfaces, since this is easy to forget about or
     // get wrong. As a broad security measure, reject requests received on any
     // interfaces which aren't on the whitelist.
 
     $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
     if ($cluster_addresses) {
       $server_addr = idx($_SERVER, 'SERVER_ADDR');
       if (!$server_addr) {
         if (php_sapi_name() == 'cli') {
           // This is a command line script (probably something like a unit
           // test) so it's fine that we don't have SERVER_ADDR defined.
         } else {
           throw new AphrontMalformedRequestException(
             pht('No %s', 'SERVER_ADDR'),
             pht(
               'Phabricator is configured to operate in cluster mode, but '.
               '%s is not defined in the request context. Your webserver '.
               'configuration needs to forward %s to PHP so Phabricator can '.
               'reject requests received on external interfaces.',
               'SERVER_ADDR',
               'SERVER_ADDR'));
         }
       } else {
         if (!PhabricatorEnv::isClusterAddress($server_addr)) {
           throw new AphrontMalformedRequestException(
             pht('External Interface'),
             pht(
               'Phabricator is configured in cluster mode and the address '.
               'this request was received on ("%s") is not whitelisted as '.
               'a cluster address.',
               $server_addr));
         }
       }
     }
 
     $site = $this->buildSiteForRequest($request);
 
     if ($site->shouldRequireHTTPS()) {
       if (!$request->isHTTPS()) {
 
         // Don't redirect intracluster requests: doing so drops headers and
         // parameters, imposes a performance penalty, and indicates a
         // misconfiguration.
         if ($request->isProxiedClusterRequest()) {
           throw new AphrontMalformedRequestException(
             pht('HTTPS Required'),
             pht(
               'This request reached a site which requires HTTPS, but the '.
               'request is not marked as HTTPS.'));
         }
 
         $https_uri = $request->getRequestURI();
         $https_uri->setDomain($request->getHost());
         $https_uri->setProtocol('https');
 
         // In this scenario, we'll be redirecting to HTTPS using an absolute
         // URI, so we need to permit an external redirect.
         return $this->buildRedirectController($https_uri, true);
       }
     }
 
     $maps = $site->getRoutingMaps();
     $path = $request->getPath();
 
     $result = $this->routePath($maps, $path);
     if ($result) {
       return $result;
     }
 
     // If we failed to match anything but don't have a trailing slash, try
     // to add a trailing slash and issue a redirect if that resolves.
 
     // NOTE: We only do this for GET, since redirects switch to GET and drop
     // data like POST parameters.
     if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
       $result = $this->routePath($maps, $path.'/');
       if ($result) {
         $slash_uri = $request->getRequestURI()->setPath($path.'/');
 
         // We need to restore URI encoding because the webserver has
         // interpreted it. For example, this allows us to redirect a path
         // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
         // resolved meaningfully by an application.
         $slash_uri = phutil_escape_uri($slash_uri);
 
         $external = strlen($request->getRequestURI()->getDomain());
         return $this->buildRedirectController($slash_uri, $external);
       }
     }
 
     return $this->build404Controller();
   }
 
   /**
    * Map a specific path to the corresponding controller. For a description
    * of routing, see @{method:buildController}.
    *
    * @param list<AphrontRoutingMap> List of routing maps.
    * @param string Path to route.
    * @return pair<AphrontController,dict> Controller and dictionary of request
    *                                      parameters.
    * @task routing
    */
   private function routePath(array $maps, $path) {
     foreach ($maps as $map) {
       $result = $map->routePath($path);
       if ($result) {
         return array($result->getController(), $result->getURIData());
       }
     }
   }
 
   private function buildSiteForRequest(AphrontRequest $request) {
     $sites = PhabricatorSite::getAllSites();
 
     $site = null;
     foreach ($sites as $candidate) {
       $site = $candidate->newSiteForRequest($request);
       if ($site) {
         break;
       }
     }
 
     if (!$site) {
       $path = $request->getPath();
       $host = $request->getHost();
       throw new AphrontMalformedRequestException(
         pht('Site Not Found'),
         pht(
           'This request asked for "%s" on host "%s", but no site is '.
           'configured which can serve this request.',
           $path,
           $host),
         true);
     }
 
     $request->setSite($site);
 
     return $site;
   }
 
 
 /* -(  Response Handling  )-------------------------------------------------- */
 
 
   /**
    * Tests if a response is of a valid type.
    *
    * @param wild Supposedly valid response.
    * @return bool True if the object is of a valid type.
    * @task response
    */
   private function isValidResponseObject($response) {
     if ($response instanceof AphrontResponse) {
       return true;
     }
 
     if ($response instanceof AphrontResponseProducerInterface) {
       return true;
     }
 
     return false;
   }
 
 
   /**
    * Verifies that the return value from an @{class:AphrontController} is
    * of an allowed type.
    *
    * @param AphrontController Controller which returned the response.
    * @param wild Supposedly valid response.
    * @return void
    * @task response
    */
   private function validateControllerResponse(
     AphrontController $controller,
     $response) {
 
     if ($this->isValidResponseObject($response)) {
       return;
     }
 
     throw new Exception(
       pht(
         'Controller "%s" returned an invalid response from call to "%s". '.
         'This method must return an object of class "%s", or an object '.
         'which implements the "%s" interface.',
         get_class($controller),
         'handleRequest()',
         'AphrontResponse',
         'AphrontResponseProducerInterface'));
   }
 
 
   /**
    * Verifies that the return value from an
    * @{class:AphrontResponseProducerInterface} is of an allowed type.
    *
    * @param AphrontResponseProducerInterface Object which produced
    *   this response.
    * @param wild Supposedly valid response.
    * @return void
    * @task response
    */
   private function validateProducerResponse(
     AphrontResponseProducerInterface $producer,
     $response) {
 
     if ($this->isValidResponseObject($response)) {
       return;
     }
 
     throw new Exception(
       pht(
         'Producer "%s" returned an invalid response from call to "%s". '.
         'This method must return an object of class "%s", or an object '.
         'which implements the "%s" interface.',
         get_class($producer),
         'produceAphrontResponse()',
         'AphrontResponse',
         'AphrontResponseProducerInterface'));
   }
 
 
   /**
    * Verifies that the return value from an
    * @{class:AphrontRequestExceptionHandler} is of an allowed type.
    *
    * @param AphrontRequestExceptionHandler Object which produced this
    *  response.
    * @param wild Supposedly valid response.
    * @return void
    * @task response
    */
   private function validateErrorHandlerResponse(
     AphrontRequestExceptionHandler $handler,
     $response) {
 
     if ($this->isValidResponseObject($response)) {
       return;
     }
 
     throw new Exception(
       pht(
         'Exception handler "%s" returned an invalid response from call to '.
         '"%s". This method must return an object of class "%s", or an object '.
         'which implements the "%s" interface.',
         get_class($handler),
         'handleRequestException()',
         'AphrontResponse',
         'AphrontResponseProducerInterface'));
   }
 
 
   /**
    * Resolves a response object into an @{class:AphrontResponse}.
    *
    * Controllers are permitted to return actual responses of class
    * @{class:AphrontResponse}, or other objects which implement
    * @{interface:AphrontResponseProducerInterface} and can produce a response.
    *
    * If a controller returns a response producer, invoke it now and produce
    * the real response.
    *
    * @param AphrontRequest Request being handled.
    * @param AphrontResponse|AphrontResponseProducerInterface Response, or
    *   response producer.
    * @return AphrontResponse Response after any required production.
    * @task response
    */
   private function produceResponse(AphrontRequest $request, $response) {
     $original = $response;
 
     // Detect cycles on the exact same objects. It's still possible to produce
     // infinite responses as long as they're all unique, but we can only
     // reasonably detect cycles, not guarantee that response production halts.
 
     $seen = array();
     while (true) {
       // NOTE: It is permissible for an object to be both a response and a
       // response producer. If so, being a producer is "stronger". This is
       // used by AphrontProxyResponse.
 
       // If this response is a valid response, hand over the request first.
       if ($response instanceof AphrontResponse) {
         $response->setRequest($request);
       }
 
       // If this isn't a producer, we're all done.
       if (!($response instanceof AphrontResponseProducerInterface)) {
         break;
       }
 
       $hash = spl_object_hash($response);
       if (isset($seen[$hash])) {
         throw new Exception(
           pht(
             'Failure while producing response for object of class "%s": '.
             'encountered production cycle (identical object, of class "%s", '.
             'was produced twice).',
             get_class($original),
             get_class($response)));
       }
 
       $seen[$hash] = true;
 
       $new_response = $response->produceAphrontResponse();
       $this->validateProducerResponse($response, $new_response);
       $response = $new_response;
     }
 
     return $response;
   }
 
 
 /* -(  Error Handling  )----------------------------------------------------- */
 
 
   /**
    * Convert an exception which has escaped the controller into a response.
    *
    * This method delegates exception handling to available subclasses of
    * @{class:AphrontRequestExceptionHandler}.
    *
    * @param Exception Exception which needs to be handled.
    * @return wild Response or response producer, or null if no available
    *   handler can produce a response.
    * @task exception
    */
   private function handleException(Exception $ex) {
     $handlers = AphrontRequestExceptionHandler::getAllHandlers();
 
     $request = $this->getRequest();
     foreach ($handlers as $handler) {
       if ($handler->canHandleRequestException($request, $ex)) {
         $response = $handler->handleRequestException($request, $ex);
         $this->validateErrorHandlerResponse($handler, $response);
         return $response;
       }
     }
 
     throw $ex;
   }
 
 
 }
diff --git a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
index b99cd37adc..fd318a5fa1 100644
--- a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
+++ b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
@@ -1,134 +1,130 @@
 <?php
 
 final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
 
   public function getDefaultGroup() {
     return self::GROUP_IMPORTANT;
   }
 
   public function getExecutionOrder() {
     // This must run after basic PHP checks, but before most other checks.
     return 500;
   }
 
   protected function executeChecks() {
     $master = PhabricatorDatabaseRef::getMasterDatabaseRef();
     if (!$master) {
       // If we're implicitly in read-only mode during disaster recovery,
       // don't bother with these setup checks.
       return;
     }
 
     $conn_raw = $master->newManagementConnection();
 
     try {
       queryfx($conn_raw, 'SELECT 1');
+      $database_exception = null;
+    } catch (AphrontInvalidCredentialsQueryException $ex) {
+      $database_exception = $ex;
     } catch (AphrontConnectionQueryException $ex) {
-      $message = pht(
-        "Unable to connect to MySQL!\n\n".
-        "%s\n\n".
-        "Make sure Phabricator and MySQL are correctly configured.",
-        $ex->getMessage());
+      $database_exception = $ex;
+    }
 
-      $this->newIssue('mysql.connect')
-        ->setName(pht('Can Not Connect to MySQL'))
-        ->setMessage($message)
-        ->setIsFatal(true)
-        ->addRelatedPhabricatorConfig('mysql.host')
-        ->addRelatedPhabricatorConfig('mysql.port')
-        ->addRelatedPhabricatorConfig('mysql.user')
-        ->addRelatedPhabricatorConfig('mysql.pass');
+    if ($database_exception) {
+      $issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
+        $database_exception);
+      $this->addIssue($issue);
       return;
     }
 
     $engines = queryfx_all($conn_raw, 'SHOW ENGINES');
     $engines = ipull($engines, 'Support', 'Engine');
 
     $innodb = idx($engines, 'InnoDB');
     if ($innodb != 'YES' && $innodb != 'DEFAULT') {
       $message = pht(
         "The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ".
         "your MySQL configuration.".
         "\n\n".
         "(If you aleady created tables, MySQL incorrectly used some other ".
         "engine to create them. You need to convert them or drop and ".
         "reinitialize them.)");
 
       $this->newIssue('mysql.innodb')
         ->setName(pht('MySQL InnoDB Engine Not Available'))
         ->setMessage($message)
         ->setIsFatal(true);
       return;
     }
 
     $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
 
     $databases = queryfx_all($conn_raw, 'SHOW DATABASES');
     $databases = ipull($databases, 'Database', 'Database');
 
     if (empty($databases[$namespace.'_meta_data'])) {
       $message = pht(
         "Run the storage upgrade script to setup Phabricator's database ".
         "schema.");
 
       $this->newIssue('storage.upgrade')
         ->setName(pht('Setup MySQL Schema'))
         ->setMessage($message)
         ->setIsFatal(true)
         ->addCommand(hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
     } else {
       $conn_meta = $master->newApplicationConnection(
         $namespace.'_meta_data');
 
       $applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status');
       $applied = ipull($applied, 'patch', 'patch');
 
       $all = PhabricatorSQLPatchList::buildAllPatches();
       $diff = array_diff_key($all, $applied);
 
       if ($diff) {
         $this->newIssue('storage.patch')
           ->setName(pht('Upgrade MySQL Schema'))
           ->setMessage(
             pht(
               "Run the storage upgrade script to upgrade Phabricator's ".
               "database schema. Missing patches:<br />%s<br />",
               phutil_implode_html(phutil_tag('br'), array_keys($diff))))
           ->addCommand(
             hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
       }
     }
 
     $host = PhabricatorEnv::getEnvConfig('mysql.host');
     $matches = null;
     if (preg_match('/^([^:]+):(\d+)$/', $host, $matches)) {
       $host = $matches[1];
       $port = $matches[2];
 
       $this->newIssue('storage.mysql.hostport')
         ->setName(pht('Deprecated mysql.host Format'))
         ->setSummary(
           pht(
             'Move port information from `%s` to `%s` in your config.',
             'mysql.host',
             'mysql.port'))
         ->setMessage(
           pht(
             'Your `%s` configuration contains a port number, but this usage '.
             'is deprecated. Instead, put the port number in `%s`.',
             'mysql.host',
             'mysql.port'))
         ->addPhabricatorConfig('mysql.host')
         ->addPhabricatorConfig('mysql.port')
         ->addCommand(
           hsprintf(
             '<tt>phabricator/ $</tt> ./bin/config set mysql.host %s',
             $host))
         ->addCommand(
           hsprintf(
             '<tt>phabricator/ $</tt> ./bin/config set mysql.port %s',
             $port));
     }
 
   }
 }
diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php
index b1f79bdd47..ad779ed63f 100644
--- a/src/applications/config/check/PhabricatorSetupCheck.php
+++ b/src/applications/config/check/PhabricatorSetupCheck.php
@@ -1,256 +1,256 @@
 <?php
 
 abstract class PhabricatorSetupCheck extends Phobject {
 
   private $issues;
 
   abstract protected function executeChecks();
 
   const GROUP_OTHER       = 'other';
   const GROUP_MYSQL       = 'mysql';
   const GROUP_PHP         = 'php';
   const GROUP_IMPORTANT   = 'important';
 
   public function getExecutionOrder() {
     if ($this->isPreflightCheck()) {
       return 0;
     } else {
       return 1000;
     }
   }
 
   /**
    * Should this check execute before we load configuration?
    *
    * The majority of checks (particularly, those checks which examine
    * configuration) should run in the normal setup phase, after configuration
    * loads. However, a small set of critical checks (mostly, tests for PHP
    * setup and extensions) need to run before we can load configuration.
    *
    * @return bool True to execute before configuration is loaded.
    */
   public function isPreflightCheck() {
     return false;
   }
 
   final protected function newIssue($key) {
     $issue = id(new PhabricatorSetupIssue())
       ->setIssueKey($key);
     $this->issues[$key] = $issue;
 
     if ($this->getDefaultGroup()) {
       $issue->setGroup($this->getDefaultGroup());
     }
 
     return $issue;
   }
 
   final public function getIssues() {
     return $this->issues;
   }
 
   protected function addIssue(PhabricatorSetupIssue $issue) {
     $this->issues[$issue->getIssueKey()] = $issue;
     return $this;
   }
 
   public function getDefaultGroup() {
     return null;
   }
 
   final public function runSetupChecks() {
     $this->issues = array();
     $this->executeChecks();
   }
 
   final public static function getOpenSetupIssueKeys() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.issue-keys');
   }
 
   final public static function setOpenSetupIssueKeys(
     array $keys,
     $update_database) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.issue-keys', $keys);
 
     if ($update_database) {
       $db_cache = new PhabricatorKeyValueDatabaseCache();
       try {
         $json = phutil_json_encode($keys);
         $db_cache->setKey('phabricator.setup.issue-keys', $json);
       } catch (Exception $ex) {
         // Ignore any write failures, since they likely just indicate that we
         // have a database-related setup issue that needs to be resolved.
       }
     }
   }
 
   final public static function getOpenSetupIssueKeysFromDatabase() {
     $db_cache = new PhabricatorKeyValueDatabaseCache();
     try {
       $value = $db_cache->getKey('phabricator.setup.issue-keys');
       if (!strlen($value)) {
         return null;
       }
       return phutil_json_decode($value);
     } catch (Exception $ex) {
       return null;
     }
   }
 
   final public static function getUnignoredIssueKeys(array $all_issues) {
     assert_instances_of($all_issues, 'PhabricatorSetupIssue');
     $keys = array();
     foreach ($all_issues as $issue) {
       if (!$issue->getIsIgnored()) {
         $keys[] = $issue->getIssueKey();
       }
     }
     return $keys;
   }
 
   final public static function getConfigNeedsRepair() {
     $cache = PhabricatorCaches::getSetupCache();
     return $cache->getKey('phabricator.setup.needs-repair');
   }
 
   final public static function setConfigNeedsRepair($needs_repair) {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->setKey('phabricator.setup.needs-repair', $needs_repair);
   }
 
   final public static function deleteSetupCheckCache() {
     $cache = PhabricatorCaches::getSetupCache();
     $cache->deleteKeys(
       array(
         'phabricator.setup.needs-repair',
         'phabricator.setup.issue-keys',
       ));
   }
 
   final public static function willPreflightRequest() {
     $checks = self::loadAllChecks();
 
     foreach ($checks as $check) {
       if (!$check->isPreflightCheck()) {
         continue;
       }
 
       $check->runSetupChecks();
 
       foreach ($check->getIssues() as $key => $issue) {
         return self::newIssueResponse($issue);
       }
     }
 
     return null;
   }
 
-  private static function newIssueResponse(PhabricatorSetupIssue $issue) {
+  public static function newIssueResponse(PhabricatorSetupIssue $issue) {
     $view = id(new PhabricatorSetupIssueView())
       ->setIssue($issue);
 
     return id(new PhabricatorConfigResponse())
       ->setView($view);
   }
 
   final public static function willProcessRequest() {
     $issue_keys = self::getOpenSetupIssueKeys();
     if ($issue_keys === null) {
       $issues = self::runNormalChecks();
       foreach ($issues as $issue) {
         if ($issue->getIsFatal()) {
           return self::newIssueResponse($issue);
         }
       }
       $issue_keys = self::getUnignoredIssueKeys($issues);
       self::setOpenSetupIssueKeys($issue_keys, $update_database = true);
     } else if ($issue_keys) {
       // If Phabricator is configured in a cluster with multiple web devices,
       // we can end up with setup issues cached on every device. This can cause
       // a warning banner to show on every device so that each one needs to
       // be dismissed individually, which is pretty annoying. See T10876.
 
       // To avoid this, check if the issues we found have already been cleared
       // in the database. If they have, we'll just wipe out our own cache and
       // move on.
       $issue_keys = self::getOpenSetupIssueKeysFromDatabase();
       if ($issue_keys !== null) {
         self::setOpenSetupIssueKeys($issue_keys, $update_database = false);
       }
     }
 
     // Try to repair configuration unless we have a clean bill of health on it.
     // We need to keep doing this on every page load until all the problems
     // are fixed, which is why it's separate from setup checks (which run
     // once per restart).
     $needs_repair = self::getConfigNeedsRepair();
     if ($needs_repair !== false) {
       $needs_repair = self::repairConfig();
       self::setConfigNeedsRepair($needs_repair);
     }
   }
 
   final public static function loadAllChecks() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setSortMethod('getExecutionOrder')
       ->execute();
   }
 
   final public static function runNormalChecks() {
     $checks = self::loadAllChecks();
 
     foreach ($checks as $key => $check) {
       if ($check->isPreflightCheck()) {
         unset($checks[$key]);
       }
     }
 
     $issues = array();
     foreach ($checks as $check) {
       $check->runSetupChecks();
       foreach ($check->getIssues() as $key => $issue) {
         if (isset($issues[$key])) {
           throw new Exception(
             pht(
               "Two setup checks raised an issue with key '%s'!",
               $key));
         }
         $issues[$key] = $issue;
         if ($issue->getIsFatal()) {
           break 2;
         }
       }
     }
 
     $ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
     foreach ($ignore_issues as $ignorable => $derp) {
       if (isset($issues[$ignorable])) {
         $issues[$ignorable]->setIsIgnored(true);
       }
     }
 
     return $issues;
   }
 
   final public static function repairConfig() {
     $needs_repair = false;
 
     $options = PhabricatorApplicationConfigOptions::loadAllOptions();
     foreach ($options as $option) {
       try {
         $option->getGroup()->validateOption(
           $option,
           PhabricatorEnv::getEnvConfig($option->getKey()));
       } catch (PhabricatorConfigValidationException $ex) {
         PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
         $needs_repair = true;
       }
     }
 
     return $needs_repair;
   }
 
 }
diff --git a/src/applications/config/issue/PhabricatorSetupIssue.php b/src/applications/config/issue/PhabricatorSetupIssue.php
index 1c2ff0f99b..53bab14a85 100644
--- a/src/applications/config/issue/PhabricatorSetupIssue.php
+++ b/src/applications/config/issue/PhabricatorSetupIssue.php
@@ -1,193 +1,213 @@
 <?php
 
 final class PhabricatorSetupIssue extends Phobject {
 
   private $issueKey;
   private $name;
   private $message;
   private $isFatal;
   private $summary;
   private $shortName;
   private $group;
 
   private $isIgnored = false;
   private $phpExtensions = array();
   private $phabricatorConfig = array();
   private $relatedPhabricatorConfig = array();
   private $phpConfig = array();
   private $commands = array();
   private $mysqlConfig = array();
   private $originalPHPConfigValues = array();
   private $links;
 
+  public static function newDatabaseConnectionIssue(
+    AphrontQueryException $ex) {
+
+    $message = pht(
+      "Unable to connect to MySQL!\n\n".
+      "%s\n\n".
+      "Make sure Phabricator and MySQL are correctly configured.",
+      $ex->getMessage());
+
+    return id(new self())
+      ->setIssueKey('mysql.connect')
+      ->setName(pht('Can Not Connect to MySQL'))
+      ->setMessage($message)
+      ->setIsFatal(true)
+      ->addRelatedPhabricatorConfig('mysql.host')
+      ->addRelatedPhabricatorConfig('mysql.port')
+      ->addRelatedPhabricatorConfig('mysql.user')
+      ->addRelatedPhabricatorConfig('mysql.pass');
+  }
+
   public function addCommand($command) {
     $this->commands[] = $command;
     return $this;
   }
 
   public function getCommands() {
     return $this->commands;
   }
 
   public function setShortName($short_name) {
     $this->shortName = $short_name;
     return $this;
   }
 
   public function getShortName() {
     if ($this->shortName === null) {
       return $this->getName();
     }
     return $this->shortName;
   }
 
   public function setGroup($group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     if ($this->group) {
       return $this->group;
     } else {
       return PhabricatorSetupCheck::GROUP_OTHER;
     }
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     if ($this->summary === null) {
       return $this->getMessage();
     }
     return $this->summary;
   }
 
   public function setIssueKey($issue_key) {
     $this->issueKey = $issue_key;
     return $this;
   }
 
   public function getIssueKey() {
     return $this->issueKey;
   }
 
   public function setIsFatal($is_fatal) {
     $this->isFatal = $is_fatal;
     return $this;
   }
 
   public function getIsFatal() {
     return $this->isFatal;
   }
 
   public function addPHPConfig($php_config) {
     $this->phpConfig[] = $php_config;
     return $this;
   }
 
   /**
    * Set an explicit value to display when showing the user PHP configuration
    * values.
    *
    * If Phabricator has changed a value by the time a config issue is raised,
    * you can provide the original value here so the UI makes sense. For example,
    * we alter `memory_limit` during startup, so if the original value is not
    * provided it will look like it is always set to `-1`.
    *
    * @param string PHP configuration option to provide a value for.
    * @param string Explicit value to show in the UI.
    * @return this
    */
   public function addPHPConfigOriginalValue($php_config, $value) {
     $this->originalPHPConfigValues[$php_config] = $value;
     return $this;
   }
 
   public function getPHPConfigOriginalValue($php_config, $default = null) {
     return idx($this->originalPHPConfigValues, $php_config, $default);
   }
 
   public function getPHPConfig() {
     return $this->phpConfig;
   }
 
   public function addMySQLConfig($mysql_config) {
     $this->mysqlConfig[] = $mysql_config;
     return $this;
   }
 
   public function getMySQLConfig() {
     return $this->mysqlConfig;
   }
 
   public function addPhabricatorConfig($phabricator_config) {
     $this->phabricatorConfig[] = $phabricator_config;
     return $this;
   }
 
   public function getPhabricatorConfig() {
     return $this->phabricatorConfig;
   }
 
   public function addRelatedPhabricatorConfig($phabricator_config) {
     $this->relatedPhabricatorConfig[] = $phabricator_config;
     return $this;
   }
 
   public function getRelatedPhabricatorConfig() {
     return $this->relatedPhabricatorConfig;
   }
 
   public function addPHPExtension($php_extension) {
     $this->phpExtensions[] = $php_extension;
     return $this;
   }
 
   public function getPHPExtensions() {
     return $this->phpExtensions;
   }
 
   public function setMessage($message) {
     $this->message = $message;
     return $this;
   }
 
   public function getMessage() {
     return $this->message;
   }
 
   public function setIsIgnored($is_ignored) {
     $this->isIgnored = $is_ignored;
     return $this;
   }
 
   public function getIsIgnored() {
     return $this->isIgnored;
   }
 
   public function addLink($href, $name) {
     $this->links[] = array(
       'href' => $href,
       'name' => $name,
     );
     return $this;
   }
 
   public function getLinks() {
     return $this->links;
   }
 
 }