Skip to content

[INS-1786] WebSocket headers tab #5080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/insomnia-smoke-test/server/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { WebSocketServer } from 'ws';
export function startWebsocketServer(server: Server) {
const wsServer = new WebSocketServer({ server });

wsServer.on('connection', ws => {
wsServer.on('connection', (ws, req) => {
console.log('WebSocket connection was opened');

console.log('Upgrade headers:', req.headers);
ws.on('message', (message, isBinary) => {
if (isBinary) {
ws.send(message);
Expand Down
15 changes: 11 additions & 4 deletions packages/insomnia/src/main/network/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { v4 as uuidV4 } from 'uuid';
import {
CloseEvent,
ErrorEvent,
Event as OpenEvent,
Event,
MessageEvent,
WebSocket,
} from 'ws';

import { websocketRequest } from '../../models';
import { BaseWebSocketRequest } from '../../models/websocket-request';

export interface WebSocketConnection extends WebSocket {
_id: string;
requestId: string;
}

export type WebsocketOpenEvent = Omit<OpenEvent, 'target'> & {
export type WebsocketOpenEvent = Omit<Event, 'target'> & {
_id: string;
requestId: string;
type: 'open';
Expand Down Expand Up @@ -77,7 +78,13 @@ async function createWebSocketConnection(
const eventChannel = `webSocketRequest.connection.${request._id}.event`;
const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`;

const ws = new WebSocket(request?.url);
// @TODO: Render nunjucks tags in these headers
const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) =>
({ ...acc, [name.toLowerCase() || '']: value || '' });
const headers = request.headers.filter(({ value, disabled }) => !!value && !disabled)
.reduce(reduceArrayToLowerCaseKeyedDictionary, {});

const ws = new WebSocket(request?.url, { headers });
WebSocketConnections.set(options.requestId, ws);

ws.addEventListener('open', () => {
Expand Down Expand Up @@ -170,6 +177,7 @@ async function sendWebSocketEvent(
}

ws.send(options.message, error => {
// @TODO: Render nunjucks tags in these messages
// @TODO: We might want to set a status in the WebsocketMessageEvent
// and update it here based on the error. e.g. status = 'sending' | 'sent' | 'error'
if (error) {
Expand Down Expand Up @@ -204,7 +212,6 @@ async function closeWebSocketConnection(
) {
const ws = WebSocketConnections.get(options.requestId);
if (!ws) {
console.warn('No websocket found for requestId: ' + options.requestId);
return;
}
ws.close();
Expand Down
19 changes: 8 additions & 11 deletions packages/insomnia/src/models/helpers/request-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { isWebSocketRequest, isWebSocketRequestId, WebSocketRequest } from '../w
export function getById(requestId: string): Promise<Request | GrpcRequest | WebSocketRequest | null> {
if (isGrpcRequestId(requestId)) {
return models.grpcRequest.getById(requestId);
} else if (isWebSocketRequestId(requestId)) {
}
if (isWebSocketRequestId(requestId)) {
return models.websocketRequest.getById(requestId);
} else {
return models.request.getById(requestId);
}
return models.request.getById(requestId);
}

export function remove(request: Request | GrpcRequest | WebSocketRequest) {
Expand All @@ -19,9 +19,8 @@ export function remove(request: Request | GrpcRequest | WebSocketRequest) {
}
if (isWebSocketRequest(request)) {
return models.websocketRequest.remove(request);
} else {
return models.request.remove(request);
}
return models.request.remove(request);
}

export function update<T extends object>(request: T, patch: Partial<T> = {}): Promise<T> {
Expand All @@ -34,10 +33,9 @@ export function update<T extends object>(request: T, patch: Partial<T> = {}): Pr
if (isWebSocketRequest(request)) {
// @ts-expect-error -- TSCONVERSION
return models.websocketRequest.update(request, patch);
} else {
// @ts-expect-error -- TSCONVERSION
return models.request.update(request, patch);
}
// @ts-expect-error -- TSCONVERSION
return models.request.update(request, patch);
}

export function duplicate<T extends object>(request: T, patch: Partial<T> = {}): Promise<T> {
Expand All @@ -50,8 +48,7 @@ export function duplicate<T extends object>(request: T, patch: Partial<T> = {}):
if (isWebSocketRequest(request)) {
// @ts-expect-error -- TSCONVERSION
return models.websocketRequest.duplicate(request, patch);
} else {
// @ts-expect-error -- TSCONVERSION
return models.request.duplicate(request, patch);
}
// @ts-expect-error -- TSCONVERSION
return models.request.duplicate(request, patch);
}
4 changes: 4 additions & 0 deletions packages/insomnia/src/models/websocket-request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { database } from '../common/database';
import type { BaseModel } from '.';
import { RequestHeader } from './request';

export const name = 'WebSocket Request';

Expand All @@ -9,12 +10,14 @@ export const prefix = 'ws-req';

export const canDuplicate = true;

// @TODO: enable this at some point
export const canSync = false;

export interface BaseWebSocketRequest {
name: string;
url: string;
metaSortKey: number;
headers: RequestHeader[];
}

export type WebSocketRequest = BaseModel & BaseWebSocketRequest & { type: typeof type };
Expand All @@ -31,6 +34,7 @@ export const init = (): BaseWebSocketRequest => ({
name: 'New WebSocket Request',
url: '',
metaSortKey: -1 * Date.now(),
headers: [],
});

export const migrate = (doc: WebSocketRequest) => doc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ export class OneLineEditor extends PureComponent<Props, State> {
getAutocompleteConstants={getAutocompleteConstants}
className={classnames('editor--single-line', className)}
defaultValue={defaultValue}
readOnly={this.props.readOnly}
/>
</Fragment>
);
Expand All @@ -402,6 +403,7 @@ export class OneLineEditor extends PureComponent<Props, State> {
}}
placeholder={placeholder}
defaultValue={defaultValue}
disabled={this.props.readOnly}
onBlur={this._handleInputBlur}
onChange={this._handleInputChange}
onMouseEnter={this._handleInputMouseEnter}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import React, { FC, useCallback } from 'react';
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
import { update } from '../../../models/helpers/request-operations';
import type { Request, RequestHeader } from '../../../models/request';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { CodeEditor } from '../codemirror/code-editor';
import { KeyValueEditor } from '../key-value-editor/key-value-editor';

interface Props {
bulk: boolean;
request: Request;
isDisabled?: boolean;
request: Request | WebSocketRequest;
}

export const RequestHeadersEditor: FC<Props> = ({
request,
bulk,
isDisabled,
}) => {
const handleBulkUpdate = useCallback((headersString: string) => {
const headers: {
Expand Down Expand Up @@ -82,6 +85,8 @@ export const RequestHeadersEditor: FC<Props> = ({
handleGetAutocompleteNameConstants={getCommonHeaderNames}
handleGetAutocompleteValueConstants={getCommonHeaderValues}
onChange={onChangeHeaders}
isDisabled={isDisabled}
isWebSocketRequest={isWebSocketRequest(request)}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ interface Props {
onDelete?: Function;
onCreate?: Function;
className?: string;
isDisabled?: boolean;
isWebSocketRequest?: boolean;
}

interface State {
Expand Down Expand Up @@ -405,7 +407,7 @@ export class KeyValueEditor extends PureComponent<Props, State> {

_setFocusedPair(pair: Pair) {
if (pair) {
this._focusedPairId = pair.id;
this._focusedPairId = pair.id || 'n/a';
} else {
this._focusedPairId = null;
}
Expand Down Expand Up @@ -435,11 +437,35 @@ export class KeyValueEditor extends PureComponent<Props, State> {
allowMultiline,
sortable,
disableDelete,
isDisabled,
isWebSocketRequest,
} = this.props;
const { pairs } = this.state;

const classes = classnames('key-value-editor', 'wide', className);
const hasMaxPairsAndNotExceeded = !maxPairs || pairs.length < maxPairs;
const showNewHeaderInput = !isDisabled && hasMaxPairsAndNotExceeded;
const readOnlyPairs = [
{ name: 'Connection', value: 'Upgrade' },
{ name: 'Upgrade', value: 'websocket' },
{ name: 'Sec-WebSocket-Key', value: '<calculated at runtime>' },
{ name: 'Sec-WebSocket-Version', value: '13' },
{ name: 'Sec-WebSocket-Extensions', value: 'permessage-deflate; client_max_window_bits' },
];
return (
<ul className={classes}>
{isWebSocketRequest ? readOnlyPairs.map((pair, i) => (
<Row
key={i}
index={i}
displayDescription={this.state.displayDescription}
descriptionPlaceholder={descriptionPlaceholder}
readOnly
hideButtons
forceInput
pair={pair}
/>
)) : null}
{pairs.map((pair, i) => (
<Row
noDelete={disableDelete}
Expand All @@ -466,17 +492,20 @@ export class KeyValueEditor extends PureComponent<Props, State> {
handleGetAutocompleteValueConstants={handleGetAutocompleteValueConstants}
allowMultiline={allowMultiline}
allowFile={allowFile}
readOnly={isDisabled}
hideButtons={isDisabled}
// @TODO disabled nunjucks: remove this when nunjucks support is added
forceInput={isWebSocketRequest}
pair={pair}
/>
))}

{!maxPairs || pairs.length < maxPairs ? (
{showNewHeaderInput ? (
<Row
key="empty-row"
hideButtons
sortable
noDropZone
readOnly
forceInput
index={-1}
onChange={noop}
Expand All @@ -502,11 +531,9 @@ export class KeyValueEditor extends PureComponent<Props, State> {
onFocusDescription={this._handleAddFromDescription}
allowMultiline={allowMultiline}
allowFile={allowFile}
// @ts-expect-error -- TSCONVERSION missing defaults
pair={{
name: '',
value: '',
description: '',
}}
/>
) : null}
Expand Down
20 changes: 10 additions & 10 deletions packages/insomnia/src/ui/components/key-value-editor/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import { CodePromptModal } from '../modals/code-prompt-modal';
import { showModal } from '../modals/index';

export interface Pair {
id: string;
id?: string;
name: string;
value: string;
description: string;
description?: string;
fileName?: string;
type?: string;
disabled?: boolean;
Expand All @@ -34,11 +34,11 @@ export type AutocompleteHandler = (pair: Pair) => string[] | PromiseLike<string[
type DragDirection = 0 | 1 | -1;

interface Props {
onChange: (pair: Pair) => void;
onDelete: (pair: Pair) => void;
onFocusName: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
onFocusValue: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
onFocusDescription: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
onChange?: (pair: Pair) => void;
onDelete?: (pair: Pair) => void;
onFocusName?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
onFocusValue?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
onFocusDescription?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
displayDescription: boolean;
index: number;
pair: Pair;
Expand Down Expand Up @@ -195,15 +195,15 @@ class KeyValueEditorRowInternal extends PureComponent<Props, State> {
}

_handleFocusName(event: FocusEvent | React.FocusEvent<Element, Element>) {
this.props.onFocusName(this.props.pair, event);
this.props.onFocusName?.(this.props.pair, event);
}

_handleFocusValue(event: FocusEvent | React.FocusEvent<Element, Element>) {
this.props.onFocusValue(this.props.pair, event);
this.props.onFocusValue?.(this.props.pair, event);
}

_handleFocusDescription(event: FocusEvent | React.FocusEvent<Element, Element>) {
this.props.onFocusDescription(this.props.pair, event);
this.props.onFocusDescription?.(this.props.pair, event);
}

_handleBlurName(event: FocusEvent | React.FocusEvent<Element, Element>) {
Expand Down
34 changes: 10 additions & 24 deletions packages/insomnia/src/ui/components/websockets/action-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React, { FC, FormEvent, useEffect } from 'react';
import { useSelector } from 'react-redux';
import React, { ChangeEvent, FC, FormEvent, useEffect } from 'react';
import styled from 'styled-components';

import * as models from '../../../models';
import { WebSocketRequest } from '../../../models/websocket-request';
import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { ReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { useWebSocketClient } from '../../context/websocket-client/websocket-client-context';
import { selectActiveRequest } from '../../redux/selectors';

const Button = styled.button({
paddingRight: 'var(--padding-md)',
Expand Down Expand Up @@ -52,6 +48,9 @@ const ActionButton: FC<ActionButtonProps> = ({ requestId, readyState }) => {

interface ActionBarProps {
requestId: string;
defaultValue: string;
readyState: ReadyState;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

const Form = styled.form({
Expand All @@ -75,25 +74,11 @@ const WebSocketIcon = styled.span({
paddingLeft: 'var(--padding-md)',
});

export const WebsocketActionBar: FC<ActionBarProps> = ({ requestId }) => {
const request = useSelector(selectActiveRequest);

export const WebSocketActionBar: FC<ActionBarProps> = ({ requestId, defaultValue, onChange, readyState }) => {
const { create, close } = useWebSocketClient();
const readyState = useWSReadyState(requestId);

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const url = (formData.get('websocketUrlInput') as string) || '';

if (!request) {
return;
}

if (request.url !== url) {
await models.websocketRequest.update(request as WebSocketRequest, { url });
}

create({ requestId });
};

Expand All @@ -111,8 +96,9 @@ export const WebsocketActionBar: FC<ActionBarProps> = ({ requestId }) => {
name="websocketUrlInput"
disabled={readyState === ReadyState.OPEN}
required
placeholder="wss://ws-feed.exchange.coinbase.com"
defaultValue={request?.url}
placeholder="wss://example.com/chat"
defaultValue={defaultValue}
onChange={onChange}
/>
</Form>
<ActionButton requestId={requestId} readyState={readyState} />
Expand Down
Loading