diff --git a/src/components/send/response-pane.tsx b/src/components/send/response-pane.tsx
index 69956059..6b1b4d64 100644
--- a/src/components/send/response-pane.tsx
+++ b/src/components/send/response-pane.tsx
@@ -3,20 +3,21 @@ import { inject, observer } from "mobx-react";
import * as portals from 'react-reverse-portal';
import { HttpExchange } from "../../types";
+import { logError } from "../../errors";
import { UiStore } from '../../model/ui/ui-store';
import { AccountStore } from '../../model/account/account-store';
-import { SuccessfulExchange } from "../../model/http/exchange";
+import { CompletedExchange, SuccessfulExchange } from "../../model/http/exchange";
import { RequestInput } from "../../model/send/send-request-model";
+import { tagsToErrorType } from "../../model/http/error-types";
import { ContainerSizedEditor } from '../editor/base-editor';
-import { HttpAbortedResponseCard } from '../view/http/http-aborted-card';
import { SendCardContainer } from './send-card-section';
-import { PendingResponseStatusSection, ResponseStatusSection } from './sent-response-status';
+import { FailedResponseStatusSection, PendingResponseStatusSection, ResponseStatusSection } from './sent-response-status';
import { PendingResponseHeaderSection, SentResponseHeaderSection } from './sent-response-headers';
import { SentResponseBodyCard } from './sent-response-body';
-
+import { SentResponseError } from './sent-response-error';
@inject('uiStore')
@inject('accountStore')
@observer
@@ -44,7 +45,7 @@ export class ResponsePane extends React.Component<{
exchange.isSuccessfulExchange()
? this.renderSuccessfulResponse(exchange)
: exchange.isCompletedExchange()
- ? this.renderAbortedResponse(exchange)
+ ? this.renderFailedResponse(exchange)
: this.renderInProgressResponse()
}
;
@@ -75,11 +76,28 @@ export class ResponsePane extends React.Component<{
}
- renderAbortedResponse(exchange: HttpExchange) {
- return ;
+ renderFailedResponse(exchange: CompletedExchange) {
+ const { uiStore } = this.props;
+
+ const errorType = tagsToErrorType(exchange.tags);
+
+ if (!errorType) {
+ logError(`Sent response failed with no error tags: ${
+ JSON.stringify(exchange.tags)
+ } (${exchange.abortMessage})`);
+ }
+
+ return <>
+
+
+ >;
}
renderInProgressResponse() {
diff --git a/src/components/send/sent-response-error.tsx b/src/components/send/sent-response-error.tsx
new file mode 100644
index 00000000..5d985986
--- /dev/null
+++ b/src/components/send/sent-response-error.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react';
+
+import { styled } from '../../styles';
+import { WarningIcon } from '../../icons';
+import { unreachableCheck } from '../../util/error';
+import { logError } from '../../errors';
+
+import {
+ ErrorType,
+ wasNotForwarded,
+ wasServerIssue
+} from '../../model/http/error-types';
+
+import { SendCardSection } from './send-card-section';
+import { ContentMonoValue } from '../common/text-content';
+
+const ExplanationBlock = styled.p`
+ margin-bottom: 10px;
+ line-height: 1.3;
+`;
+
+const FailureBlock = styled(ExplanationBlock)`
+ font-weight: bold;
+`;
+
+export const SentResponseError = (props: {
+ errorType: ErrorType,
+ errorMessage: string | undefined
+}) => {
+ const { errorType, errorMessage } = props;
+
+ if (
+ !wasNotForwarded(errorType) &&
+ !wasServerIssue(errorType)
+ ) {
+ logError(`Unexpected Send error type: ${errorType}`);
+ }
+
+ return
+
+
+ {
+ wasNotForwarded(errorType)
+ ? 'This request was not sent successfully'
+ : wasServerIssue(errorType)
+ ? 'This response was not received successfully'
+ : `The request failed because of an unexpected error: ${errorType}`
+ }
+
+ {
+ wasNotForwarded(errorType)
+ ?
+ The upstream server {
+ errorType === 'wrong-host'
+ ? 'responded with an HTTPS certificate for the wrong hostname'
+ : errorType === 'expired'
+ ? 'has an expired HTTPS certificate'
+ : errorType === 'not-yet-valid'
+ ? 'has an HTTPS certificate with a start date in the future'
+ : errorType === 'untrusted'
+ ? 'has an untrusted HTTPS certificate'
+ : errorType === 'tls-error'
+ ? 'failed to complete a TLS handshake'
+ : errorType === 'host-unreachable'
+ ? 'was not reachable on your network connection'
+ : errorType === 'host-not-found' || errorType === 'dns-error'
+ ? 'hostname could be not found'
+ : errorType === 'connection-refused'
+ ? 'refused the connection'
+ : unreachableCheck(errorType)
+ }, so HTTP Toolkit did not send the request.
+
+ : wasServerIssue(errorType)
+ ?
+ The upstream request failed because {
+ errorType === 'connection-reset'
+ ? 'the connection to the server was reset'
+ : errorType === 'server-unparseable'
+ ? 'the response from the server was unparseable'
+ : errorType === 'server-timeout'
+ ? 'of a timeout waiting for a response from the server'
+ : unreachableCheck(errorType)
+ }.
+
+ :
+ It's not clear what's gone wrong here, but for some reason HTTP Toolkit
+ couldn't successfully and/or securely complete this request. This might be an
+ intermittent issue, and may be resolved by retrying the request.
+
+ }
+ { !!errorMessage &&
+ { errorMessage }
+ }
+
+}
\ No newline at end of file
diff --git a/src/components/send/sent-response-status.tsx b/src/components/send/sent-response-status.tsx
index f0fdd677..0da5a5b7 100644
--- a/src/components/send/sent-response-status.tsx
+++ b/src/components/send/sent-response-status.tsx
@@ -1,11 +1,13 @@
+import * as _ from 'lodash';
import * as React from 'react';
import { Theme, styled } from '../../styles';
+import { getReadableSize } from '../../util/buffer';
import { getStatusColor } from '../../model/events/categorization';
import { getStatusMessage } from '../../model/http/http-docs';
-import { getReadableSize } from '../../util/buffer';
-import { SuccessfulExchange } from '../../model/http/exchange';
+import { CompletedExchange, SuccessfulExchange } from '../../model/http/exchange';
+import { ErrorType } from '../../model/http/error-types';
import { SendCardSection } from './send-card-section';
import { Pill } from '../common/pill';
@@ -69,4 +71,26 @@ export const PendingResponseStatusSection = (props: {
;
+}
+
+export const FailedResponseStatusSection = (props: {
+ exchange: CompletedExchange,
+ errorType: ErrorType
+ theme: Theme
+}) => {
+ return
+
+
+ Failed: { _.startCase(props.errorType) }
+
+
+
+ ;
}
\ No newline at end of file
diff --git a/src/components/view/http/http-details-pane.tsx b/src/components/view/http/http-details-pane.tsx
index 4872daa3..0f93c3b5 100644
--- a/src/components/view/http/http-details-pane.tsx
+++ b/src/components/view/http/http-details-pane.tsx
@@ -16,6 +16,7 @@ import { buildRuleFromRequest } from '../../../model/rules/rule-creation';
import { findItem } from '../../../model/rules/rules-structure';
import { HtkMockRule, getRulePartKey } from '../../../model/rules/rules';
import { WebSocketStream } from '../../../model/websockets/websocket-stream';
+import { tagsToErrorType } from '../../../model/http/error-types';
import { PaneOuterContainer, PaneScrollContainer } from '../view-details-pane';
import { StreamMessageListCard } from '../stream-message-list-card';
@@ -29,7 +30,7 @@ import { HttpAbortedResponseCard } from './http-aborted-card';
import { HttpPerformanceCard } from './http-performance-card';
import { HttpExportCard } from './http-export-card';
import { SelfSizedEditor } from '../../editor/base-editor';
-import { HttpErrorHeader, tagsToErrorType } from './http-error-header';
+import { HttpErrorHeader } from './http-error-header';
import { HttpDetailsFooter } from './http-details-footer';
import { HttpRequestBreakpointHeader, HttpResponseBreakpointHeader } from './http-breakpoint-header';
import { HttpBreakpointRequestCard } from './http-breakpoint-request-card';
diff --git a/src/components/view/http/http-error-header.tsx b/src/components/view/http/http-error-header.tsx
index a4f3b46b..ce80a5a3 100644
--- a/src/components/view/http/http-error-header.tsx
+++ b/src/components/view/http/http-error-header.tsx
@@ -2,7 +2,6 @@ import * as React from 'react';
import * as semver from 'semver';
import { WarningIcon } from '../../../icons';
-import { logError } from '../../../errors';
import {
desktopVersion,
@@ -11,6 +10,18 @@ import {
} from '../../../services/service-versions';
import { unreachableCheck } from '../../../util/error';
+
+import {
+ ErrorType,
+ isClientBug,
+ isInitialRequestError,
+ isMockable,
+ isWhitelistable,
+ wasNotForwarded,
+ wasResponseIssue,
+ wasTimeout
+} from '../../../model/http/error-types';
+
import { clickOnEnter } from '../../component-utils';
import {
HeaderCard,
@@ -18,165 +29,6 @@ import {
HeaderButton
} from '../header-card';
-type ErrorType =
- | 'untrusted'
- | 'expired'
- | 'not-yet-valid'
- | 'wrong-host'
- | 'tls-error'
- | 'host-not-found'
- | 'host-unreachable'
- | 'dns-error'
- | 'connection-refused'
- | 'connection-reset'
- | 'client-abort'
- | 'server-timeout'
- | 'client-timeout'
- | 'invalid-http-version'
- | 'invalid-method'
- | 'client-unparseable-url'
- | 'client-unparseable'
- | 'server-unparseable'
- | 'header-overflow'
- | 'invalid-headers'
- | 'unknown';
-
-export function tagsToErrorType(tags: string[]): ErrorType | undefined {
- if (
- tags.includes("passthrough-error:SELF_SIGNED_CERT_IN_CHAIN") ||
- tags.includes("passthrough-error:DEPTH_ZERO_SELF_SIGNED_CERT") ||
- tags.includes("passthrough-error:UNABLE_TO_VERIFY_LEAF_SIGNATURE") ||
- tags.includes("passthrough-error:UNABLE_TO_GET_ISSUER_CERT_LOCALLY")
- ) {
- return 'untrusted';
- }
-
- if (tags.includes("passthrough-error:CERT_HAS_EXPIRED")) return 'expired';
- if (tags.includes("passthrough-error:CERT_NOT_YET_VALID")) return 'not-yet-valid';
- if (tags.includes("passthrough-error:ERR_TLS_CERT_ALTNAME_INVALID")) return 'wrong-host';
-
- if (
- tags.filter(t => t.startsWith("passthrough-tls-error:")).length > 0 ||
- tags.includes("passthrough-error:EPROTO") ||
- tags.includes("passthrough-error:ERR_SSL_WRONG_VERSION_NUMBER") ||
- tags.includes("passthrough-error:ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC") ||
- tags.includes("passthrough-error:ERR_SSL_CIPHER_OPERATION_FAILED") ||
- tags.includes("passthrough-error:ERR_SSL_BAD_RECORD_TYPE") ||
- tags.includes("passthrough-error:ERR_SSL_INTERNAL_ERROR")
- ) {
- return 'tls-error';
- }
-
- if (tags.includes("passthrough-error:ENOTFOUND")) return 'host-not-found';
- if (
- tags.includes("passthrough-error:EHOSTUNREACH") || // No known route to this host
- tags.includes("passthrough-error:ENETUNREACH") // Whole network is unreachable
- ) return 'host-unreachable';
- if (tags.includes("passthrough-error:EAI_AGAIN")) return 'dns-error';
- if (tags.includes("passthrough-error:ECONNREFUSED")) return 'connection-refused';
- if (tags.includes("passthrough-error:ECONNRESET")) return 'connection-reset';
- if (tags.includes("passthrough-error:ETIMEDOUT")) return 'server-timeout';
- if (
- tags.includes("passthrough-error:HPE_INVALID_CONSTANT") ||
- tags.includes("passthrough-error:ERR_INVALID_HTTP_TOKEN") ||
- tags.includes("passthrough-error:ERR_HTTP_INVALID_STATUS_CODE") ||
- tags.includes("passthrough-error:ERR_INVALID_CHAR")
- ) {
- return 'server-unparseable';
- }
-
- if (tags.includes("http-2") || tags.includes("client-error:HPE_INVALID_VERSION")) {
- return 'invalid-http-version';
- }
-
- if (tags.includes("client-error:HPE_INVALID_METHOD")) return 'invalid-method'; // QWE / HTTP/1.1
- if (tags.includes("client-error:HPE_INVALID_URL")) return 'client-unparseable-url'; // http://
- if (
- tags.includes("client-error:HPE_INVALID_CONSTANT") || // GET / HTTQ <- incorrect constant char
- tags.includes("client-error:HPE_INVALID_EOF_STATE") // Unexpected 0-length packet in parser
- ) return 'client-unparseable'; // ABC/1.1
- if (tags.includes("client-error:HPE_HEADER_OVERFLOW")) return 'header-overflow'; // More than ~80KB of headers
- if (
- tags.includes("client-error:HPE_INVALID_CONTENT_LENGTH") ||
- tags.includes("client-error:HPE_INVALID_TRANSFER_ENCODING") ||
- tags.includes("client-error:HPE_INVALID_HEADER_TOKEN") || // Invalid received (req or res) headers
- tags.includes("client-error:HPE_UNEXPECTED_CONTENT_LENGTH") || // T-E with C-L
- tags.includes("passthrough-error:HPE_INVALID_HEADER_TOKEN") // Invalid headers upstream, e.g. after breakpoint
- ) return 'invalid-headers';
-
- if (tags.includes("client-error:ERR_HTTP_REQUEST_TIMEOUT")) return 'client-timeout';
- if (
- tags.includes("client-error:ECONNABORTED") ||
- tags.includes("client-error:EPIPE")
- ) return 'client-abort';
-
- if (
- tags.filter(t => t.startsWith("passthrough-error:")).length > 0 ||
- tags.filter(t => t.startsWith("client-error:")).length > 0
- ) {
- logError(`Unrecognized error tag ${JSON.stringify(tags)}`);
- return 'unknown';
- }
-}
-
-function typeCheck(types: readonly T[]) {
- return (type: string): type is T => types.includes(type as T);
-}
-
-const isInitialRequestError = typeCheck([
- 'invalid-http-version',
- 'invalid-method',
- 'client-unparseable',
- 'client-unparseable-url',
- 'header-overflow',
- 'invalid-headers'
-]);
-
-const isClientBug = typeCheck([
- 'client-unparseable',
- 'client-unparseable-url',
- 'invalid-headers'
-]);
-
-const wasNotForwarded = typeCheck([
- 'untrusted',
- 'expired',
- 'not-yet-valid',
- 'wrong-host',
- 'tls-error',
- 'host-not-found',
- 'host-unreachable',
- 'dns-error',
- 'connection-refused'
-]);
-
-const wasResponseIssue = typeCheck([
- 'server-unparseable',
- 'connection-reset'
-]);
-
-const wasTimeout = typeCheck([
- 'client-timeout',
- 'server-timeout'
-]);
-
-const isWhitelistable = typeCheck([
- 'untrusted',
- 'expired',
- 'not-yet-valid',
- 'wrong-host',
- 'tls-error'
-]);
-
-const isMockable = typeCheck([
- 'host-not-found',
- 'host-unreachable',
- 'dns-error',
- 'connection-refused',
- 'connection-reset',
- 'server-timeout'
-]);
-
export const HttpErrorHeader = (p: {
isPaidUser: boolean,
type: ErrorType,
@@ -202,7 +54,7 @@ export const HttpErrorHeader = (p: {
? This request was not forwarded successfully
: // Forwarded but failed later, or unknown:
This exchange was not completed successfully
- }
+ }
diff --git a/src/components/view/http/http-performance-card.tsx b/src/components/view/http/http-performance-card.tsx
index 890430b7..2e4f0947 100644
--- a/src/components/view/http/http-performance-card.tsx
+++ b/src/components/view/http/http-performance-card.tsx
@@ -4,11 +4,7 @@ import { observer, inject } from 'mobx-react';
import { get } from 'typesafe-get';
import { styled } from '../../../styles';
-import {
- HttpExchange,
- TimingEvents,
- ExchangeMessage
-} from '../../../types';
+import { HttpExchange, ExchangeMessage } from '../../../types';
import { getReadableSize } from '../../../util/buffer';
import { asHeaderArray } from '../../../util/headers';
diff --git a/src/model/http/error-types.ts b/src/model/http/error-types.ts
new file mode 100644
index 00000000..369a8ee7
--- /dev/null
+++ b/src/model/http/error-types.ts
@@ -0,0 +1,163 @@
+import { logError } from '../../errors';
+import { typeCheck } from '../../util';
+
+export type ErrorType =
+ | 'untrusted'
+ | 'expired'
+ | 'not-yet-valid'
+ | 'wrong-host'
+ | 'tls-error'
+ | 'host-not-found'
+ | 'host-unreachable'
+ | 'dns-error'
+ | 'connection-refused'
+ | 'connection-reset'
+ | 'client-abort'
+ | 'server-timeout'
+ | 'client-timeout'
+ | 'invalid-http-version'
+ | 'invalid-method'
+ | 'client-unparseable-url'
+ | 'client-unparseable'
+ | 'server-unparseable'
+ | 'header-overflow'
+ | 'invalid-headers'
+ | 'unknown';
+
+export const isInitialRequestError = typeCheck([
+ 'invalid-http-version',
+ 'invalid-method',
+ 'client-unparseable',
+ 'client-unparseable-url',
+ 'header-overflow',
+ 'invalid-headers'
+]);
+
+export const isClientBug = typeCheck([
+ 'client-unparseable',
+ 'client-unparseable-url',
+ 'invalid-headers'
+]);
+
+export const wasNotForwarded = typeCheck([
+ 'untrusted',
+ 'expired',
+ 'not-yet-valid',
+ 'wrong-host',
+ 'tls-error',
+ 'host-not-found',
+ 'host-unreachable',
+ 'dns-error',
+ 'connection-refused'
+]);
+
+export const wasServerIssue = typeCheck([
+ 'server-unparseable',
+ 'server-timeout',
+ 'connection-reset'
+]);
+
+export const wasResponseIssue = typeCheck([
+ 'server-unparseable',
+ 'connection-reset'
+]);
+
+export const wasTimeout = typeCheck([
+ 'client-timeout',
+ 'server-timeout'
+]);
+
+export const isWhitelistable = typeCheck([
+ 'untrusted',
+ 'expired',
+ 'not-yet-valid',
+ 'wrong-host',
+ 'tls-error'
+]);
+
+export const isMockable = typeCheck([
+ 'host-not-found',
+ 'host-unreachable',
+ 'dns-error',
+ 'connection-refused',
+ 'connection-reset',
+ 'server-timeout'
+]);
+
+export function tagsToErrorType(tags: string[]): ErrorType | undefined {
+ if (
+ tags.includes("passthrough-error:SELF_SIGNED_CERT_IN_CHAIN") ||
+ tags.includes("passthrough-error:DEPTH_ZERO_SELF_SIGNED_CERT") ||
+ tags.includes("passthrough-error:UNABLE_TO_VERIFY_LEAF_SIGNATURE") ||
+ tags.includes("passthrough-error:UNABLE_TO_GET_ISSUER_CERT_LOCALLY")
+ ) {
+ return 'untrusted';
+ }
+
+ if (tags.includes("passthrough-error:CERT_HAS_EXPIRED")) return 'expired';
+ if (tags.includes("passthrough-error:CERT_NOT_YET_VALID")) return 'not-yet-valid';
+ if (tags.includes("passthrough-error:ERR_TLS_CERT_ALTNAME_INVALID")) return 'wrong-host';
+
+ if (
+ tags.filter(t => t.startsWith("passthrough-tls-error:")).length > 0 ||
+ tags.includes("passthrough-error:EPROTO") ||
+ tags.includes("passthrough-error:ERR_SSL_WRONG_VERSION_NUMBER") ||
+ tags.includes("passthrough-error:ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC") ||
+ tags.includes("passthrough-error:ERR_SSL_CIPHER_OPERATION_FAILED") ||
+ tags.includes("passthrough-error:ERR_SSL_BAD_RECORD_TYPE") ||
+ tags.includes("passthrough-error:ERR_SSL_INTERNAL_ERROR")
+ ) {
+ return 'tls-error';
+ }
+
+ if (tags.includes("passthrough-error:ENOTFOUND")) return 'host-not-found';
+ if (
+ tags.includes("passthrough-error:EHOSTUNREACH") || // No known route to this host
+ tags.includes("passthrough-error:ENETUNREACH") // Whole network is unreachable
+ ) return 'host-unreachable';
+ if (tags.includes("passthrough-error:EAI_AGAIN")) return 'dns-error';
+ if (tags.includes("passthrough-error:ECONNREFUSED")) return 'connection-refused';
+ if (tags.includes("passthrough-error:ECONNRESET")) return 'connection-reset';
+ if (tags.includes("passthrough-error:ETIMEDOUT")) return 'server-timeout';
+ if (
+ tags.includes("passthrough-error:HPE_INVALID_CONSTANT") ||
+ tags.includes("passthrough-error:ERR_INVALID_HTTP_TOKEN") ||
+ tags.includes("passthrough-error:ERR_HTTP_INVALID_STATUS_CODE") ||
+ tags.includes("passthrough-error:ERR_INVALID_CHAR")
+ ) {
+ return 'server-unparseable';
+ }
+
+ if (tags.includes("http-2") || tags.includes("client-error:HPE_INVALID_VERSION")) {
+ return 'invalid-http-version';
+ }
+
+ if (tags.includes("client-error:HPE_INVALID_METHOD")) return 'invalid-method'; // QWE / HTTP/1.1
+ if (tags.includes("client-error:HPE_INVALID_URL")) return 'client-unparseable-url'; // http://
+ if (
+ tags.includes("client-error:HPE_INVALID_CONSTANT") || // GET / HTTQ <- incorrect constant char
+ tags.includes("client-error:HPE_INVALID_EOF_STATE") // Unexpected 0-length packet in parser
+ ) return 'client-unparseable'; // ABC/1.1
+ if (tags.includes("client-error:HPE_HEADER_OVERFLOW")) return 'header-overflow'; // More than ~80KB of headers
+ if (
+ tags.includes("client-error:HPE_INVALID_CONTENT_LENGTH") ||
+ tags.includes("client-error:HPE_INVALID_TRANSFER_ENCODING") ||
+ tags.includes("client-error:HPE_INVALID_HEADER_TOKEN") || // Invalid received (req or res) headers
+ tags.includes("client-error:HPE_UNEXPECTED_CONTENT_LENGTH") || // T-E with C-L
+ tags.includes("passthrough-error:HPE_INVALID_HEADER_TOKEN") // Invalid headers upstream, e.g. after breakpoint
+ ) return 'invalid-headers';
+
+ if (tags.includes("client-error:ERR_HTTP_REQUEST_TIMEOUT")) return 'client-timeout';
+ if (
+ tags.includes("client-error:ECONNABORTED") ||
+ tags.includes("client-error:EPIPE")
+ ) return 'client-abort';
+
+ if (
+ tags.filter(t => t.startsWith("passthrough-error:")).length > 0 ||
+ tags.filter(t => t.startsWith("client-error:")).length > 0
+ ) {
+ logError(`Unrecognized error tag ${JSON.stringify(tags)}`);
+ return 'unknown';
+ }
+}
\ No newline at end of file
diff --git a/src/util/index.ts b/src/util/index.ts
index 2b0dfd4f..1ef3d52c 100644
--- a/src/util/index.ts
+++ b/src/util/index.ts
@@ -21,6 +21,10 @@ export function firstMatch(...tests: Array | R | undefined>): R | und
}
}
+export function typeCheck(types: readonly T[]) {
+ return (type: string): type is T => types.includes(type as T);
+}
+
export function longestPrefix(baseString: string, ...strings: string[]) {
let prefix = "";
const shortestLength = Math.min(