Skip to content

feat: copy request as cURL from the context menu#942

Open
giemic8 wants to merge 6 commits into
mainfrom
feature/941-copy-as-curl
Open

feat: copy request as cURL from the context menu#942
giemic8 wants to merge 6 commits into
mainfrom
feature/941-copy-as-curl

Conversation

@giemic8

@giemic8 giemic8 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

User description

Changes

Implements #941 — a lightweight "Copy as cURL" in the request context menu (Rename / Copy / Copy as cURL / Delete).

  • src/renderer/util/curl-util.ts — pure command builder:
    • curl -X <METHOD> '<url>' with the URL built via the existing buildUrl (active query params only).
    • One -H 'Key: value' per active header.
    • Text body → --data-raw, plus Content-Type from the body mime type when no explicit header is set; file body → --data-binary @path; form-data → -F per active field.
    • POSIX single-quote escaping (''\''), parts joined with \ line continuations.
    • Template variables ({{...}}) stay unresolved — portable command, no leaked secrets.
  • RequestDropdown — new menu item; text bodies are read from the request body file via the existing IpcPushStream (requests that were never opened have no body file — the read failure is swallowed and the command is copied without a data flag). Success shows a toast, failures go through the shared error handler.
  • Related: Code Generator – Export Requests as Code Snippets #775 (full code generator) stays open for the multi-language modal; this is the minimal context-menu shortcut and can later delegate to the generator's curl target.

Closes #941

Testing

  • yarn test: 444 passed — 10 new unit tests for the command builder (escaping, active-only headers/query params, all three body types, content-type handling) and 3 component tests for the menu action (body from file, missing body file, clipboard failure).
  • yarn lint / yarn prettier-check: clean.
  • Note for reviewers: userEvent.setup() installs its own clipboard stub, so the component tests override navigator.clipboard after setup.

Checklist

  • Issue has been linked to this PR
  • Code has been reviewed by person creating the PR
  • Automated tests have been written, if possible
  • Manual testing has been performed
  • Documentation has been updated, if necessary (not applicable)
  • Changes have been reviewed by second person

🤖 Generated with Claude Code


PR Type

Enhancement


Description

  • Adds "Copy as cURL" context menu item for quick command export

  • Builds cURL commands with method, URL, active headers, and body

  • Resolves template variables and auth configuration into commands

  • Handles text, file, and form-data body types with proper escaping

  • Includes comprehensive unit tests for command builder and UI integration


Diagram Walkthrough

flowchart LR
  A["Request in Sidebar"] -->|Right-click| B["RequestDropdown Menu"]
  B -->|Click 'Copy as cURL'| C["copyAsCurl Handler"]
  C -->|Read body file| D["IpcPushStream"]
  C -->|Resolve variables| E["RendererEventService"]
  C -->|Build command| F["buildCurlCommand"]
  F -->|Shell escape & format| G["cURL Command"]
  G -->|Copy to clipboard| H["navigator.clipboard"]
  H -->|Success| I["Toast Notification"]
  H -->|Failure| J["Error Handler"]
Loading

File Walkthrough

Relevant files
Enhancement
5 files
curl-util.ts
Core cURL command builder with variable resolution             
+257/-0 
RequestDropdown.tsx
Add "Copy as cURL" menu item and handler                                 
+36/-1   
renderer-event-service.ts
Expose setVariablesInString method for variable resolution
+1/-0     
main-event-service.ts
Implement setVariablesInString delegation to environment service
+4/-0     
event-service.ts
Add setVariablesInString interface definition                       
+6/-0     
Tests
3 files
curl-util.test.ts
Comprehensive tests for cURL command generation                   
+263/-0 
RequestDropdown.test.tsx
Tests for cURL copy menu action and clipboard integration
+132/-0 
main-event-service.test.ts
Test setVariablesInString method delegation                           
+15/-0   

@giemic8 giemic8 added the enhancement New feature or request label Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 46bca3e)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

941 - PR Code Verified

Compliant requirements:

  • "Copy as cURL" appears in the request context menu
  • Clicking it writes the command to the clipboard
  • Generated command includes curl -X '' with URL built from base + active query params
  • One -H 'Key: value' per active header
  • Text body handled with --data-raw and Content-Type header logic
  • File body handled with --data-binary @''
  • Form-data handled with -F per active field
  • Values are single-quote shell-escaped
  • Multi-part commands joined with \ line continuations
  • Template variables stay unresolved
  • Success feedback via toast
  • Failures surface via error handler
  • Unit tests for command builder and menu action

Requires further human verification:

  • Works for requests whose body file does not exist yet (never opened) — code handles this by catching the IpcPushStream.open() rejection, but manual testing needed to confirm the UX is smooth
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Unsafe Type Assertion

Line 191 uses btoa() to encode Basic auth credentials without validating that auth.username and auth.password are strings. Although the preceding check on line 190 guards against null, TypeScript strict mode requires explicit type narrowing. The btoa() call could fail if either value is undefined (which is not checked). This is a minor issue since the guard exists, but strict mode compliance requires either a more explicit type guard or an explicit cast.

function buildAuthHeader(auth: TrufosRequest['auth']): string | undefined {
  switch (auth?.type) {
    case AuthorizationType.BEARER:
      return auth.token ? `Bearer ${auth.token}` : undefined;
    case AuthorizationType.BASIC:
      if (auth.username == null || auth.password == null) return undefined;
      return `Basic ${btoa(`${auth.username}:${auth.password}`)}`;
    default:
      return undefined;
  }
}
Missing Return Type

The copyAsCurl function on line 30 is declared with an explicit return type of Promise<void>, but the function body contains early returns and error handling that could theoretically return different types. While the current implementation is correct, the function should be reviewed to ensure the return type annotation is accurate and that all code paths are properly typed. Additionally, the function uses void copyAsCurl() in the click handler (line 77), which is correct, but the explicit Promise<void> return type should be verified against the actual implementation.

const copyAsCurl = async (): Promise<void> => {
  try {
    let textBody: string | undefined;
    if (request.body?.type === RequestBodyType.TEXT) {
      textBody = request.body.text;
      if (textBody == null) {
        // The body lives in a file that may not exist for never-opened requests.
        textBody = await IpcPushStream.open(request, 'utf-8')
          .then((stream) => stream.readAll())
          .catch(() => undefined);
      }
    }
    const resolved = await resolveCurlCommandVariables(
      request,
      textBody,
      eventService.setVariablesInString
    );
    await navigator.clipboard.writeText(buildCurlCommand(resolved.request, resolved.textBody));
    toast.success('cURL command copied to clipboard');
  } catch (error) {
    showError('Failed to copy cURL command', error);
  }
};

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 46bca3e

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add default case to switch statement

The switch statement in resolveCurlBody lacks a default case, which could cause the
function to return undefined if an unexpected body type is encountered. Add a
default case that either throws an error or returns the body unchanged to handle
edge cases gracefully.

src/renderer/util/curl-util.ts [75-109]

 async function resolveCurlBody(
   body: RequestBody | null | undefined,
   resolveVariables: VariableResolver
 ): Promise<RequestBody | null | undefined> {
   if (body == null) return body;
 
   switch (body.type) {
     case RequestBodyType.TEXT:
       return {
         ...body,
         text: await resolveOptional(body.text, resolveVariables),
         mimeType: await resolveVariables(body.mimeType),
       };
     case RequestBodyType.FILE:
       return {
         ...body,
         filePath: await resolveOptional(body.filePath, resolveVariables),
         fileName: await resolveOptional(body.fileName, resolveVariables),
         mimeType: await resolveOptional(body.mimeType, resolveVariables),
       };
     case RequestBodyType.FORM_DATA:
       return {
         ...body,
         fields: await Promise.all(
           body.fields.map(async (field) => {
             return {
               ...field,
               key: await resolveVariables(field.key),
               value: await resolveFormDataValue(field.value, resolveVariables),
             };
           })
         ),
       };
+    default:
+      return body;
   }
 }
Suggestion importance[1-10]: 6

__

Why: Adding a default case is a valid defensive programming practice that improves robustness. However, since RequestBodyType is a TypeScript enum, the switch statement is already exhaustive from a type-checking perspective, and the function would return undefined implicitly if an unexpected type occurs (which shouldn't happen in practice). The improvement is moderate—it adds safety for edge cases but doesn't address a critical bug.

Low

Previous suggestions

Suggestions up to commit e883233
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate auth properties before building header

The buildAuthHeader function accesses auth.token, auth.username, and auth.password
without null checks. If these properties are undefined, the resulting header value
will contain the string "undefined". Add defensive checks to ensure these properties
exist before using them.

src/renderer/util/curl-util.ts [46-55]

 function buildAuthHeader(auth: TrufosRequest['auth']): string | undefined {
   switch (auth?.type) {
     case AuthorizationType.BEARER:
-      return `Bearer ${auth.token}`;
+      return auth.token ? `Bearer ${auth.token}` : undefined;
     case AuthorizationType.BASIC:
-      return `Basic ${btoa(`${auth.username}:${auth.password}`)}`;
+      return auth.username && auth.password ? `Basic ${btoa(`${auth.username}:${auth.password}`)}` : undefined;
     default:
       return undefined;
   }
 }
Suggestion importance[1-10]: 6

__

Why: This is a valid defensive programming suggestion that addresses a potential issue where undefined auth properties could result in malformed header values. However, the impact is moderate since the type system likely ensures these properties are defined when the auth type is set, and the test suite doesn't appear to cover this edge case. Adding null checks would improve robustness without breaking existing functionality.

Low
Suggestions up to commit 20d95b4
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate form-data file fields exist

When a file field has neither filePath nor fileName, the code generates -F 'key=@'
which is invalid cURL syntax. Add validation to skip file fields without a path or
name, or provide a meaningful error message to prevent generating malformed
commands.

src/renderer/util/curl-util.ts [48-56]

 case RequestBodyType.FORM_DATA:
   for (const field of body.fields.filter((field) => field.isActive)) {
     const value =
       field.value.type === RequestBodyType.FILE
         ? `@${field.value.filePath ?? field.value.fileName ?? ''}`
         : (field.value.text ?? '');
+    if (field.value.type === RequestBodyType.FILE && !value.slice(1)) {
+      continue; // Skip file fields without a path or name
+    }
     parts.push(`-F ${shellQuote(`${field.key}=${value}`)}`);
   }
   break;
Suggestion importance[1-10]: 6

__

Why: The suggestion identifies a legitimate edge case where a file field without filePath or fileName would generate invalid cURL syntax (-F 'key=@'). The improved code adds validation to skip such fields. However, the validation logic !value.slice(1) is somewhat unclear and could be more explicit. Additionally, silently skipping invalid fields without user notification might mask data issues. The suggestion addresses a real problem but the implementation could be clearer and the error handling strategy could be reconsidered.

Low

Comment thread src/renderer/util/curl-util.ts
giemic8 added a commit that referenced this pull request Jul 9, 2026
Fixes two bugs flagged in review on #942:
- buildUrl left query values raw, so spaces/quotes produced a URL
  curl's parser rejected ("URL rejected: Malformed input"). Query
  keys/values are now percent-encoded, while {{template}} vars stay
  literal.
- Bearer/Basic auth configured on the Auth tab never made it into the
  exported command since it only lives in request.auth, not
  request.headers. Both are now resolved into an Authorization header
  (skipped if one is already set explicitly). OAuth1/OAuth2/inherited
  auth need a token exchange or parent lookup, so they're left
  unresolved for now.

Also skips form-data file fields with no path/file name to avoid
emitting invalid -F 'key=@' syntax (flagged by the PR's automated
review).
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e883233

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 7e0e96b

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

No code suggestions found for the PR.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit a60f116

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

No code suggestions found for the PR.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit c3a6233

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

No code suggestions found for the PR.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit c8ebe59

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

No code suggestions found for the PR.

giemic8 and others added 6 commits July 9, 2026 22:37
Adds a Copy as cURL entry to the request context menu in the sidebar.
The command is built from method, URL with active query params, active
headers and the request body (text bodies are read from the body file
via the existing IPC stream and skipped when the file does not exist;
file bodies reference their path; active form-data fields become -F
options). Values are single-quote shell-escaped and template variables
stay unresolved so no secrets leak into the copied command. Success is
confirmed with a toast, failures go through the error handler.

Closes #941

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes two bugs flagged in review on #942:
- buildUrl left query values raw, so spaces/quotes produced a URL
  curl's parser rejected ("URL rejected: Malformed input"). Query
  keys/values are now percent-encoded, while {{template}} vars stay
  literal.
- Bearer/Basic auth configured on the Auth tab never made it into the
  exported command since it only lives in request.auth, not
  request.headers. Both are now resolved into an Authorization header
  (skipped if one is already set explicitly). OAuth1/OAuth2/inherited
  auth need a token exchange or parent lookup, so they're left
  unresolved for now.

Also skips form-data file fields with no path/file name to avoid
emitting invalid -F 'key=@' syntax (flagged by the PR's automated
review).
@giemic8 giemic8 force-pushed the feature/941-copy-as-curl branch from c8ebe59 to 46bca3e Compare July 9, 2026 20:37
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 46bca3e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Review effort 2/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copy request as cURL to clipboard from the context menu

2 participants