Skip to content

feat(http): add cURL tab to View Results Tree Request panel - #6736

Open
poliakov-alex wants to merge 3 commits into
apache:masterfrom
poliakov-alex:feature/6375-curl-request-view
Open

feat(http): add cURL tab to View Results Tree Request panel#6736
poliakov-alex wants to merge 3 commits into
apache:masterfrom
poliakov-alex:feature/6375-curl-request-view

Conversation

@poliakov-alex

@poliakov-alex poliakov-alex commented Jul 24, 2026

Copy link
Copy Markdown

Render the sampled HTTP request as a ready-to-run curl command in a new "cURL" sub-tab next to Raw and HTTP, so it can be copied to a console or shared with a developer.

Closes #6375

Description

Adds a cURL tab to the Request panel of the View Results Tree listener, next to the existing Raw and HTTP tabs. It renders the sampled HTTP request as a ready-to-run curl command.

What the generated command contains:

  • Method--head for HEAD (a plain -X HEAD makes curl wait for a body it never receives); no -X for a plain GET; -X 'GET' only when a GET carries a body (otherwise curl switches it to POST); -X for every other method.
  • URL and request headers (-H). Headers are split line by line so repeated header names (e.g. several Accept values) are all preserved.
  • Cookies via -b (JMeter tracks them separately from the header list).
  • Body:
    • regular bodies → --data-raw (verbatim, no @/< interpretation);
    • multipart uploads → --form-string 'field=value' for text fields and -F 'name=@filename;type=...' for files. Since JMeter does not keep the uploaded bytes, the file is referenced as @filename — a placeholder to edit to a real path before running. The original Content-Type is dropped so curl sets its own multipart boundary;
    • a file sent as the whole body, or a non-repeatable body (JMeter stores only a placeholder) → omitted, with a short shell-comment note.
  • --compressed when the request set Accept-Encoding (HttpClient disables automatic decompression, so it is only present when explicitly set); the explicit header is then dropped as redundant.

Headers that curl manages itself or that never went on the wire are omitted, so the command actually runs: Content-Length, Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade (e.g. Connection is forbidden in HTTP/2 and yields curl: (92) ... PROTOCOL_ERROR; a manual Content-Length conflicts with the body curl computes), and the X-LocalAddress pseudo-header JMeter adds only for reporting.

Values are single-quoted and shell-escaped, so the output is paste-safe in a POSIX-compatible shell (it is not cmd.exe / PowerShell syntax — noted in the docs). The command reproduces whatever the sample carried, including Authorization headers, cookies and API keys, so it is documented as sensitive when shared.

The tab is contributed through the existing RequestView service interface (@AutoService), so no wiring changes were needed in RequestPanel. The command builder lives in org.apache.jmeter.protocol.http.curl.CurlCommandFormatter (next to BasicCurlParser) and has no Swing dependency, so it can be reused later — e.g. by a "Copy as cURL" action on the sampler itself, which would also have the real file paths for uploads.

Motivation and Context

Fixes #6375. Users frequently need to reproduce a sampled request outside JMeter — in a terminal or when handing it to a developer. Today they must reconstruct the curl command by hand from the Raw/HTTP tabs.

How Has This Been Tested?

  • Added CurlCommandFormatterTest (17 JUnit tests) covering: plain GET (no -X), GET with a body, POST with headers and body, repeated header names, skipped connection/auto/X-LocalAddress headers, HEAD--head, multipart rebuilt as --form-string / -F with the file name, a text field whose value starts with @ (must stay --form-string), file-as-body and non-repeatable placeholders omitted, whitespace-only body kept, cookies, Accept-Encoding--compressed, single-quote escaping, null URL, and two round-trips that feed the generated command back through BasicCurlParser and compare method / URL / headers / body.
  • Ran ./gradlew :src:protocol:http:test, checkstyleMain, checkstyleTest, autostyleJavaCheck — all green.
  • Manually verified in the GUI: ran an HTTP sampler, opened View Results Tree → Request → cURL, copied the command and executed it in a terminal successfully.

Screenshots (if appropriate):

image

Types of changes

New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the code style of this project.
  • I have updated the documentation accordingly.

Render the sampled HTTP request as a ready-to-run curl command in a new
"cURL" sub-tab next to Raw and HTTP, so it can be copied to a console or shared.

Closes apache#6375

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@milamberspace milamberspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline notes from a review of the cURL tab (closes #6375). Overall the feature is clean and closely follows the existing RequestViewHTTP sibling; the main functional gap is multipart/file-upload handling.

For context, the original request is #6375.

Comment thread xdocs/usermanual/component_reference.xml
Comment thread xdocs/changes.xml Outdated

@vlsi vlsi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial pass over the cURL tab, on top of @milamberspace's review. I built the branch, ran the existing tests (green), and exercised buildCurlCommand with a probe test plus real curl against a local HTTP server.

The feature is worth having and the SPI integration is clean. Requesting changes for one class of problem: several ordinary samples render a command that looks correct but reproduces a different request from the one JMeter sent. Four inline notes fall into that class.

  • Repeated header names are collapsed by parseHeaders (RequestViewCurl#119).
  • The X-LocalAddress pseudo-header is copied into the command although it never went on the wire (RequestViewCurl#57).
  • HEAD samples render curl -X 'HEAD', which fails or hangs (RequestViewCurl#107).
  • Rendered-placeholder bodies reach --data-raw on two non-multipart paths, so the multipart fix @milamberspace asked for will not cover them (RequestViewCurl#139).

The remaining notes are suggestions, not conditions.

Checked and found correct, for the record: single-quote escaping (name=O'Brien reached the server as exactly 12 bytes), -b cookie syntax, @AutoService registration (both views land in META-INF/services), message-key ordering, locale fallback through the parent bundle, and redirect handling (HTTPSamplerBase takes URL, method, headers, and body from the last hop consistently).

@milamberspace milamberspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @poliakov-alex — really nice work. CurlCommandFormatter is kept Swing-free and reusable, the test coverage on the tricky cases is excellent, and I appreciate the care around the curl pitfalls (--head vs -X HEAD, -X GET to keep the method, --data-raw instead of --data, hop-by-hop headers dropped, and the security note in the docs).

One correctness issue I'd like fixed before merge, plus a couple of minor notes.

Must fix — regular multipart fields should use --form-string

In parseMultipartForm the non-file fields are emitted as -F 'name=value':

parts.add(argument.getName() + "=" + argument.getValue()); // -> -F 'name=value'

curl gives special meaning to a -F value that starts with @ or <: @ reads a file to upload, < reads a file's contents as the field value. So a legitimate text field whose value begins with @ or < (e.g. handle=@someuser, xml=<root/>) is silently misinterpreted — curl tries to open a file, or fails. Shell single-quoting does not protect against this, because it is curl itself (not the shell) doing the interpretation.

Please emit regular fields with --form-string 'name=value' (verbatim value, no @/< handling) and keep -F 'name=@path;type=...' only for the file parts. This mirrors the reasoning you already applied when choosing --data-raw over --data.

Minor (non-blocking)

  • Redundant --compressed + explicit Accept-Encoding: when --compressed is emitted, curl sends its own Accept-Encoding, so the explicit -H 'Accept-Encoding: ...' is redundant. Harmless, but you could drop the header when emitting --compressed.
  • Tab order: only the Raw tab is pinned first in RequestPanel; the HTTP↔cURL order comes from ServiceLoader and isn't deterministic, so "next to Raw and HTTP" is best-effort. Nothing to change in the PR — just noting it.
  • PR description nit: the description mentions RequestViewCurlTest (8 tests) but the file is CurlCommandFormatterTest (15 tests).

Once the --form-string change is in (ideally with a test for a field value starting with @), this looks good to merge.

@poliakov-alex

Copy link
Copy Markdown
Author

Thanks for this, @poliakov-alex — really nice work. CurlCommandFormatter is kept Swing-free and reusable, the test coverage on the tricky cases is excellent, and I appreciate the care around the curl pitfalls (--head vs -X HEAD, -X GET to keep the method, --data-raw instead of --data, hop-by-hop headers dropped, and the security note in the docs).

One correctness issue I'd like fixed before merge, plus a couple of minor notes.

Must fix — regular multipart fields should use --form-string

In parseMultipartForm the non-file fields are emitted as -F 'name=value':

parts.add(argument.getName() + "=" + argument.getValue()); // -> -F 'name=value'

curl gives special meaning to a -F value that starts with @ or <: @ reads a file to upload, < reads a file's contents as the field value. So a legitimate text field whose value begins with @ or < (e.g. handle=@someuser, xml=<root/>) is silently misinterpreted — curl tries to open a file, or fails. Shell single-quoting does not protect against this, because it is curl itself (not the shell) doing the interpretation.

Please emit regular fields with --form-string 'name=value' (verbatim value, no @/< handling) and keep -F 'name=@path;type=...' only for the file parts. This mirrors the reasoning you already applied when choosing --data-raw over --data.

Minor (non-blocking)

  • Redundant --compressed + explicit Accept-Encoding: when --compressed is emitted, curl sends its own Accept-Encoding, so the explicit -H 'Accept-Encoding: ...' is redundant. Harmless, but you could drop the header when emitting --compressed.
  • Tab order: only the Raw tab is pinned first in RequestPanel; the HTTP↔cURL order comes from ServiceLoader and isn't deterministic, so "next to Raw and HTTP" is best-effort. Nothing to change in the PR — just noting it.
  • PR description nit: the description mentions RequestViewCurlTest (8 tests) but the file is CurlCommandFormatterTest (15 tests).

Once the --form-string change is in (ideally with a test for a field value starting with @), this looks good to merge.

Added commit, please check

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why can't I export my HTTP request to cURL in JMeter?

3 participants