diff --git a/CHANGELOG.md b/CHANGELOG.md
index 156a6e0a4..f3592bb65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 - Tags: Ability to set the tags page as the home page.
 - `bidi` attribute for Mithril elements as a shortcut to set up bidirectional bindings.
 - Abstract SettingsModal component for quickly building admin config modals.
+- "Debug" button to inspect the response of a failed AJAX request.
 
 ### Changed
 - Migrations must be namespaced under `Flarum\Migrations\{Core|ExtensionName}`. ([#422](https://github.com/flarum/core/issues/422))
diff --git a/js/lib/App.js b/js/lib/App.js
index 1e00cd992..627c3c2ac 100644
--- a/js/lib/App.js
+++ b/js/lib/App.js
@@ -1,8 +1,11 @@
 import ItemList from 'flarum/utils/ItemList';
 import Alert from 'flarum/components/Alert';
+import Button from 'flarum/components/Button';
+import RequestErrorModal from 'flarum/components/RequestErrorModal';
 import Translator from 'flarum/Translator';
 import extract from 'flarum/utils/extract';
 import patchMithril from 'flarum/utils/patchMithril';
+import RequestError from 'flarum/utils/RequestError';
 
 /**
  * The `App` class provides a container for an application, as well as various
@@ -193,7 +196,7 @@ export default class App {
       try {
         return JSON.parse(responseText);
       } catch (e) {
-        throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
+        throw new RequestError(e.message, responseText);
       }
     });
 
@@ -202,35 +205,52 @@ export default class App {
     // awry.
     const original = options.extract;
     options.extract = xhr => {
+      let responseText;
+
+      if (original) {
+        responseText = original(xhr.responseText);
+      } else {
+        responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
+      }
+
       const status = xhr.status;
 
       if (status >= 500 && status <= 599) {
-        throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
+        throw new RequestError('Internal Server Error', responseText);
       }
 
-      if (original) {
-        return original(xhr.responseText);
-      }
-
-      return xhr.responseText.length > 0 ? xhr.responseText : null;
+      return responseText;
     };
 
-    this.alerts.dismiss(this.requestError);
+    this.alerts.dismiss(this.requestErrorAlert);
 
     // Now make the request. If it's a failure, inspect the error that was
     // returned and show an alert containing its contents.
-    return m.request(options).then(null, response => {
-      if (response instanceof Error) {
-        this.alerts.show(this.requestError = new Alert({
+    return m.request(options).then(null, error => {
+      if (error instanceof RequestError) {
+        this.alerts.show(this.requestErrorAlert = new Alert({
           type: 'error',
-          children: response.message
+          children: 'Oops! Something went wrong. Please reload the page and try again.',
+          controls: app.forum.attribute('debug') ? [
+            <Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
+          ] : undefined
         }));
       }
 
-      throw response;
+      throw error;
     });
   }
 
+  /**
+   * @param {RequestError} error
+   * @private
+   */
+  showDebug(error) {
+    this.alerts.dismiss(this.requestErrorAlert);
+
+    this.modal.show(new RequestErrorModal({error}));
+  }
+
   /**
    * Show alert error messages for each error returned in an API response.
    *
diff --git a/js/lib/components/RequestErrorModal.js b/js/lib/components/RequestErrorModal.js
new file mode 100644
index 000000000..ed84ec7b8
--- /dev/null
+++ b/js/lib/components/RequestErrorModal.js
@@ -0,0 +1,25 @@
+import Modal from 'flarum/components/Modal';
+
+export default class RequestErrorModal extends Modal {
+  className() {
+    return 'RequestErrorModal Modal--large';
+  }
+
+  title() {
+    return this.props.error.message;
+  }
+
+  content() {
+    let responseText;
+
+    try {
+      responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
+    } catch (e) {
+      responseText = this.props.error.responseText;
+    }
+
+    return <div className="Modal-body">
+      <pre>{responseText}</pre>
+    </div>;
+  }
+}
diff --git a/js/lib/utils/RequestError.js b/js/lib/utils/RequestError.js
new file mode 100644
index 000000000..f4ee196fb
--- /dev/null
+++ b/js/lib/utils/RequestError.js
@@ -0,0 +1,6 @@
+export default class RequestError {
+  constructor(message, responseText) {
+    this.message = message;
+    this.responseText = responseText;
+  }
+}
diff --git a/less/lib/Alert.less b/less/lib/Alert.less
index f034a237d..7f493fcac 100755
--- a/less/lib/Alert.less
+++ b/less/lib/Alert.less
@@ -4,21 +4,21 @@
   background: @alert-bg;
   line-height: 1.5;
 
-  &, button, button:hover {
+  &, .Button, .Button:hover {
     color: @alert-color;
   }
 }
 .Alert--error {
   background: @alert-error-bg;
 
-  &, a, a:hover, button, button:hover {
+  &, a, a:hover, .Button, .Button:hover {
     color: @alert-error-color;
   }
 }
 .Alert--success {
   background: @alert-success-bg;
 
-  &, a, a:hover, button, button:hover {
+  &, a, a:hover, .Button, .Button:hover {
     color: @alert-success-color;
   }
   a, a:hover {
@@ -35,7 +35,7 @@
     display: inline-block;
     margin: 0 5px;
 
-    > a {
+    > a, > .Button {
       text-transform: uppercase;
       font-size: 12px;
       font-weight: bold;
@@ -45,6 +45,9 @@
         text-decoration: none;
         opacity: 0.5;
       }
+      &:hover {
+        text-decoration: underline;
+      }
     }
 
     > .Button {
diff --git a/less/lib/scaffolding.less b/less/lib/scaffolding.less
index f1d0aa4ea..1e2ad4e0d 100755
--- a/less/lib/scaffolding.less
+++ b/less/lib/scaffolding.less
@@ -130,3 +130,10 @@ blockquote ol:last-child {
     position: fixed;
   }
 }
+
+.RequestErrorModal {
+  pre {
+    white-space: pre-wrap;
+    margin: 0;
+  }
+}