write-out: make %header{} able to output *all* occurances of a header

By appending `:all:[separator]` to the header name. The `[separator]` string
is output between each header value if there are more than one to output.

Test 764 and 765 verify

Idea-by: kapsiR on github
Ref: #18449
Closes #18491
This commit is contained in:
Daniel Stenberg 2025-09-08 00:18:52 +02:00
parent 522c991336
commit 6abe788bb8
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
5 changed files with 249 additions and 17 deletions

View file

@ -93,6 +93,14 @@ The value of header `name` from the transfer's most recent server response.
Unlike other variables, the variable name `header` is not in braces. For
example `%header{date}`. Refer to --write-out remarks. (Added in 7.84.0)
Starting with 8.17.0, this output the contents of *all* header fields using a
specific name - even for a whole redirect "chain" by appending
`:all:[separator]` to the header name. The `[separator]` string (if not blank)
is output between the headers if there are more than one. When more than one
header is shown, they are output in the chronological order of appearance over
the wire. To include a close brace (`}`) in the separator, escape it with a
backslash: `\}`.
## `header_json`
A JSON object with all HTTP response headers from the recent transfer. Values
are provided as arrays, since in the case of multiple headers there can be

View file

@ -615,6 +615,111 @@ static const char *outtime(const char *ptr, /* %time{ ... */
return ptr;
}
static void separator(const char *sep, size_t seplen, FILE *stream)
{
while(seplen) {
if(*sep == '\\') {
switch(sep[1]) {
case 'r':
fputc('\r', stream);
break;
case 'n':
fputc('\n', stream);
break;
case 't':
fputc('\t', stream);
break;
case '}':
fputc('}', stream);
break;
case '\0':
break;
default:
/* unknown, just output this */
fputc(sep[0], stream);
fputc(sep[1], stream);
break;
}
sep += 2;
seplen -= 2;
}
else {
fputc(*sep, stream);
sep++;
seplen--;
}
}
}
static void output_header(struct per_transfer *per,
FILE *stream,
const char **pptr)
{
const char *ptr = *pptr;
const char *end;
end = strchr(ptr, '}');
do {
if(!end || (end && (end[-1] != '\\')))
break;
end = strchr(&end[1], '}');
} while(end);
if(end) {
char hname[256]; /* holds the longest header field name */
struct curl_header *header;
const char *instr;
const char *sep = NULL;
size_t seplen = 0;
size_t vlen = end - ptr;
instr = memchr(ptr, ':', vlen);
if(instr) {
/* instructions follow */
if(!strncmp(&instr[1], "all:", 4)) {
sep = &instr[5];
seplen = end - sep;
vlen -= (seplen + 5);
}
}
if(vlen < sizeof(hname)) {
memcpy(hname, ptr, vlen);
hname[vlen] = 0;
if(sep) {
/* get headers from all requests */
int reqno = 0;
size_t indno = 0;
bool output = FALSE;
do {
if(CURLHE_OK == curl_easy_header(per->curl, hname, indno,
CURLH_HEADER, reqno,
&header)) {
if(output)
/* output separator */
separator(sep, seplen, stream);
fputs(header->value, stream);
output = TRUE;
}
else
break;
if((header->index + 1) < header->amount)
indno++;
else {
++reqno;
indno = 0;
}
} while(1);
}
else {
if(CURLHE_OK == curl_easy_header(per->curl, hname, 0,
CURLH_HEADER, -1, &header))
fputs(header->value, stream);
}
}
ptr = end + 1;
}
else
fputs("%header{", stream);
*pptr = ptr;
}
void ourWriteOut(struct OperationConfig *config, struct per_transfer *per,
CURLcode per_result)
{
@ -699,22 +804,7 @@ void ourWriteOut(struct OperationConfig *config, struct per_transfer *per,
}
else if(!strncmp("header{", &ptr[1], 7)) {
ptr += 8;
end = strchr(ptr, '}');
if(end) {
char hname[256]; /* holds the longest header field name */
struct curl_header *header;
vlen = end - ptr;
if(vlen < sizeof(hname)) {
memcpy(hname, ptr, vlen);
hname[vlen] = 0;
if(CURLHE_OK == curl_easy_header(per->curl, hname, 0,
CURLH_HEADER, -1, &header))
fputs(header->value, stream);
}
ptr = end + 1;
}
else
fputs("%header{", stream);
output_header(per, stream, &ptr);
}
else if(!strncmp("time{", &ptr[1], 5)) {
ptr = outtime(ptr, stream);

View file

@ -109,7 +109,7 @@ test727 test728 test729 test730 test731 test732 test733 test734 test735 \
test736 test737 test738 test739 test740 test741 test742 test743 test744 \
test745 test746 test747 test748 test749 test750 test751 test752 test753 \
test754 test755 test756 test757 test758 test759 test760 test761 test762 \
test763 \
test763 test764 test765 \
\
test780 test781 test782 test783 test784 test785 test786 test787 test788 \
test789 test790 test791 test792 test793 test794 test795 test796 test797 \

67
tests/data/test764 Normal file
View file

@ -0,0 +1,67 @@
<testcase>
<info>
<keywords>
HTTP
HTTP GET
-w
%header
</keywords>
</info>
#
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 301 Redirect
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
ETag: "21025-dc7-39462498"
Accept-Ranges: bytes
This: one
This: two
Content-Length: 6
Location: %TESTNUMBER0002
Content-Type: text/html
Funny-head: yesyes
-foo-
</data>
<data2 nocheck="yes">
HTTP/1.1 200 Not a redirect
Accept-Ranges: bytes
This: three
This: four
Content-Length: 6
Funny-head: yesyes
-foo-
</data2>
</reply>
#
# Client-side
<client>
<features>
headers-api
</features>
<server>
http
</server>
<name>
-w with multiple header output
</name>
<command option="no-output">
http://%HOSTIP:%HTTPPORT/%TESTNUMBER -L -w '%header{this:all:***}\n' -o %LOGDIR/%TESTNUMBER.out
</command>
</client>
#
# Verify data after the test has been "shot"
<verify>
<stdout mode="text">
one***two***three***four
</stdout>
</verify>
</testcase>

67
tests/data/test765 Normal file
View file

@ -0,0 +1,67 @@
<testcase>
<info>
<keywords>
HTTP
HTTP GET
-w
%header
</keywords>
</info>
#
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 301 Redirect
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
ETag: "21025-dc7-39462498"
Accept-Ranges: bytes
This: one
This: two
Content-Length: 6
Location: %TESTNUMBER0002
Content-Type: text/html
Funny-head: yesyes
-foo-
</data>
<data2 nocheck="yes">
HTTP/1.1 200 Not a redirect
Accept-Ranges: bytes
This: three
This: four
Content-Length: 6
Funny-head: yesyes
-foo-
</data2>
</reply>
#
# Client-side
<client>
<features>
headers-api
</features>
<server>
http
</server>
<name>
-w with multiple header output using } in separator
</name>
<command option="no-output">
http://%HOSTIP:%HTTPPORT/%TESTNUMBER -L -w '%header{this:all:-{\}-}\n' -o %LOGDIR/%TESTNUMBER.out
</command>
</client>
#
# Verify data after the test has been "shot"
<verify>
<stdout mode="text">
one-{}-two-{}-three-{}-four
</stdout>
</verify>
</testcase>