diff --git a/packages/insomnia-smoke-test/server/websocket.ts b/packages/insomnia-smoke-test/server/websocket.ts
index edbdecfd2de..de19084886a 100644
--- a/packages/insomnia-smoke-test/server/websocket.ts
+++ b/packages/insomnia-smoke-test/server/websocket.ts
@@ -45,13 +45,26 @@ Location: ws://localhost:4010
`);
return;
};
+const return401withBody = (socket: Socket) => {
+ socket.end(`HTTP/1.1 401 Unauthorized
+
+
+
+
+
+
401 Unauthorized
+
+
+ `);
+ return;
+};
const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, head: Buffer) => {
if (request.url === '/redirect') {
return redirectOnSuccess(socket);
}
if (request.url === '/bearer') {
if (request.headers.authorization !== 'Bearer insomnia-cool-token-!!!1112113243111') {
- socket.end('HTTP/1.1 401 Unauthorized\n\n');
+ return401withBody(socket);
return;
}
return redirectOnSuccess(socket);
@@ -59,7 +72,7 @@ const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket,
if (request.url === '/basic-auth') {
// login with user:password
if (request.headers.authorization !== 'Basic dXNlcjpwYXNzd29yZA==') {
- socket.end('HTTP/1.1 401 Unauthorized\n\n');
+ return401withBody(socket);
return;
}
return redirectOnSuccess(socket);
diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts
index 118fa82b73c..cbb9e238428 100644
--- a/packages/insomnia/src/common/constants.ts
+++ b/packages/insomnia/src/common/constants.ts
@@ -562,6 +562,7 @@ export const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__';
export const EXPORT_TYPE_REQUEST = 'request';
export const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request';
export const EXPORT_TYPE_WEBSOCKET_REQUEST = 'websocket_request';
+export const EXPORT_TYPE_WEBSOCKET_PAYLOAD = 'websocket_payload';
export const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
export const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite';
export const EXPORT_TYPE_UNIT_TEST = 'unit_test';
diff --git a/packages/insomnia/src/common/export.ts b/packages/insomnia/src/common/export.ts
index c7403f1d925..073740ea559 100644
--- a/packages/insomnia/src/common/export.ts
+++ b/packages/insomnia/src/common/export.ts
@@ -14,6 +14,7 @@ import { isRequest } from '../models/request';
import { isRequestGroup } from '../models/request-group';
import { isUnitTest } from '../models/unit-test';
import { isUnitTestSuite } from '../models/unit-test-suite';
+import { isWebSocketPayload } from '../models/websocket-payload';
import { isWebSocketRequest } from '../models/websocket-request';
import { isWorkspace, Workspace } from '../models/workspace';
import { resetKeys } from '../sync/ignore-keys';
@@ -29,6 +30,7 @@ import {
EXPORT_TYPE_REQUEST_GROUP,
EXPORT_TYPE_UNIT_TEST,
EXPORT_TYPE_UNIT_TEST_SUITE,
+ EXPORT_TYPE_WEBSOCKET_PAYLOAD,
EXPORT_TYPE_WEBSOCKET_REQUEST,
EXPORT_TYPE_WORKSPACE,
getAppVersion,
@@ -176,7 +178,8 @@ export async function exportRequestsData(
isUnitTestSuite(d) ||
isUnitTest(d) ||
isProtoFile(d) ||
- isProtoDirectory(d)
+ isProtoDirectory(d) ||
+ isWebSocketPayload(d)
);
});
docs.push(...descendants);
@@ -190,6 +193,7 @@ export async function exportRequestsData(
isUnitTestSuite(d) ||
isUnitTest(d) ||
isRequest(d) ||
+ isWebSocketPayload(d) ||
isWebSocketRequest(d) ||
isGrpcRequest(d) ||
isRequestGroup(d) ||
@@ -234,6 +238,9 @@ export async function exportRequestsData(
} else if (isGrpcRequest(d)) {
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
d._type = EXPORT_TYPE_GRPC_REQUEST;
+ } else if (isWebSocketPayload(d)) {
+ // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
+ d._type = EXPORT_TYPE_WEBSOCKET_PAYLOAD;
} else if (isWebSocketRequest(d)) {
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
d._type = EXPORT_TYPE_WEBSOCKET_REQUEST;
diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts
index 32f874e96e4..8247706a815 100644
--- a/packages/insomnia/src/main/network/websocket.ts
+++ b/packages/insomnia/src/main/network/websocket.ts
@@ -238,6 +238,9 @@ const createWebSocketConnection = async (
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
});
ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
+ incomingMessage.on('data', chunk => {
+ timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: chunk.toString(), name: 'DataOut', timestamp: Date.now() }) + '\n');
+ });
// @ts-expect-error -- private property
const internalRequestHeader = clientRequest._header;
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
diff --git a/packages/insomnia/src/models/index.ts b/packages/insomnia/src/models/index.ts
index e8b674f925b..f3249af3e85 100644
--- a/packages/insomnia/src/models/index.ts
+++ b/packages/insomnia/src/models/index.ts
@@ -9,6 +9,7 @@ import {
EXPORT_TYPE_REQUEST_GROUP,
EXPORT_TYPE_UNIT_TEST,
EXPORT_TYPE_UNIT_TEST_SUITE,
+ EXPORT_TYPE_WEBSOCKET_PAYLOAD,
EXPORT_TYPE_WEBSOCKET_REQUEST,
EXPORT_TYPE_WORKSPACE,
} from '../common/constants';
@@ -36,6 +37,7 @@ import * as _stats from './stats';
import * as _unitTest from './unit-test';
import * as _unitTestResult from './unit-test-result';
import * as _unitTestSuite from './unit-test-suite';
+import * as _webSocketPayload from './websocket-payload';
import * as _webSocketRequest from './websocket-request';
import * as _webSocketResponse from './websocket-response';
import * as _workspace from './workspace';
@@ -78,6 +80,7 @@ export const protoFile = _protoFile;
export const protoDirectory = _protoDirectory;
export const grpcRequest = _grpcRequest;
export const grpcRequestMeta = _grpcRequestMeta;
+export const webSocketPayload = _webSocketPayload;
export const webSocketRequest = _webSocketRequest;
export const webSocketResponse = _webSocketResponse;
export const workspace = _workspace;
@@ -113,6 +116,7 @@ export function all() {
protoDirectory,
grpcRequest,
grpcRequestMeta,
+ webSocketPayload,
webSocketRequest,
webSocketResponse,
] as const;
@@ -214,6 +218,7 @@ export async function initModel(type: string, ...sources: R
export const MODELS_BY_EXPORT_TYPE: Record = {
[EXPORT_TYPE_REQUEST]: request,
+ [EXPORT_TYPE_WEBSOCKET_PAYLOAD]: webSocketPayload,
[EXPORT_TYPE_WEBSOCKET_REQUEST]: webSocketRequest,
[EXPORT_TYPE_GRPC_REQUEST]: grpcRequest,
[EXPORT_TYPE_REQUEST_GROUP]: requestGroup,
diff --git a/packages/insomnia/src/models/websocket-payload.ts b/packages/insomnia/src/models/websocket-payload.ts
new file mode 100644
index 00000000000..80d28a17c7b
--- /dev/null
+++ b/packages/insomnia/src/models/websocket-payload.ts
@@ -0,0 +1,70 @@
+import { database } from '../common/database';
+import type { BaseModel } from '.';
+
+export const name = 'WebSocket Payload';
+
+export const type = 'WebSocketPayload';
+
+export const prefix = 'ws-payload';
+
+export const canDuplicate = true;
+
+// @TODO: enable this at some point
+export const canSync = false;
+
+export interface BaseWebSocketPayload {
+ value: string;
+ mode: string;
+}
+
+export type WebSocketPayload = BaseModel & BaseWebSocketPayload & { type: typeof type };
+
+export const isWebSocketPayload = (model: Pick): model is WebSocketPayload => (
+ model.type === type
+);
+
+export const isWebSocketPayloadId = (id: string | null) => (
+ id?.startsWith(`${prefix}_`)
+);
+
+export const init = (): BaseWebSocketPayload => ({
+ value: '',
+ mode: 'application/json',
+});
+
+export const migrate = (doc: WebSocketPayload) => doc;
+
+export const create = (patch: Partial = {}) => {
+ if (!patch.parentId) {
+ throw new Error(`New WebSocketPayload missing \`parentId\`: ${JSON.stringify(patch)}`);
+ }
+
+ return database.docCreate(type, patch);
+};
+
+export const remove = (obj: WebSocketPayload) => database.remove(obj);
+
+export const update = (
+ obj: WebSocketPayload,
+ patch: Partial = {}
+) => database.docUpdate(obj, patch);
+
+export async function duplicate(request: WebSocketPayload, patch: Partial = {}) {
+ // Only set name and "(Copy)" if the patch does
+ // not define it and the request itself has a name.
+ // Otherwise leave it blank so the request URL can
+ // fill it in automatically.
+ if (!patch.name && request.name) {
+ patch.name = `${request.name} (Copy)`;
+ }
+
+ return database.duplicate(request, {
+ name,
+ ...patch,
+ });
+}
+
+export const getById = (_id: string) => database.getWhere(type, { _id });
+export const getByParentId = (parentId: string) => database.getWhere(type, { parentId });
+
+export const all = () => database.all(type);
diff --git a/packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx
similarity index 81%
rename from packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx
rename to packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx
index c0685a125c0..b8466b391c1 100644
--- a/packages/insomnia/src/ui/components/dropdowns/payload-type-dropdown.tsx
+++ b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx
@@ -5,17 +5,17 @@ import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownItem } from '../base/dropdown/dropdown-item';
interface Props {
- payloadType: string;
- onClick: (payloadType: string) => void;
+ previewMode: string;
+ onClick: (previewMode: string) => void;
}
-export const PayloadTypeDropdown: FC = ({ payloadType, onClick }) => {
+export const WebSocketPreviewModeDropdown: FC = ({ previewMode, onClick }) => {
return (
{{
[CONTENT_TYPE_JSON]: 'JSON',
[CONTENT_TYPE_PLAINTEXT]: 'Raw',
- }[payloadType]}
+ }[previewMode]}
diff --git a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx
index 544917624df..6b5112323a8 100644
--- a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx
+++ b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx
@@ -1,4 +1,4 @@
-import React, { FC, FormEvent, useRef, useState } from 'react';
+import React, { FC, FormEvent, useEffect, useRef, useState } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import styled from 'styled-components';
@@ -9,7 +9,7 @@ import { WebSocketRequest } from '../../../models/websocket-request';
import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor';
import { AuthDropdown } from '../dropdowns/auth-dropdown';
-import { PayloadTypeDropdown } from '../dropdowns/payload-type-dropdown';
+import { WebSocketPreviewModeDropdown } from '../dropdowns/websocket-preview-mode';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
import { RequestHeadersEditor } from '../editors/request-headers-editor';
import { showAlert, showModal } from '../modals';
@@ -52,16 +52,30 @@ const PaneHeader = styled(OriginalPaneHeader)({
interface FormProps {
request: WebSocketRequest;
- payloadType: string;
+ previewMode: string;
+ initialValue: string;
environmentId: string;
+ createOrUpdatePayload: (payload: string, mode: string) => Promise;
}
const WebSocketRequestForm: FC = ({
request,
- payloadType,
+ previewMode,
+ initialValue,
+ createOrUpdatePayload,
environmentId,
}) => {
const editorRef = useRef(null);
+ useEffect(() => {
+ let isMounted = true;
+ if (isMounted) {
+ editorRef.current?.codeMirror?.setValue(initialValue);
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [initialValue]);
+
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
const message = editorRef.current?.getValue() || '';
@@ -102,9 +116,10 @@ const WebSocketRequestForm: FC = ({
createOrUpdatePayload(value, previewMode)}
enableNunjucks
/>
@@ -125,13 +140,49 @@ interface Props {
// TODO: @gatzjames discuss above assertion in light of request and settings drills
export const WebSocketRequestPane: FC = ({ request, workspaceId, environmentId, forceRefreshKey }) => {
const readyState = useWSReadyState(request._id);
+
const disabled = readyState === ReadyState.OPEN || readyState === ReadyState.CLOSING;
const handleOnChange = (url: string) => {
if (url !== request.url) {
models.webSocketRequest.update(request, { url });
}
};
- const [payloadType, setPayloadType] = useState(CONTENT_TYPE_JSON);
+ const [previewMode, setPreviewMode] = useState(CONTENT_TYPE_JSON);
+ const [initialValue, setInitialValue] = useState('');
+
+ useEffect(() => {
+ let isMounted = true;
+ const fn = async () => {
+ const payload = await models.webSocketPayload.getByParentId(request._id);
+ if (isMounted && payload) {
+ setInitialValue(payload?.value || '');
+ setPreviewMode(payload.mode);
+ }
+ };
+ fn();
+ return () => {
+ isMounted = false;
+ };
+ }, [request._id]);
+
+ const changeMode = (mode: string) => {
+ setPreviewMode(mode);
+ createOrUpdatePayload(initialValue, mode);
+ };
+
+ const createOrUpdatePayload = async (value: string, mode: string) => {
+ // @TODO: multiple payloads
+ const payload = await models.webSocketPayload.getByParentId(request._id);
+ if (payload) {
+ await models.webSocketPayload.update(payload, { value, mode });
+ return;
+ }
+ await models.webSocketPayload.create({
+ parentId: request._id,
+ value,
+ mode,
+ });
+ };
const uniqueKey = `${forceRefreshKey}::${request._id}`;
@@ -151,7 +202,7 @@ export const WebSocketRequestPane: FC = ({ request, workspaceId, environm
-
+
= ({ request, workspaceId, environm
diff --git a/packages/insomnia/src/ui/redux/modules/entities.ts b/packages/insomnia/src/ui/redux/modules/entities.ts
index cd4c416e631..293f1c00f3b 100644
--- a/packages/insomnia/src/ui/redux/modules/entities.ts
+++ b/packages/insomnia/src/ui/redux/modules/entities.ts
@@ -27,6 +27,7 @@ import { Stats } from '../../../models/stats';
import { UnitTest } from '../../../models/unit-test';
import { UnitTestResult } from '../../../models/unit-test-result';
import { UnitTestSuite } from '../../../models/unit-test-suite';
+import { WebSocketPayload } from '../../../models/websocket-payload';
import { WebSocketRequest } from '../../../models/websocket-request';
import { WebSocketResponse } from '../../../models/websocket-response';
import { Workspace } from '../../../models/workspace';
@@ -72,6 +73,7 @@ export interface EntitiesState {
protoDirectories: EntityRecord;
grpcRequests: EntityRecord;
grpcRequestMetas: EntityRecord;
+ webSocketPayloads: EntityRecord;
webSocketRequests: EntityRecord;
webSocketResponses: EntityRecord;
}
@@ -102,6 +104,7 @@ export const initialEntitiesState: EntitiesState = {
protoDirectories: {},
grpcRequests: {},
grpcRequestMetas: {},
+ webSocketPayloads: {},
webSocketRequests: {},
webSocketResponses: {},
};
@@ -203,6 +206,7 @@ export async function allDocs() {
...(await models.protoDirectory.all()),
...(await models.grpcRequest.all()),
...(await models.grpcRequestMeta.all()),
+ ...(await models.webSocketPayload.all()),
...(await models.webSocketRequest.all()),
...(await models.webSocketResponse.all()),
];
diff --git a/packages/insomnia/src/ui/redux/sidebar-selectors.ts b/packages/insomnia/src/ui/redux/sidebar-selectors.ts
index 54587c33796..69f2e1c08eb 100644
--- a/packages/insomnia/src/ui/redux/sidebar-selectors.ts
+++ b/packages/insomnia/src/ui/redux/sidebar-selectors.ts
@@ -21,7 +21,7 @@ export const shouldShowInSidebar = (model: BaseModel): boolean =>
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model) || isRequestGroup(model);
export const shouldIgnoreChildrenOf = (model: SidebarModel): boolean =>
- isRequest(model) || isGrpcRequest(model);
+ isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model);
export const sortByMetaKeyOrId = (a: SidebarModel, b: SidebarModel): number => {
if (a.metaSortKey === b.metaSortKey) {