Added error boundaries for better failures (#527)

* Added error boundaries for better failing

* Fix regex escaping (fixes #525)

* Many more error boundaries
This commit is contained in:
Gregory Schier 2017-10-13 15:01:27 +02:00 committed by GitHub
parent 01f6363eeb
commit 7f86b46417
12 changed files with 493 additions and 305 deletions

View File

@ -321,7 +321,19 @@ export function escapeRegex (str: string): string {
}
export function fuzzyMatch (searchString: string, text: string): boolean {
const regexSearchString = escapeRegex(searchString.toLowerCase()).split('').join('.*');
const toMatch = new RegExp(regexSearchString);
const lowercase = searchString.toLowerCase();
// Split into individual chars, then escape the ones that need it.
const regexSearchString = lowercase.split('').map(v => escapeRegex(v)).join('.*');
let toMatch;
try {
toMatch = new RegExp(regexSearchString);
} catch (err) {
console.warn('Invalid regex', searchString, regexSearchString);
// Invalid regex somehow
return false;
}
return toMatch.test(text.toLowerCase());
}

View File

@ -1,17 +1,26 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import {trackEvent} from '../../../analytics/index';
import * as misc from '../../../common/misc';
type Props = {|
href: string,
title?: string,
button?: boolean,
onClick?: Function,
children?: React.Node
|};
@autobind
class Link extends PureComponent {
_handleClick (e) {
class Link extends React.PureComponent<Props> {
_handleClick (e: SyntheticEvent<HTMLAnchorElement>) {
e && e.preventDefault();
const {href, onClick} = this.props;
// Also call onClick that was passed to us if there was one
onClick && onClick(e);
misc.clickLink(href);
trackEvent('Link', 'Click', href);
}
@ -30,13 +39,4 @@ class Link extends PureComponent {
}
}
Link.propTypes = {
href: PropTypes.string.isRequired,
// Optional
button: PropTypes.bool,
onClick: PropTypes.func,
children: PropTypes.node
};
export default Link;

View File

@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import * as querystring from '../../../common/querystring';
import Link from './link';
type Props = {|
email: string,
children?: React.Node,
subject?: string,
body?: string,
|};
@autobind
class Mailto extends React.PureComponent<Props> {
render () {
const {email, body, subject, children} = this.props;
const params = [];
if (subject) {
params.push({name: 'subject', value: subject});
}
if (body) {
params.push({name: 'body', value: body});
}
const qs = querystring.buildFromParams(params);
const href = querystring.joinUrl(`mailto:${email}`, qs);
return (
<Link href={href}>{children || email}</Link>
);
}
}
export default Mailto;

View File

@ -0,0 +1,94 @@
// @flow
import * as React from 'react';
import {showError} from './modals/index';
import Mailto from './base/mailto';
type Props = {
children: React.Node,
errorClassName?: string,
showAlert?: boolean,
replaceWith?: React.Node
};
type State = {
error: Error | null,
info: {componentStack: string} | null
};
class SingleErrorBoundary extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
error: null,
info: null
};
}
componentDidCatch (error: Error, info: {componentStack: string}) {
const {children} = this.props;
const firstChild = Array.isArray(children) && children.length === 1 ? children[0] : children;
this.setState({error, info});
let componentName = 'component';
try {
componentName = (firstChild: any).type.name;
} catch (err) {
// It's okay
}
if (this.props.showAlert) {
try {
showError({
error,
title: 'Application Error',
message: (
<p>
Failed to render {componentName}.
Please send the following error to
{' '}
<Mailto email="support@insomnia.rest" subject="Error Report" body={error.stack}/>.
</p>
)
});
} catch (err) {
// UI is so broken that we can't even show an alert
}
}
}
render () {
const {error, info} = this.state;
const {errorClassName, children} = this.props;
if (error && info) {
return (
<div className={errorClassName || null}>
Render Failure: {error.message}
</div>
);
}
return children;
}
}
class ErrorBoundary extends React.PureComponent<Props> {
render () {
const {children, ...extraProps} = this.props;
if (!children) {
return null;
}
// Unwrap multiple children into single children for better error isolation
const childArray = Array.isArray(children) ? children : [children];
return childArray.map((child, i) => (
<SingleErrorBoundary key={i} {...extraProps}>
{child}
</SingleErrorBoundary>
));
}
}
export default ErrorBoundary;

View File

@ -35,8 +35,6 @@ class ErrorModal extends PureComponent {
const {title, error, addCancel, message} = options;
this.setState({title, error, addCancel, message});
console.warn(`Error: ${message || ''}`, error.stack);
this.modal.show();
return new Promise(resolve => {
@ -51,9 +49,11 @@ class ErrorModal extends PureComponent {
<Modal ref={this._setModalRef} closeOnKeyCodes={[13]}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">
<strong>{message && message}</strong>
{message ? (
<div className="notice error">{message}</div>
) : null}
{error && (
<pre className="pad-top-sm force-wrap">
<pre className="pad-top-sm force-wrap selectable">
<code>{error.stack}</code>
</pre>
)}

View File

@ -161,7 +161,8 @@ class SettingsModal extends PureComponent {
activeTheme={settings.theme}
/>
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable"><SettingsShortcuts/></TabPanel>
<TabPanel
className="react-tabs__tab-panel pad scrollable"><SettingsShortcuts/></TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable"><Account/></TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable"><Plugins/></TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable"><About/></TabPanel>

View File

@ -25,6 +25,7 @@ import RequestSettingsModal from './modals/request-settings-modal';
import MarkdownPreview from './markdown-preview';
import type {Settings} from '../../models/settings';
import * as hotkeys from '../../common/hotkeys';
import ErrorBoundary from './error-boundary';
type Props = {
// Functions
@ -249,6 +250,7 @@ class RequestPane extends React.PureComponent<Props> {
return (
<section className="pane request-pane">
<header className="pane__header">
<ErrorBoundary errorClassName="font-error pad text-center">
<RequestUrlBar
uniquenessKey={uniqueKey}
method={request.method}
@ -265,6 +267,7 @@ class RequestPane extends React.PureComponent<Props> {
url={request.url}
requestId={request._id}
/>
</ErrorBoundary>
</header>
<Tabs className="react-tabs pane__body" forceRenderTabPanel>
<TabList>
@ -331,6 +334,7 @@ class RequestPane extends React.PureComponent<Props> {
</TabPanel>
<TabPanel className="react-tabs__tab-panel scrollable-container">
<div className="scrollable">
<ErrorBoundary errorClassName="font-error pad text-center">
<AuthWrapper
key={uniqueKey}
oAuth2Token={oAuth2Token}
@ -342,21 +346,27 @@ class RequestPane extends React.PureComponent<Props> {
nunjucksPowerUserMode={nunjucksPowerUserMode}
onChange={updateRequestAuthentication}
/>
</ErrorBoundary>
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel query-editor">
<div className="pad pad-bottom-sm query-editor__preview">
<label className="label--small no-pad-top">Url Preview</label>
<code className="txt-sm block faint">
<ErrorBoundary
errorClassName="tall wide vertically-align font-error pad text-center">
<RenderedQueryString
key={uniqueKey}
handleRender={handleRender}
request={request}
/>
</ErrorBoundary>
</code>
</div>
<div className="scrollable-container">
<div className="scrollable">
<ErrorBoundary
errorClassName="tall wide vertically-align font-error pad text-center">
<KeyValueEditor
sortable
key={uniqueKey}
@ -371,6 +381,7 @@ class RequestPane extends React.PureComponent<Props> {
nunjucksPowerUserMode={nunjucksPowerUserMode}
onChange={updateRequestParameters}
/>
</ErrorBoundary>
</div>
</div>
<div className="pad-right text-right">
@ -382,6 +393,7 @@ class RequestPane extends React.PureComponent<Props> {
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel header-editor">
<ErrorBoundary errorClassName="font-error pad text-center">
<RequestHeadersEditor
key={uniqueKey}
headers={request.headers}
@ -394,6 +406,7 @@ class RequestPane extends React.PureComponent<Props> {
onChange={updateRequestHeaders}
bulk={useBulkHeaderEditor}
/>
</ErrorBoundary>
<div className="pad-right text-right">
<button className="margin-top-sm btn btn--clicky"
@ -411,12 +424,14 @@ class RequestPane extends React.PureComponent<Props> {
</button>
</div>
<div className="pad">
<ErrorBoundary errorClassName="font-error pad text-center">
<MarkdownPreview
heading={request.name}
debounceMillis={1000}
markdown={request.description}
handleRender={handleRender}
/>
</ErrorBoundary>
</div>
</div>
) : (

View File

@ -26,6 +26,7 @@ import {cancelCurrentRequest} from '../../network/network';
import {trackEvent} from '../../analytics';
import Hotkey from './hotkey';
import * as hotkeys from '../../common/hotkeys';
import ErrorBoundary from './error-boundary';
type Props = {
// Functions
@ -278,6 +279,7 @@ class ResponsePane extends React.PureComponent<Props> {
</Tab>
</TabList>
<TabPanel className="react-tabs__tab-panel">
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseViewer
key={response._id}
// Send larger one because legacy responses have bytesContent === -1
@ -297,17 +299,21 @@ class ResponsePane extends React.PureComponent<Props> {
editorKeyMap={editorKeyMap}
url={response.url}
/>
</ErrorBoundary>
</TabPanel>
<TabPanel className="react-tabs__tab-panel scrollable-container">
<div className="scrollable pad">
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseHeadersViewer
key={response._id}
headers={response.headers}
/>
</ErrorBoundary>
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel scrollable-container">
<div className="scrollable pad">
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseCookiesViewer
handleShowRequestSettings={handleShowRequestSettings}
cookiesSent={response.settingSendCookies}
@ -316,9 +322,11 @@ class ResponsePane extends React.PureComponent<Props> {
key={response._id}
headers={cookieHeaders}
/>
</ErrorBoundary>
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel">
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseTimelineViewer
key={response._id}
timeline={response.timeline || []}
@ -326,12 +334,15 @@ class ResponsePane extends React.PureComponent<Props> {
editorFontSize={editorFontSize}
editorIndentSize={editorIndentSize}
/>
</ErrorBoundary>
</TabPanel>
</Tabs>
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseTimer
handleCancel={cancelCurrentRequest}
loadStartTime={loadStartTime}
/>
</ErrorBoundary>
</section>
);
}

View File

@ -10,9 +10,8 @@ class ResponseHeadersViewer extends PureComponent {
h => `${h.name}: ${h.value}`
).join('\n');
return (
<div>
<table className="table--fancy table--striped">
return [
<table key='table' className="table--fancy table--striped">
<thead>
<tr>
<th>Name</th>
@ -27,15 +26,14 @@ class ResponseHeadersViewer extends PureComponent {
</tr>
))}
</tbody>
</table>
<p className="pad-top">
</table>,
<p key='copy' className="pad-top">
<CopyButton
className="pull-right btn btn--clicky"
content={headersString}
/>
</p>
</div>
);
];
}
}

View File

@ -41,6 +41,7 @@ import {trackEvent} from '../../analytics/index';
import * as importers from 'insomnia-importers';
import type {CookieJar} from '../../models/cookie-jar';
import type {Environment} from '../../models/environment';
import ErrorBoundary from './error-boundary';
type Props = {
// Helper Functions
@ -389,124 +390,15 @@ class Wrapper extends React.PureComponent<Props, State> {
const columns = `${realSidebarWidth}rem 0 minmax(0, ${paneWidth}fr) 0 minmax(0, ${1 - paneWidth}fr)`;
const rows = `minmax(0, ${paneHeight}fr) 0 minmax(0, ${1 - paneHeight}fr)`;
return (
<div id="wrapper"
className={classnames('wrapper', {'wrapper--vertical': settings.forceVerticalLayout})}
style={{gridTemplateColumns: columns, gridTemplateRows: rows}}>
<Sidebar
ref={handleSetSidebarRef}
showEnvironmentsModal={this._handleShowEnvironmentsModal}
showCookiesModal={this._handleShowCookiesModal}
handleActivateRequest={handleActivateRequest}
handleChangeFilter={handleSetSidebarFilter}
handleImportFile={this._handleImportFile}
handleExportFile={handleExportFile}
handleSetActiveWorkspace={handleSetActiveWorkspace}
handleDuplicateRequest={handleDuplicateRequest}
handleGenerateCode={handleGenerateCode}
handleCopyAsCurl={handleCopyAsCurl}
handleDuplicateRequestGroup={handleDuplicateRequestGroup}
handleSetActiveEnvironment={handleSetActiveEnvironment}
moveDoc={handleMoveDoc}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
activeRequest={activeRequest}
activeEnvironment={activeEnvironment}
handleCreateRequest={handleCreateRequest}
handleCreateRequestGroup={handleCreateRequestGroup}
filter={sidebarFilter || ''}
hidden={sidebarHidden || false}
workspace={activeWorkspace}
unseenWorkspaces={unseenWorkspaces}
childObjects={sidebarChildren}
width={sidebarWidth}
isLoading={isLoading}
workspaces={workspaces}
environments={environments}
/>
<div className="drag drag--sidebar">
<div onDoubleClick={handleResetDragSidebar} onMouseDown={this._handleStartDragSidebar}>
</div>
</div>
<RequestPane
ref={handleSetRequestPaneRef}
handleImportFile={this._handleImportFile}
request={activeRequest}
showPasswords={settings.showPasswords}
useBulkHeaderEditor={settings.useBulkHeaderEditor}
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}
editorKeyMap={settings.editorKeyMap}
editorLineWrapping={settings.editorLineWrapping}
workspace={activeWorkspace}
settings={settings}
environmentId={activeEnvironment ? activeEnvironment._id : ''}
oAuth2Token={oAuth2Token}
forceUpdateRequest={this._handleForceUpdateRequest}
handleCreateRequest={handleCreateRequestForWorkspace}
handleGenerateCode={handleGenerateCodeForActiveRequest}
handleImport={this._handleImport}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
updateRequestBody={this._handleUpdateRequestBody}
updateRequestUrl={this._handleUpdateRequestUrl}
updateRequestMethod={this._handleUpdateRequestMethod}
updateRequestParameters={this._handleUpdateRequestParameters}
updateRequestAuthentication={this._handleUpdateRequestAuthentication}
updateRequestHeaders={this._handleUpdateRequestHeaders}
updateRequestMimeType={this._handleUpdateRequestMimeType}
updateSettingsShowPasswords={this._handleUpdateSettingsShowPasswords}
updateSettingsUseBulkHeaderEditor={this._handleUpdateSettingsUseBulkHeaderEditor}
forceRefreshCounter={this.state.forceRefreshKey}
handleSend={this._handleSendRequestWithActiveEnvironment}
handleSendAndDownload={this._handleSendAndDownloadRequestWithActiveEnvironment}
/>
<div className="drag drag--pane-horizontal">
<div
onMouseDown={handleStartDragPaneHorizontal}
onDoubleClick={handleResetDragPaneHorizontal}>
</div>
</div>
<div className="drag drag--pane-vertical">
<div
onMouseDown={handleStartDragPaneVertical}
onDoubleClick={handleResetDragPaneVertical}>
</div>
</div>
<ResponsePane
ref={handleSetResponsePaneRef}
request={activeRequest}
responses={activeRequestResponses}
response={activeResponse}
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}
editorKeyMap={settings.editorKeyMap}
editorLineWrapping={settings.editorLineWrapping}
previewMode={responsePreviewMode}
filter={responseFilter}
filterHistory={responseFilterHistory}
loadStartTime={loadStartTime}
showCookiesModal={this._handleShowCookiesModal}
handleShowRequestSettings={this._handleShowRequestSettingsModal}
handleSetActiveResponse={this._handleSetActiveResponse}
handleSetPreviewMode={this._handleSetPreviewMode}
handleDeleteResponses={this._handleDeleteResponses}
handleDeleteResponse={this._handleDeleteResponse}
handleSetFilter={this._handleSetResponseFilter}
/>
<div className="modals">
return [
<div key="modals" className="modals">
<ErrorBoundary showAlert>
<AlertModal ref={registerModal}/>
<ErrorModal ref={registerModal}/>
<PromptModal ref={registerModal}/>
<ChangelogModal ref={registerModal}/>
<LoginModal ref={registerModal}/>
<PromptModal ref={registerModal}/>
<AskModal ref={registerModal}/>
<RequestCreateModal ref={registerModal}/>
<PaymentNotificationModal ref={registerModal}/>
@ -630,9 +522,127 @@ class Wrapper extends React.PureComponent<Props, State> {
getRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
/>
</ErrorBoundary>
</div>,
<div key="wrapper"
id="wrapper"
className={classnames('wrapper', {'wrapper--vertical': settings.forceVerticalLayout})}
style={{gridTemplateColumns: columns, gridTemplateRows: rows}}>
<ErrorBoundary showAlert>
<Sidebar
ref={handleSetSidebarRef}
showEnvironmentsModal={this._handleShowEnvironmentsModal}
showCookiesModal={this._handleShowCookiesModal}
handleActivateRequest={handleActivateRequest}
handleChangeFilter={handleSetSidebarFilter}
handleImportFile={this._handleImportFile}
handleExportFile={handleExportFile}
handleSetActiveWorkspace={handleSetActiveWorkspace}
handleDuplicateRequest={handleDuplicateRequest}
handleGenerateCode={handleGenerateCode}
handleCopyAsCurl={handleCopyAsCurl}
handleDuplicateRequestGroup={handleDuplicateRequestGroup}
handleSetActiveEnvironment={handleSetActiveEnvironment}
moveDoc={handleMoveDoc}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
activeRequest={activeRequest}
activeEnvironment={activeEnvironment}
handleCreateRequest={handleCreateRequest}
handleCreateRequestGroup={handleCreateRequestGroup}
filter={sidebarFilter || ''}
hidden={sidebarHidden || false}
workspace={activeWorkspace}
unseenWorkspaces={unseenWorkspaces}
childObjects={sidebarChildren}
width={sidebarWidth}
isLoading={isLoading}
workspaces={workspaces}
environments={environments}
/>
</ErrorBoundary>
<div className="drag drag--sidebar">
<div onDoubleClick={handleResetDragSidebar} onMouseDown={this._handleStartDragSidebar}>
</div>
</div>
);
<ErrorBoundary showAlert>
<RequestPane
ref={handleSetRequestPaneRef}
handleImportFile={this._handleImportFile}
request={activeRequest}
showPasswords={settings.showPasswords}
useBulkHeaderEditor={settings.useBulkHeaderEditor}
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}
editorKeyMap={settings.editorKeyMap}
editorLineWrapping={settings.editorLineWrapping}
workspace={activeWorkspace}
settings={settings}
environmentId={activeEnvironment ? activeEnvironment._id : ''}
oAuth2Token={oAuth2Token}
forceUpdateRequest={this._handleForceUpdateRequest}
handleCreateRequest={handleCreateRequestForWorkspace}
handleGenerateCode={handleGenerateCodeForActiveRequest}
handleImport={this._handleImport}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
updateRequestBody={this._handleUpdateRequestBody}
updateRequestUrl={this._handleUpdateRequestUrl}
updateRequestMethod={this._handleUpdateRequestMethod}
updateRequestParameters={this._handleUpdateRequestParameters}
updateRequestAuthentication={this._handleUpdateRequestAuthentication}
updateRequestHeaders={this._handleUpdateRequestHeaders}
updateRequestMimeType={this._handleUpdateRequestMimeType}
updateSettingsShowPasswords={this._handleUpdateSettingsShowPasswords}
updateSettingsUseBulkHeaderEditor={this._handleUpdateSettingsUseBulkHeaderEditor}
forceRefreshCounter={this.state.forceRefreshKey}
handleSend={this._handleSendRequestWithActiveEnvironment}
handleSendAndDownload={this._handleSendAndDownloadRequestWithActiveEnvironment}
/>
</ErrorBoundary>
<div className="drag drag--pane-horizontal">
<div
onMouseDown={handleStartDragPaneHorizontal}
onDoubleClick={handleResetDragPaneHorizontal}>
</div>
</div>
<div className="drag drag--pane-vertical">
<div
onMouseDown={handleStartDragPaneVertical}
onDoubleClick={handleResetDragPaneVertical}>
</div>
</div>
<ErrorBoundary showAlert>
<ResponsePane
ref={handleSetResponsePaneRef}
request={activeRequest}
responses={activeRequestResponses}
response={activeResponse}
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}
editorKeyMap={settings.editorKeyMap}
editorLineWrapping={settings.editorLineWrapping}
previewMode={responsePreviewMode}
filter={responseFilter}
filterHistory={responseFilterHistory}
loadStartTime={loadStartTime}
showCookiesModal={this._handleShowCookiesModal}
handleShowRequestSettings={this._handleShowRequestSettingsModal}
handleSetActiveResponse={this._handleSetActiveResponse}
handleSetPreviewMode={this._handleSetPreviewMode}
handleDeleteResponses={this._handleDeleteResponses}
handleDeleteResponse={this._handleDeleteResponse}
handleSetFilter={this._handleSetResponseFilter}
/>
</ErrorBoundary>
</div>
];
}
}

View File

@ -38,6 +38,7 @@ import {exportHar} from '../../common/har';
import * as hotkeys from '../../common/hotkeys';
import KeydownBinder from '../components/keydown-binder';
import {executeHotKey} from '../../common/hotkeys';
import ErrorBoundary from '../components/error-boundary';
@autobind
class App extends PureComponent {
@ -720,6 +721,7 @@ class App extends PureComponent {
return (
<KeydownBinder onKeydown={this._handleKeyDown}>
<div className="app">
<ErrorBoundary showAlert>
<Wrapper
{...this.props}
ref={this._setWrapperRef}
@ -758,7 +760,11 @@ class App extends PureComponent {
handleSetSidebarFilter={this._handleSetSidebarFilter}
handleToggleMenuBar={this._handleToggleMenuBar}
/>
</ErrorBoundary>
<ErrorBoundary showAlert>
<Toast/>
</ErrorBoundary>
{/* Block all mouse activity by showing an overlay while dragging */}
{this.state.showDragOverlay ? <div className="blocker-overlay"></div> : null}

View File

@ -129,6 +129,11 @@ code, pre, .monospace {
font-family: @font-default;
}
.pre {
line-height: 1.3em;
white-space: pre;
}
div.notice,
p.notice {
text-align: center;