Skip to content

Commit fbd57d4

Browse files
author
stephanbuettig
committed
fix: address all review feedback from PR #222
Changes addressing @pimterry's review: Architecture: - DI pattern for ZipExportController (testable without module stubs) - Shared sanitizer: simplifyHarEntryRequestForSnippetExport (single source of truth for snippet export sanitization, used by both single-export and ZIP-export paths) - Cooperative cancellation via MessageChannel + yieldToEventLoop() - callApi abort now rejects immediately (no 5-min hang if worker is stuck before first yield); exportAsZip translates AbortError back to cancelled response to preserve the public API contract Bug fixes: - Cancellation race: abortListener in callApi now calls finalize() + reject(AbortError) immediately, matching timeout handler behavior - Listener ordering: emitter.once registered before worker.postMessage to prevent latent race with synchronous worker responses - Type safety: replaced `undefined as any` with conditional spread in buildUltraSafeRequest Code quality: - All comments and debug logs translated to English (upstream PR ready) - formatBytes JSDoc corrected (SI-style labels, not IEC) - ZIP_DEBUG flag defaults to false Tests: - New: snippet-export-sanitization.spec.ts (hop-by-hop headers, empty query params, cookie clearing, postData.text preservation) - New: zip-export-service.spec.ts (stale-run invalidation, reset during in-flight run) - All 808 tests pass, 0 failures
1 parent 4107473 commit fbd57d4

17 files changed

Lines changed: 3348 additions & 436 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"date-fns": "^1.30.1",
9292
"dedent": "^0.7.0",
9393
"deserialize-error": "0.0.3",
94-
"dompurify": "^3.3.3",
94+
"dompurify": "^3.4.0",
9595
"fast-json-patch": "^3.1.1",
9696
"fast-xml-parser": "^5.5.7",
9797
"fflate": "^0.8.2",

src/components/view/http/http-export-card.tsx

Lines changed: 85 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import * as _ from 'lodash';
21
import React from "react";
3-
import { action, computed } from "mobx";
2+
import { action, computed, observable } from "mobx";
43
import { inject, observer } from "mobx-react";
54
import dedent from 'dedent';
65

7-
import { HttpExchangeView } from '../../../types';
6+
import { CollectedEvent, HttpExchangeView } from '../../../types';
87
import { styled } from '../../../styles';
98
import { Icon } from '../../../icons';
109
import { logError } from '../../../errors';
@@ -16,13 +15,11 @@ import {
1615
generateCodeSnippet,
1716
getCodeSnippetFormatKey,
1817
getCodeSnippetFormatName,
19-
getCodeSnippetOptionFromKey,
18+
getSafeCodeSnippetOptionFromKey,
2019
DEFAULT_SNIPPET_FORMAT_KEY,
2120
snippetExportOptions,
2221
SnippetOption
2322
} from '../../../model/ui/export';
24-
import { ZIP_ALL_FORMAT_KEY } from '../../../model/ui/snippet-formats';
25-
import { ZipDownloadPanel } from '../zip-download-panel';
2623

2724
import { ProHeaderPill, CardSalesPitch } from '../../account/pro-placeholders';
2825
import {
@@ -34,6 +31,7 @@ import { PillSelector, PillButton } from '../../common/pill';
3431
import { CopyButtonPill } from '../../common/copy-button';
3532
import { DocsLink } from '../../common/docs-link';
3633
import { SelfSizedEditor } from '../../editor/base-editor';
34+
import { ZipExportDialog } from '../zip-export-dialog';
3735

3836
interface ExportCardProps extends CollapsibleCardProps {
3937
exchange: HttpExchangeView;
@@ -138,116 +136,109 @@ const ExportHarPill = styled(observer((p: {
138136
margin-right: auto;
139137
`;
140138

141-
// Virtual SnippetOption used as the PillSelector value when ZIP is selected.
142-
// This is never passed to httpsnippet — it's only used for dropdown rendering.
143-
const ZIP_SNIPPET_OPTION: SnippetOption = {
144-
target: ZIP_ALL_FORMAT_KEY as any,
145-
client: '' as any,
146-
name: 'ZIP (Selected Formats)',
147-
description: 'Download selected code snippet formats in a single ZIP archive',
148-
link: ''
149-
};
150-
151-
// Build extended optGroups with ZIP at the top
152-
const exportOptionsWithZip: _.Dictionary<SnippetOption[]> = {
153-
'Archive': [ZIP_SNIPPET_OPTION],
154-
...snippetExportOptions
155-
};
156-
157-
const getExportFormatKey = (option: SnippetOption): string => {
158-
if (option === ZIP_SNIPPET_OPTION) return ZIP_ALL_FORMAT_KEY;
159-
return getCodeSnippetFormatKey(option);
160-
};
161-
162-
const getExportFormatName = (option: SnippetOption): string => {
163-
if (option === ZIP_SNIPPET_OPTION) return ZIP_SNIPPET_OPTION.name;
164-
return getCodeSnippetFormatName(option);
165-
};
166-
167139
@inject('accountStore')
168140
@inject('uiStore')
169141
@observer
170142
export class HttpExportCard extends React.Component<ExportCardProps> {
171143

144+
@observable
145+
private zipDialogOpen = false;
146+
147+
@action.bound
148+
private openZipDialog() { this.zipDialogOpen = true; }
149+
150+
@action.bound
151+
private closeZipDialog() { this.zipDialogOpen = false; }
152+
172153
render() {
173154
const { exchange, accountStore } = this.props;
174155
const isPaidUser = accountStore!.user.isPaidUser();
175-
const isZipSelected = this.isZipSelected;
176156

177-
return <CollapsibleCard {...this.props}>
178-
<header>
179-
{ isPaidUser
180-
? <ExportHarPill exchange={exchange} />
181-
: <ProHeaderPill />
182-
}
157+
return <>
158+
<CollapsibleCard {...this.props}>
159+
<header>
160+
{ isPaidUser
161+
? <>
162+
<ExportHarPill exchange={exchange} />
163+
{/*
164+
* ZIP PillButton is active immediately (even when
165+
* the card is collapsed). The click stops propagation
166+
* so a header click underneath does not inadvertently
167+
* toggle the card.
168+
*/}
169+
<PillButton
170+
onClick={(e) => {
171+
e.stopPropagation();
172+
this.openZipDialog();
173+
}}
174+
>
175+
<Icon icon={['fas', 'file-archive']} /> ZIP
176+
</PillButton>
177+
</>
178+
: <ProHeaderPill />
179+
}
183180

184-
<PillSelector<SnippetOption>
185-
onChange={this.setSnippetOption}
186-
value={this.currentDropdownValue}
187-
optGroups={exportOptionsWithZip}
188-
keyFormatter={getExportFormatKey}
189-
nameFormatter={getExportFormatName}
190-
/>
191-
192-
<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
193-
Export
194-
</CollapsibleCardHeading>
195-
</header>
196-
197-
{ isPaidUser ?
198-
<div>
199-
{ isZipSelected
200-
? <ZipDownloadPanel exchanges={[exchange]} />
201-
: <ExportSnippetEditor
181+
<PillSelector<SnippetOption>
182+
onChange={this.setSnippetOption}
183+
value={this.snippetOption}
184+
optGroups={snippetExportOptions}
185+
keyFormatter={getCodeSnippetFormatKey}
186+
nameFormatter={getCodeSnippetFormatName}
187+
/>
188+
189+
<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
190+
Export
191+
</CollapsibleCardHeading>
192+
</header>
193+
194+
{ isPaidUser ?
195+
<div>
196+
<ExportSnippetEditor
202197
exchange={exchange}
203198
exportOption={this.snippetOption}
204199
/>
205-
}
206-
</div>
207-
:
208-
<CardSalesPitch source='export'>
209-
<p>
210-
Instantly export requests as code, for languages and tools including cURL, wget, JS
211-
(XHR, Node HTTP, Request, ...), Python (native or Requests), Ruby, Java (OkHttp
212-
or Unirest), Go, PHP, Swift, HTTPie, and a whole lot more.
213-
</p>
214-
<p>
215-
Want to save the exchange itself? Export one or all requests as HAR (the{' '}
216-
<a href="https://en.wikipedia.org/wiki/.har">HTTP Archive Format</a>), to import
217-
and examine elsewhere, share with your team, or store for future reference.
218-
</p>
219-
</CardSalesPitch>
220-
}
221-
</CollapsibleCard>;
222-
}
223-
224-
@computed
225-
private get isZipSelected(): boolean {
226-
return (this.props.uiStore!.exportSnippetFormat || '') === ZIP_ALL_FORMAT_KEY;
227-
}
228-
229-
@computed
230-
private get currentDropdownValue(): SnippetOption {
231-
if (this.isZipSelected) return ZIP_SNIPPET_OPTION;
232-
return this.snippetOption;
200+
</div>
201+
:
202+
<CardSalesPitch source='export'>
203+
<p>
204+
Instantly export requests as code, for languages and tools including cURL, wget, JS
205+
(XHR, Node HTTP, Request, ...), Python (native or Requests), Ruby, Java (OkHttp
206+
or Unirest), Go, PHP, Swift, HTTPie, and a whole lot more.
207+
</p>
208+
<p>
209+
Want to save the exchange itself? Export one or all requests as HAR (the{' '}
210+
<a href="https://en.wikipedia.org/wiki/.har">HTTP Archive Format</a>), to import
211+
and examine elsewhere, share with your team, or store for future reference.
212+
</p>
213+
</CardSalesPitch>
214+
}
215+
</CollapsibleCard>
216+
{/*
217+
* Dialog intentionally rendered OUTSIDE the CollapsibleCard.
218+
* `CollapsibleCard.renderChildren()` discards all children
219+
* after child 0 when the card is collapsed; placed there the
220+
* dialog JSX would never appear in the DOM when the card is
221+
* closed. The modal component uses a portal internally anyway,
222+
* so its position in the React tree does not matter.
223+
*/}
224+
{this.zipDialogOpen && <ZipExportDialog
225+
events={[exchange as unknown as CollectedEvent]}
226+
onClose={this.closeZipDialog}
227+
titleSuffix='1 request'
228+
/>}
229+
</>;
233230
}
234231

235232
@computed
236233
private get snippetOption(): SnippetOption {
237234
let exportSnippetFormat = this.props.uiStore!.exportSnippetFormat ||
238235
DEFAULT_SNIPPET_FORMAT_KEY;
239-
// If ZIP is selected, fall back to default for the snippet option
240-
if (exportSnippetFormat === ZIP_ALL_FORMAT_KEY) {
241-
exportSnippetFormat = DEFAULT_SNIPPET_FORMAT_KEY;
242-
}
243-
// Guard: if the format key doesn't resolve (e.g. deleted/invalid key),
244-
// fall back to the default cURL option
245-
return getCodeSnippetOptionFromKey(exportSnippetFormat)
246-
?? getCodeSnippetOptionFromKey(DEFAULT_SNIPPET_FORMAT_KEY);
236+
return getSafeCodeSnippetOptionFromKey(exportSnippetFormat);
247237
}
248238

249239
@action.bound
250240
setSnippetOption(optionKey: string) {
251241
this.props.uiStore!.exportSnippetFormat = optionKey;
252242
}
253-
};
243+
};
244+

0 commit comments

Comments
 (0)