diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php
index a644c45a9..8e031a977 100644
--- a/src/aphront/response/AphrontProxyResponse.php
+++ b/src/aphront/response/AphrontProxyResponse.php
@@ -1,67 +1,74 @@
 <?php
 
 /**
  * Base class for responses which augment other types of responses. For example,
  * a response might be substantially an Ajax response, but add structure to the
  * response content. It can do this by extending @{class:AphrontProxyResponse},
  * instantiating an @{class:AphrontAjaxResponse} in @{method:buildProxy}, and
- * then using the proxy to construct the response string in
- * @{method:buildResponseString}.
+ * then constructing a real @{class:AphrontAjaxResponse} in
+ * @{method:reduceProxyResponse}.
  *
  * @group aphront
  */
 abstract class AphrontProxyResponse extends AphrontResponse {
 
   private $proxy;
 
   protected function getProxy() {
     if (!$this->proxy) {
       $this->proxy = $this->buildProxy();
     }
     return $this->proxy;
   }
 
   public function setRequest($request) {
     $this->getProxy()->setRequest($request);
     return $this;
   }
 
   public function getRequest() {
     return $this->getProxy()->getRequest();
   }
 
   public function getHeaders() {
     return $this->getProxy()->getHeaders();
   }
 
   public function setCacheDurationInSeconds($duration) {
     $this->getProxy()->setCacheDurationInSeconds($duration);
     return $this;
   }
 
   public function setLastModified($epoch_timestamp) {
     $this->getProxy()->setLastModified($epoch_timestamp);
     return $this;
   }
 
   public function setHTTPResponseCode($code) {
     $this->getProxy()->setHTTPResponseCode($code);
     return $this;
   }
 
   public function getHTTPResponseCode() {
     return $this->getProxy()->getHTTPResponseCode();
   }
 
   public function setFrameable($frameable) {
     $this->getProxy()->setFrameable($frameable);
     return $this;
   }
 
   public function getCacheHeaders() {
     return $this->getProxy()->getCacheHeaders();
   }
 
   abstract protected function buildProxy();
+  abstract public function reduceProxyResponse();
+
+  final public function buildResponseString() {
+    throw new Exception(
+      "AphrontProxyResponse must implement reduceProxyResponse().");
+  }
+
 
 }
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index 39ca6fdfd..297ea6502 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,293 +1,309 @@
 <?php
 
 abstract class PhabricatorController extends AphrontController {
 
   private $handles;
 
   public function shouldRequireLogin() {
 
     // If this install is configured to allow public resources and the
     // controller works in public mode, allow the request through.
     $is_public_allowed = PhabricatorEnv::getEnvConfig('policy.allow-public');
     if ($is_public_allowed && $this->shouldAllowPublic()) {
       return false;
     }
 
     return true;
   }
 
   public function shouldRequireAdmin() {
     return false;
   }
 
   public function shouldRequireEnabledUser() {
     return true;
   }
 
   public function shouldAllowPublic() {
     return false;
   }
 
   public function shouldRequireEmailVerification() {
     $need_verify = PhabricatorUserEmail::isEmailVerificationRequired();
     $need_login = $this->shouldRequireLogin();
 
     return ($need_login && $need_verify);
   }
 
   final public function willBeginExecution() {
 
     $request = $this->getRequest();
 
     $user = new PhabricatorUser();
 
     $phusr = $request->getCookie('phusr');
     $phsid = $request->getCookie('phsid');
 
     if (strlen($phusr) && $phsid) {
       $info = queryfx_one(
         $user->establishConnection('r'),
         'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID
           AND s.type LIKE %> AND s.sessionKey = %s',
         $user->getTableName(),
         'phabricator_session',
         'web-',
         $phsid);
       if ($info) {
         $user->loadFromArray($info);
       }
     }
 
     $translation = $user->getTranslation();
     if ($translation &&
         $translation != PhabricatorEnv::getEnvConfig('translation.provider')) {
       $translation = newv($translation, array());
       PhutilTranslator::getInstance()
         ->setLanguage($translation->getLanguage())
         ->addTranslations($translation->getTranslations());
     }
 
     $request->setUser($user);
 
     if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) {
       $disabled_user_controller = new PhabricatorDisabledUserController(
         $request);
       return $this->delegateToController($disabled_user_controller);
     }
 
     $event = new PhabricatorEvent(
       PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST,
       array(
         'request' => $request,
         'controller' => get_class($this),
       ));
     $event->setUser($user);
     PhutilEventEngine::dispatchEvent($event);
     $checker_controller = $event->getValue('controller');
     if ($checker_controller != get_class($this)) {
       return $this->delegateToController($checker_controller);
     }
 
     if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
       if ($user->getConsoleEnabled() ||
           PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
         $console = new DarkConsoleCore();
         $request->getApplicationConfiguration()->setConsole($console);
       }
     }
 
     if ($this->shouldRequireLogin() && !$user->getPHID()) {
       $login_controller = new PhabricatorLoginController($request);
       return $this->delegateToController($login_controller);
     }
 
     if ($this->shouldRequireEmailVerification()) {
       $email = $user->loadPrimaryEmail();
       if (!$email) {
         throw new Exception(
           "No primary email address associated with this account!");
       }
       if (!$email->getIsVerified()) {
         $verify_controller = new PhabricatorMustVerifyEmailController($request);
         return $this->delegateToController($verify_controller);
       }
     }
 
     if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
       return new Aphront403Response();
     }
 
   }
 
   public function buildStandardPageView() {
     $view = new PhabricatorStandardPageView();
     $view->setRequest($this->getRequest());
     $view->setController($this);
     return $view;
   }
 
   public function buildStandardPageResponse($view, array $data) {
     $page = $this->buildStandardPageView();
     $page->appendChild($view);
     $response = new AphrontWebpageResponse();
     $response->setContent($page->render());
     return $response;
   }
 
   public function getApplicationURI($path = '') {
     if (!$this->getCurrentApplication()) {
       throw new Exception("No application!");
     }
     return $this->getCurrentApplication()->getBaseURI().ltrim($path, '/');
   }
 
   public function buildApplicationPage($view, array $options) {
     $page = $this->buildStandardPageView();
 
     $application = $this->getCurrentApplication();
     if ($application) {
       $page->setApplicationName($application->getName());
       $page->setTitle(idx($options, 'title'));
       if ($application->getTitleGlyph()) {
         $page->setGlyph($application->getTitleGlyph());
       }
     }
 
     if (!($view instanceof AphrontSideNavFilterView)) {
       $nav = new AphrontSideNavFilterView();
       $nav->appendChild($view);
       $view = $nav;
     }
 
     $view->setUser($this->getRequest()->getUser());
     $view->setFlexNav(true);
 
     $page->appendChild($view);
 
     if (idx($options, 'device')) {
       $page->setDeviceReady(true);
       $view->appendChild($page->renderFooter());
     }
 
     $application_menu = $this->buildApplicationMenu();
     if ($application_menu) {
       $page->setApplicationMenu($application_menu);
     }
 
     $response = new AphrontWebpageResponse();
     return $response->setContent($page->render());
   }
 
   public function didProcessRequest($response) {
     $request = $this->getRequest();
     $response->setRequest($request);
+
+    $seen = array();
+    while ($response instanceof AphrontProxyResponse) {
+
+      $hash = spl_object_hash($response);
+      if (isset($seen[$hash])) {
+        $seen[] = get_class($response);
+        throw new Exception(
+          "Cycle while reducing proxy responses: ".
+          implode(' -> ', $seen));
+      }
+      $seen[$hash] = get_class($response);
+
+      $response = $response->reduceProxyResponse();
+    }
+
     if ($response instanceof AphrontDialogResponse) {
       if (!$request->isAjax()) {
         $view = new PhabricatorStandardPageView();
         $view->setRequest($request);
         $view->setController($this);
         $view->appendChild(
           '<div style="padding: 2em 0;">'.
             $response->buildResponseString().
           '</div>');
         $response = new AphrontWebpageResponse();
         $response->setContent($view->render());
         return $response;
       } else {
         return id(new AphrontAjaxResponse())
           ->setContent(array(
             'dialog' => $response->buildResponseString(),
           ));
       }
     } else if ($response instanceof AphrontRedirectResponse) {
       if ($request->isAjax()) {
         return id(new AphrontAjaxResponse())
           ->setContent(
             array(
               'redirect' => $response->getURI(),
             ));
       }
     }
     return $response;
   }
 
   protected function getHandle($phid) {
     if (empty($this->handles[$phid])) {
       throw new Exception(
         "Attempting to access handle which wasn't loaded: {$phid}");
     }
     return $this->handles[$phid];
   }
 
   protected function loadHandles(array $phids) {
     $phids = array_filter($phids);
     $this->handles = $this->loadViewerHandles($phids);
     return $this;
   }
 
   protected function getLoadedHandles() {
     return $this->handles;
   }
 
   protected function loadViewerHandles(array $phids) {
     return id(new PhabricatorObjectHandleData($phids))
       ->setViewer($this->getRequest()->getUser())
       ->loadHandles();
   }
 
 
   /**
    * Render a list of links to handles, identified by PHIDs. The handles must
    * already be loaded.
    *
    * @param   list<phid>  List of PHIDs to render links to.
    * @param   string      Style, one of "\n" (to put each item on its own line)
    *                      or "," (to list items inline, separated by commas).
    * @return  string      Rendered list of handle links.
    */
   protected function renderHandlesForPHIDs(array $phids, $style = "\n") {
     $style_map = array(
       "\n"  => '<br />',
       ','   => ', ',
     );
 
     if (empty($style_map[$style])) {
       throw new Exception("Unknown handle list style '{$style}'!");
     }
 
     $items = array();
     foreach ($phids as $phid) {
       $items[] = $this->getHandle($phid)->renderLink();
     }
     return implode($style_map[$style], $items);
   }
 
   protected function buildApplicationMenu() {
     return null;
   }
 
   protected function buildApplicationCrumbs() {
 
     $crumbs = array();
 
     $application = $this->getCurrentApplication();
     if ($application) {
       $sprite = $application->getIconName();
       if (!$sprite) {
         $sprite = 'application';
       }
 
       $crumbs[] = id(new PhabricatorCrumbView())
         ->setHref($this->getApplicationURI())
         ->setIcon($sprite);
     }
 
     $view = new PhabricatorCrumbsView();
     foreach ($crumbs as $crumb) {
       $view->addCrumb($crumb);
     }
 
     return $view;
   }
 
 }
diff --git a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
index a0ff24345..cc5eede13 100644
--- a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
+++ b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
@@ -1,65 +1,62 @@
 <?php
 
 final class PhabricatorApplicationTransactionResponse
   extends AphrontProxyResponse {
 
   private $viewer;
   private $transactions;
   private $anchorOffset;
 
   protected function buildProxy() {
     return new AphrontAjaxResponse();
   }
 
   public function setAnchorOffset($anchor_offset) {
     $this->anchorOffset = $anchor_offset;
     return $this;
   }
 
   public function getAnchorOffset() {
     return $this->anchorOffset;
   }
 
   public function setTransactions($transactions) {
     assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
 
     $this->transactions = $transactions;
     return $this;
   }
 
   public function getTransactions() {
     return $this->transactions;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
-  public function buildResponseString() {
+  public function reduceProxyResponse() {
     $view = id(new PhabricatorApplicationTransactionView())
       ->setViewer($this->getViewer())
       ->setTransactions($this->getTransactions());
 
     if ($this->getAnchorOffset()) {
       $view->setAnchorOffset($this->getAnchorOffset());
     }
 
     $xactions = mpull($view->buildEvents(), 'render', 'getTransactionPHID');
 
     $content = array(
       'xactions' => $xactions,
       'spacer'   => PhabricatorTimelineView::renderSpacer(),
     );
 
-    return $this
-      ->getProxy()
-      ->setContent($content)
-      ->buildResponseString();
+    return $this->getProxy()->setContent($content);
   }
 
 }
diff --git a/src/infrastructure/diff/PhabricatorChangesetResponse.php b/src/infrastructure/diff/PhabricatorChangesetResponse.php
index 86f4f3dcc..b4897eb18 100644
--- a/src/infrastructure/diff/PhabricatorChangesetResponse.php
+++ b/src/infrastructure/diff/PhabricatorChangesetResponse.php
@@ -1,34 +1,34 @@
 <?php
 
 final class PhabricatorChangesetResponse extends AphrontProxyResponse {
 
   private $renderedChangeset;
   private $coverage;
 
   public function setRenderedChangeset($rendered_changeset) {
     $this->renderedChangeset = $rendered_changeset;
     return $this;
   }
 
   public function setCoverage($coverage) {
     $this->coverage = $coverage;
     return $this;
   }
 
   protected function buildProxy() {
     return new AphrontAjaxResponse();
   }
 
-  public function buildResponseString() {
+  public function reduceProxyResponse() {
     $content = array(
       'changeset' => $this->renderedChangeset,
     );
 
     if ($this->coverage) {
       $content['coverage'] = $this->coverage;
     }
 
-    return $this->getProxy()->setContent($content)->buildResponseString();
+    return $this->getProxy()->setContent($content);
   }
 
 }