Skip to content

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

Open
poliakov-alex wants to merge 1 commit 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 1 commit 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 or shared.

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 that can be copied or shared.

The command includes the method (-X), the URL, request headers (-H), cookies (-b), and the request body (--data-raw). Values are single-quoted and shell-escaped so the output is paste-safe.

Connection-specific (hop-by-hop) and curl-managed headers — Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade, Content-Length — are omitted, because reproducing them makes the request fail (e.g. Connection is forbidden in HTTP/2 and produces curl: (92) ... PROTOCOL_ERROR, and a manual Content-Length conflicts with the body curl computes).

The tab is contributed through the existing RequestView service interface (@autoservice), so no wiring changes were needed in RequestPanel.

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 RequestViewCurlTest (8 JUnit tests) covering GET, headers, POST body, header skipping, cookie handling / de-duplication, single-quote escaping, and a null URL.
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][style-guide] 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.


String body = sampleResult.getQueryString();
if (StringUtilities.isNotBlank(body)) {
sb.append(NEWLINE).append(" --data-raw ").append(quote(body)); //$NON-NLS-1$

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.

Multipart / file-upload requests render a broken curl command.

getQueryString() is not the real wire body for multipart/form-data: it is JMeter's rendered representation, and for file uploads it contains the literal placeholder <actual file content, not shown here> (see HTTPHC4Impl.writeEntityToSB / PostWriter, fed into the query string at HTTPHC4Impl#883). Since the Content-Type: multipart/form-data; boundary=... header is kept and this value is emitted verbatim via --data-raw, a sampled file upload produces:

--data-raw '...--boundary...<actual file content, not shown here>...'

Pasted into a terminal this sends the placeholder text instead of the file bytes, so the request is not reproduced. This is exactly the case RequestViewHTTP special-cases via isMultipart(...). Suggestion: detect multipart requests and either skip --data-raw or replace it with a short note, rather than emit a silently-wrong command.


String method = sampleResult.getHTTPMethod();
if (StringUtilities.isNotBlank(method)) {
sb.append(" -X ").append(quote(method)); //$NON-NLS-1$

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.

Consider dropping -X for GET. -X is emitted unconditionally, so a plain GET renders as curl -X 'GET' .... It is harmless but noisy; most curl generators omit -X for a GET with no body. Consider only emitting -X when the method is not GET (or when a body is present).

Note that the Request panel only shows the headers added by JMeter.
It does not show any headers (such as <code>Host</code>) that may be added by the HTTP protocol implementation.
For HTTP samples the Request panel also provides a <code>cURL</code> tab that renders the request as a
ready-to-run <code>curl</code> command, so it can be copied to a console or shared with a developer.

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.

Nice that the doc is updated here. Consider adding one sentence noting that multipart / file-upload requests cannot be reproduced faithfully (the file content is not captured by JMeter), so users are not surprised when such a command fails — see the related note on RequestViewCurl.

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.

+1, and two more things worth a sentence there.

The command carries whatever the sample carried, including Authorization headers, cookies, and API keys. The PR description suggests sharing it with a developer, so a note that it contains credentials earns its line.

Also, the new text is added without a <p> wrapper, so it runs into the preceding paragraph about headers added by JMeter. The surrounding text does the same, so this is a question rather than a request: keep it as is, or wrap the new sentences?

Comment thread xdocs/changes.xml
<ul>
<li><pr>6333</pr>Apply HiDPI mode automatically when setting up the GUI so JMeter looks sharp on high-resolution displays. Contributed by Gabriele Coletta (github.com/gdmg92)</li>
<li><pr>6656</pr>Replace the previous feather icon with the new oak leaf in the JMeter logo.</li>
<li><issue>6375</issue>Add a <code>cURL</code> tab to the Request panel in View Results Tree, showing the sampled HTTP request as a ready-to-run <code>curl</code> command that can be copied to a console. Contributed by Oleksandr Poliakov (github.com/poliakov-alex)</li>

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.

This entry uses <issue>6375</issue> but no <pr> tag. Other entries in the section reference the PR number (often alongside the issue); consider adding <pr>6736</pr> for traceability.

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.

Agreed on <pr>6736</pr>. One more on the same entry: it sits under Changes -> UI, but the feature is HTTP-specific and only renders for HTTPSampleResult. HTTP Samplers and Test Script Recorder looks like the better home.

@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).

boolean hasCookieHeader = false;
String requestHeaders = sampleResult.getRequestHeaders();
if (StringUtilities.isNotEmpty(requestHeaders)) {
LinkedHashMap<String, String> headers = JMeterUtils.parseHeaders(requestHeaders);

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.

Repeated headers are silently collapsed, so the command does not reproduce the request.

JMeterUtils.parseHeaders returns a LinkedHashMap<String, String>, so only the last value survives per header name. Repeated headers do reach requestHeaders: HeaderManager entries go through request.addHeader (HTTPHC4Impl#1415), and getFromHeadersMatchingPredicate (HTTPHC4Impl#1477) writes every Header instance on its own line.

Verified against this branch:

input:  X-Trace: a\nX-Trace: b\nAccept: text/html\nAccept: application/json

output: curl -X 'GET' \
          'http://example.com/' \
          -H 'X-Trace: b' \
          -H 'Accept: application/json'

X-Trace: a and Accept: text/html are gone. For the HTTP tab that is a display quirk; for a command advertised as ready to run it is a different request. Splitting requestHeaders line by line here, instead of delegating to parseHeaders, keeps the duplicates.

* them itself, or they are connection-specific (hop-by-hop) headers that are
* forbidden in HTTP/2 and would make the request fail with a protocol error.
*/
private static final Set<String> SKIPPED_HEADERS = Set.of(

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.

X-LocalAddress should be skipped: it never went on the wire.

When a sampler has a source address configured, JMeter injects a pseudo-header into the result purely for display:

  • HTTPHC4Impl#657request.addHeader(HEADER_LOCAL_ADDRESS, localAddress.toString()), immediately before res.setRequestHeaders(...)
  • HTTPConstantsInterface#75String HEADER_LOCAL_ADDRESS = "X-LocalAddress"; // pseudo-header for reporting Local Address

The curl tab copies it verbatim, so those samples render -H 'X-LocalAddress: /10.0.0.5'. Adding "x-localaddress" to SKIPPED_HEADERS covers it.

sb.append("curl"); //$NON-NLS-1$

String method = sampleResult.getHTTPMethod();
if (StringUtilities.isNotBlank(method)) {

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.

HEAD samples render a command that fails.

-X is emitted for every method, so a HEAD sample becomes curl -X 'HEAD' '<url>'. curl sends HEAD but still waits for a response body. Against a local server:

$ curl -X 'HEAD' 'http://127.0.0.1:18099/' -o /dev/null
curl: (18) transfer closed with 340 bytes remaining to read   # exit 18

$ curl -I 'http://127.0.0.1:18099/' -o /dev/null              # exit 0

On a keep-alive connection it hangs until the transfer times out instead of failing fast. HEAD needs --head rather than -X 'HEAD'. This overlaps the -X note on line 108 — both fall out of one branch on the method.

Comment on lines +139 to +141
String body = sampleResult.getQueryString();
if (StringUtilities.isNotBlank(body)) {
sb.append(NEWLINE).append(" --data-raw ").append(quote(body)); //$NON-NLS-1$

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.

Two more paths put a placeholder into --data-raw, and neither is multipart.

Extending the multipart note on line 141: filtering on multipart/form-data alone leaves two cases that produce the same silently wrong command.

  • File sent as the request body, where Content-Type is the file's own type rather than multipart. HTTPHC4Impl#1545 appends <actual file content, not shown here> in the getSendFileAsPostBody branch.
  • Non-repeatable entity. HTTPHC4Impl#1619 appends <Entity was not repeatable, cannot view what was sent>.

Both render as --data-raw '<actual file content, not shown here>' and the equivalent. Detecting "this body is a rendered placeholder, not the wire body" covers all three cases; a multipart check covers one.

Separately, line 140 uses isNotBlank, which drops a body made only of whitespace. A body of " " is a real body with Content-Length: 1. isNotEmpty matches what the header and cookie branches above already use.

// Used by Request Panel
static final String KEY_LABEL = "view_results_table_request_tab_curl"; //$NON-NLS-1$

private static final String NEWLINE = " \\\n"; //$NON-NLS-1$

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.

The output is POSIX-shell syntax, with nothing saying so.

NEWLINE is a backslash line continuation, and quote() on line 154 uses single quotes with the '\'' idiom. Neither works in cmd.exe, which has no single-quote quoting at all, or in PowerShell, which doubles '' and continues lines with a backtick. A large share of View Results Tree users are on Windows.

Two options that both beat the status quo: emit the command on one line, so only the quoting differs; or state in component_reference.xml that the command targets a POSIX-compatible shell.

if (HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(name)) {
hasCookieHeader = true;
}
sb.append(NEWLINE).append(" -H ").append(quote(name + ": " + entry.getValue())); //$NON-NLS-1$

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.

Consider adding --compressed when Accept-Encoding is reproduced.

HttpClient4 disables automatic content compression (HTTPHC4Impl#1150), so Accept-Encoding shows up only when a Header Manager sets it — which is common, and the recorder template does it. The header is then copied into the command without --compressed, and curl prints the gzip stream to the terminal.

// Cookies are tracked separately in JMeter; only add them if they were
// not already emitted as a Cookie header above.
String cookies = sampleResult.getCookies();
if (!hasCookieHeader && StringUtilities.isNotEmpty(cookies)) {

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.

The hasCookieHeader branch is unreachable for the shipped HTTP implementations.

HTTPHC4Impl and HTTPJavaImpl both fill requestHeaders from getAllHeadersExceptCookie, so Cookie is stripped by construction and the value arrives through getCookies() instead. testCookieHeaderNotDuplicated therefore pins a state that no sampler produces, which reads as coverage it is not.

If some implementation does leave Cookie in requestHeaders, a comment naming it would help. Otherwise dropping the flag makes the method shorter.

* @param sampleResult the sampled HTTP request
* @return the curl command as a string
*/
static String buildCurlCommand(HTTPSampleResult sampleResult) {

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.

Consider moving the builder out of the GUI class.

#6375 asks for "Copy as cURL", and the follow-up most people will want is a context-menu action on the sampler itself, not only on a sample that has already run. buildCurlCommand has no Swing dependency, so it could live next to BasicCurlParser in org.apache.jmeter.protocol.http.curl and be reused by such an action later.

That placement also enables a round-trip test the current suite cannot express: generate the command, feed it to BasicCurlParser, compare the parsed request against the sampler. The parser already handles -X, -H, -b, and --data-raw (BasicCurlParser#70, #551-555), so the test is cheap — and it would have caught the header and body issues above.

String curl = RequestViewCurl.buildCurlCommand(res);

// curl manages these / they are forbidden in HTTP/2, so they must be dropped
assertFalse(curl.contains("Connection"), curl);

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.

These assertions can pass for the wrong reason.

assertFalse(curl.contains("Connection")) also matches the URL, so the same test against http://connection.example.com/ fails with no regression present. The commands are short: asserting the whole string with assertEquals is stricter and easier to read when it breaks.

Cases worth adding while you are here: repeated header names, HEAD, a multipart body, X-LocalAddress, and a whitespace-only body.

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