feat(http): add cURL tab to View Results Tree Request panel#6736
feat(http): add cURL tab to View Results Tree Request panel#6736poliakov-alex wants to merge 1 commit into
Conversation
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>
|
|
||
| String body = sampleResult.getQueryString(); | ||
| if (StringUtilities.isNotBlank(body)) { | ||
| sb.append(NEWLINE).append(" --data-raw ").append(quote(body)); //$NON-NLS-1$ |
There was a problem hiding this comment.
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$ |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
+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?
| <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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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-LocalAddresspseudo-header is copied into the command although it never went on the wire (RequestViewCurl#57). HEADsamples rendercurl -X 'HEAD', which fails or hangs (RequestViewCurl#107).- Rendered-placeholder bodies reach
--data-rawon 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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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#657—request.addHeader(HEADER_LOCAL_ADDRESS, localAddress.toString()), immediately beforeres.setRequestHeaders(...)HTTPConstantsInterface#75—String 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)) { |
There was a problem hiding this comment.
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 0On 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.
| String body = sampleResult.getQueryString(); | ||
| if (StringUtilities.isNotBlank(body)) { | ||
| sb.append(NEWLINE).append(" --data-raw ").append(quote(body)); //$NON-NLS-1$ |
There was a problem hiding this comment.
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-Typeis the file's own type rather than multipart.HTTPHC4Impl#1545appends<actual file content, not shown here>in thegetSendFileAsPostBodybranch. - Non-repeatable entity.
HTTPHC4Impl#1619appends<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$ |
There was a problem hiding this comment.
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$ |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
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):
Types of changes
New feature (non-breaking change which adds functionality)
Checklist: