Skip to content

Commit bd3d9b5

Browse files
committed
Add support for pasting curl commands into the Send page
1 parent 5477f29 commit bd3d9b5

File tree

7 files changed

+100
-14
lines changed

7 files changed

+100
-14
lines changed

package-lock.json

Lines changed: 31 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"ajv-formats": "^2.1.1",
8888
"auth0-js": "^9.13.2",
8989
"base64-arraybuffer": "^0.2.0",
90+
"curl-as-har-request": "^0.1.0",
9091
"d3-scale": "^4.0.2",
9192
"date-fns": "^1.30.1",
9293
"dedent": "^0.7.0",

src/components/send/request-pane.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as _ from 'lodash';
22
import * as React from 'react';
3-
import { action, flow, observable, reaction } from 'mobx';
3+
import { action, reaction } from 'mobx';
44
import { disposeOnUnmount, inject, observer } from 'mobx-react';
55
import * as portals from 'react-reverse-portal';
6+
import * as HarFormat from 'har-format';
67

78
import { RawHeaders } from '../../types';
89

@@ -57,7 +58,8 @@ export class RequestPane extends React.Component<{
5758

5859
requestInput: RequestInput,
5960
sendRequest: () => void,
60-
isSending: boolean
61+
isSending: boolean,
62+
updateFromHar: (harRequest: HarFormat.Request) => void
6163
}> {
6264

6365
get cardProps() {
@@ -103,6 +105,7 @@ export class RequestPane extends React.Component<{
103105
updateUrl={this.updateUrl}
104106
isSending={isSending}
105107
sendRequest={sendRequest}
108+
updateFromHar={this.props.updateFromHar}
106109
/>
107110
<SendRequestHeadersCard
108111
{...this.cardProps.requestHeaders}

src/components/send/send-page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
2+
import { action } from 'mobx';
23
import { inject, observer } from 'mobx-react';
34
import * as portals from 'react-reverse-portal';
5+
import * as HarFormat from 'har-format';
46

57
import { styled } from '../../styles';
68
import { useHotkeys } from '../../util/ui';
@@ -9,6 +11,7 @@ import { WithInjected } from '../../types';
911
import { ApiError } from '../../services/server-api-types';
1012
import { SendStore } from '../../model/send/send-store';
1113
import { UiStore } from '../../model/ui/ui-store';
14+
import { buildRequestInputFromHarRequest } from '../../model/send/send-request-model';
1215

1316
import { ContainerSizedEditor } from '../editor/base-editor';
1417

@@ -147,6 +150,7 @@ class SendPage extends React.Component<{
147150
selectedRequest.pendingSend?.promise.state === 'pending'
148151
}
149152
editorNode={this.requestEditorNode}
153+
updateFromHar={this.updateFromHar}
150154
/>
151155
<ResponsePane
152156
requestInput={selectedRequest.request}
@@ -170,6 +174,12 @@ class SendPage extends React.Component<{
170174
</SendPageContainer>;
171175
}
172176

177+
@action.bound
178+
updateFromHar(harRequest: HarFormat.Request) {
179+
const { selectedRequest } = this.props.sendStore;
180+
selectedRequest.request = buildRequestInputFromHarRequest(harRequest);
181+
}
182+
173183
}
174184

175185
// Annoying cast required to handle the store prop nicely in our types

src/components/send/send-request-line.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as React from 'react';
22
import { Method } from 'mockttp';
3+
import * as HarFormat from 'har-format';
4+
import { parseCurlCommand } from 'curl-as-har-request';
35

46
import { styled } from '../../styles';
57
import { Icon, ArrowIcon } from '../../icons';
@@ -110,6 +112,8 @@ const SendButton = styled(Button)`
110112
`;
111113

112114
export const SendRequestLine = (props: {
115+
updateFromHar: (harRequest: HarFormat.Request) => void;
116+
113117
method: string;
114118
updateMethod: (method: string) => void;
115119

@@ -138,6 +142,28 @@ export const SendRequestLine = (props: {
138142
return false;
139143
}, [props.sendRequest]);
140144

145+
const onPaste = React.useCallback((event: React.ClipboardEvent<HTMLInputElement>) => {
146+
const pastedText = event.clipboardData.getData('text/plain');
147+
if (pastedText.match(/^\s*curl /)) {
148+
event.preventDefault();
149+
150+
// Looks like you've pasted a curl command, try to parse it to HAR, then
151+
// generate a request from there:
152+
try {
153+
// For now we use the first command, we could pop into multiple tabs if
154+
// we want to support multiple commands here later:
155+
const harRequest = parseCurlCommand(pastedText)[0];
156+
if (!harRequest.url) {
157+
throw new Error('Could not extract URL from pasted curl command.');
158+
}
159+
props.updateFromHar(harRequest);
160+
} catch (e: any) {
161+
console.log(e);
162+
alert(`Could not parse pasted curl command:\n\n${e.message || e}`);
163+
}
164+
}
165+
}, [props.updateFromHar]);
166+
141167
const borderColor = getMethodColor(props.method);
142168

143169
return <SendRequestLineContainer
@@ -164,12 +190,13 @@ export const SendRequestLine = (props: {
164190
<UrlInput
165191
type='url'
166192
spellCheck='false'
167-
placeholder='https://example.com/hello?name=world'
193+
placeholder='https://example.com/hello?name=world or paste a cURL command'
168194
required={true}
169195

170196
value={props.url}
171197
onFocus={prepopulateUrl}
172198
onChange={updateUrlFromEvent}
199+
onPaste={onPaste}
173200
/>
174201
<SendButton
175202
type='submit'

src/model/http/har.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ function cleanRawHarData(harContents: any) {
731731
return harContents;
732732
}
733733

734-
function parseHarRequest(
734+
export function parseHarRequest(
735735
id: string,
736736
request: ExtendedHarRequest,
737737
timingEvents: TimingEvents

src/model/send/send-request-model.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import * as Mockttp from 'mockttp';
22
import * as serializr from 'serializr';
33
import { observable } from 'mobx';
4+
import * as HarFormat from 'har-format';
45

56
import { HttpExchange, RawHeaders, HttpExchangeView } from "../../types";
67
import { ObservablePromise } from '../../util/observable';
7-
import { h2HeadersToH1 } from '../http/headers';
88

9-
import { EditableContentType, getEditableContentTypeFromViewable } from "../events/content-types";
9+
import { EditableContentType, getEditableContentType, getEditableContentTypeFromViewable } from "../events/content-types";
1010
import { EditableBody } from '../http/editable-body';
1111
import {
1212
syncBodyToContentLength,
1313
syncFormattingToContentType,
1414
syncUrlToHeaders
1515
} from '../http/editable-request-parts';
16+
import { getHeaderValue, h2HeadersToH1 } from '../http/headers';
17+
import { parseHarRequest } from '../http/har';
1618

1719
// This is our model of a Request for sending. Smilar to the API model,
1820
// but not identical, as we add extra UI metadata etc.
@@ -132,6 +134,26 @@ export async function buildRequestInputFromExchange(exchange: HttpExchangeView):
132134
});
133135
}
134136

137+
export function buildRequestInputFromHarRequest(requestData: HarFormat.Request): RequestInput {
138+
const harRequest = parseHarRequest('', requestData, {} as any);
139+
140+
let headers = harRequest.rawHeaders;
141+
if (parseInt(harRequest.httpVersion.split('.')[0], 10) >= 2) {
142+
headers = h2HeadersToH1(headers, harRequest.method);
143+
}
144+
145+
return new RequestInput({
146+
method: harRequest.method,
147+
url: harRequest.url,
148+
headers: headers,
149+
requestContentType: getEditableContentType(
150+
getHeaderValue(harRequest.headers, 'content-type')
151+
?? 'application/octet-stream'
152+
) ?? 'text',
153+
rawBody: harRequest.body.decoded
154+
});
155+
}
156+
135157
// These are the types that the sever client API expects. They are _not_ the same as
136158
// the Input type above, which is more flexible and includes various UI concerns that
137159
// we don't need to share with the server to actually send the request.

0 commit comments

Comments
 (0)