Skip to content

Commit 75ddf09

Browse files
committed
feat(trailers): add support for parsing prohibited trailers in chunked responses
1 parent 378088c commit 75ddf09

File tree

2 files changed

+359
-17
lines changed

2 files changed

+359
-17
lines changed

httplib.h

Lines changed: 268 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,12 +1531,19 @@ class ClientImpl {
15311531
// Implementation is below decompressor class definition
15321532
ssize_t read(char *buf, size_t len);
15331533

1534+
// Parse trailers for chunked responses if they have not been parsed yet.
1535+
// Public to allow tests to trigger parsing explicitly when needed.
1536+
void parse_trailers_if_needed();
1537+
15341538
// Get the last error that occurred during reading (socket direct mode only)
15351539
Error get_read_error() const { return body_reader_.last_error; }
15361540

15371541
// Check if a read error occurred (socket direct mode only)
15381542
bool has_read_error() const { return body_reader_.has_error(); }
15391543

1544+
// Whether trailers have been parsed for chunked transfer
1545+
bool trailers_parsed_ = false;
1546+
15401547
private:
15411548
friend class ClientImpl;
15421549

@@ -2929,7 +2936,134 @@ inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {
29292936
if (!is_valid() || !response) { return -1; }
29302937

29312938
if (decompressor_) { return read_with_decompression(buf, len); }
2932-
return body_reader_.read(buf, len);
2939+
auto n = detail::read_body_content(stream_, body_reader_, buf, len);
2940+
2941+
// If we hit EOF on a chunked response, parse trailers once so callers can
2942+
// observe them via `response->trailers`.
2943+
fprintf(stderr,
2944+
"[DEBUG] StreamHandle::read: n=%d chunked=%d trailers_parsed=%d "
2945+
"stream=%p\n",
2946+
(int)n, (int)body_reader_.chunked, (int)trailers_parsed_,
2947+
(void *)stream_);
2948+
if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) {
2949+
trailers_parsed_ = true;
2950+
2951+
// Read first line; if there's no CRLF/trailer block, nothing to do.
2952+
const auto bufsiz = 128;
2953+
char line_buf[bufsiz];
2954+
detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz);
2955+
2956+
if (!line_reader.getline()) { return n; }
2957+
2958+
// RFC 7230 Section 4.1.2 - Headers prohibited in trailers
2959+
thread_local detail::case_ignore::unordered_set<std::string>
2960+
prohibited_trailers = {"transfer-encoding",
2961+
"content-length",
2962+
"host",
2963+
"authorization",
2964+
"www-authenticate",
2965+
"proxy-authenticate",
2966+
"proxy-authorization",
2967+
"cookie",
2968+
"set-cookie",
2969+
"cache-control",
2970+
"expect",
2971+
"max-forwards",
2972+
"pragma",
2973+
"range",
2974+
"te",
2975+
"age",
2976+
"expires",
2977+
"date",
2978+
"location",
2979+
"retry-after",
2980+
"vary",
2981+
"warning",
2982+
"content-encoding",
2983+
"content-type",
2984+
"content-range",
2985+
"trailer"};
2986+
2987+
detail::case_ignore::unordered_set<std::string> declared_trailers;
2988+
auto trailer_header = response->get_header_value("Trailer");
2989+
if (!trailer_header.empty()) {
2990+
auto lenh = trailer_header.size();
2991+
detail::split(trailer_header.c_str(), trailer_header.c_str() + lenh, ',',
2992+
[&](const char *b, const char *e) {
2993+
const char *kbeg = b;
2994+
const char *kend = e;
2995+
while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
2996+
++kbeg;
2997+
}
2998+
while (kend > kbeg &&
2999+
(kend[-1] == ' ' || kend[-1] == '\t')) {
3000+
--kend;
3001+
}
3002+
std::string key(kbeg, static_cast<size_t>(kend - kbeg));
3003+
if (!key.empty() && prohibited_trailers.find(key) ==
3004+
prohibited_trailers.end()) {
3005+
declared_trailers.insert(key);
3006+
}
3007+
});
3008+
// Debug: print declared trailers for diagnosis
3009+
// (temporary, remove after debugging)
3010+
for (const auto &k : declared_trailers) {
3011+
fprintf(stderr, "[DEBUG] declared_trailer: '%s'\n", k.c_str());
3012+
}
3013+
}
3014+
3015+
size_t trailer_header_count = 0;
3016+
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
3017+
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { break; }
3018+
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { break; }
3019+
3020+
constexpr auto line_terminator_len = 2;
3021+
auto line_beg = line_reader.ptr();
3022+
auto line_end =
3023+
line_reader.ptr() + line_reader.size() - line_terminator_len;
3024+
3025+
// Parse header key/value and trim
3026+
const char *colon = std::find(line_beg, line_end, ':');
3027+
if (colon != line_end) {
3028+
const char *kbeg = line_beg;
3029+
const char *kend = colon;
3030+
while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
3031+
++kbeg;
3032+
}
3033+
while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) {
3034+
--kend;
3035+
}
3036+
const std::string key(kbeg, static_cast<size_t>(kend - kbeg));
3037+
3038+
const char *vbeg = colon + 1;
3039+
const char *vend = line_end;
3040+
while (vbeg < vend && (*vbeg == ' ' || *vbeg == '\t')) {
3041+
++vbeg;
3042+
}
3043+
while (vend > vbeg && (vend[-1] == ' ' || vend[-1] == '\t')) {
3044+
--vend;
3045+
}
3046+
const std::string val(vbeg, static_cast<size_t>(vend - vbeg));
3047+
3048+
if (!key.empty()) {
3049+
fprintf(stderr, "[DEBUG] parsed trailer key='%s' val='%s'\n",
3050+
key.c_str(), val.c_str());
3051+
if (declared_trailers.find(key) != declared_trailers.end()) {
3052+
response->trailers.emplace(key, val);
3053+
trailer_header_count++;
3054+
fprintf(stderr, "[DEBUG] accepted trailer '%s'\n", key.c_str());
3055+
} else {
3056+
fprintf(stderr, "[DEBUG] ignored trailer '%s' (not declared)\n",
3057+
key.c_str());
3058+
}
3059+
}
3060+
}
3061+
3062+
if (!line_reader.getline()) { break; }
3063+
}
3064+
}
3065+
3066+
return n;
29333067
}
29343068

29353069
inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,
@@ -2948,26 +3082,34 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,
29483082
decompress_offset_ = 0;
29493083

29503084
char compressed_buf[8192];
2951-
auto n = body_reader_.read(compressed_buf, sizeof(compressed_buf));
29523085

2953-
if (n <= 0) { return n; } // EOF or error
3086+
// Read compressed data via the centralized helper. Loop until decompressor
3087+
// produces output or EOF/error occurs. This avoids recursion and makes the
3088+
// decompression buffering behavior explicit.
3089+
while (true) {
3090+
auto n = detail::read_body_content(stream_, body_reader_, compressed_buf,
3091+
sizeof(compressed_buf));
29543092

2955-
// Decompress the data
2956-
bool decompress_ok =
2957-
decompressor_->decompress(compressed_buf, static_cast<size_t>(n),
2958-
[this](const char *data, size_t data_len) {
2959-
decompress_buffer_.append(data, data_len);
2960-
return true;
2961-
});
3093+
if (n <= 0) { return n; } // EOF or error
29623094

2963-
if (!decompress_ok) {
2964-
body_reader_.last_error = Error::Read;
2965-
return -1;
2966-
}
3095+
// Decompress the data
3096+
bool decompress_ok =
3097+
decompressor_->decompress(compressed_buf, static_cast<size_t>(n),
3098+
[this](const char *data, size_t data_len) {
3099+
decompress_buffer_.append(data, data_len);
3100+
return true;
3101+
});
29673102

2968-
if (decompress_buffer_.empty()) {
2969-
// Decompressor needs more data, try again
2970-
return read_with_decompression(buf, len);
3103+
if (!decompress_ok) {
3104+
body_reader_.last_error = Error::Read;
3105+
return -1;
3106+
}
3107+
3108+
if (!decompress_buffer_.empty()) {
3109+
break; // we have output to return
3110+
}
3111+
3112+
// Otherwise continue reading more compressed data
29713113
}
29723114

29733115
// Return from the newly decompressed buffer
@@ -2977,6 +3119,113 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,
29773119
return static_cast<ssize_t>(to_copy);
29783120
}
29793121

3122+
inline void ClientImpl::StreamHandle::parse_trailers_if_needed() {
3123+
if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) {
3124+
return;
3125+
}
3126+
3127+
trailers_parsed_ = true;
3128+
3129+
const auto bufsiz = 128;
3130+
char line_buf[bufsiz];
3131+
detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz);
3132+
3133+
if (!line_reader.getline()) { return; }
3134+
3135+
thread_local detail::case_ignore::unordered_set<std::string>
3136+
prohibited_trailers = {"transfer-encoding",
3137+
"content-length",
3138+
"host",
3139+
"authorization",
3140+
"www-authenticate",
3141+
"proxy-authenticate",
3142+
"proxy-authorization",
3143+
"cookie",
3144+
"set-cookie",
3145+
"cache-control",
3146+
"expect",
3147+
"max-forwards",
3148+
"pragma",
3149+
"range",
3150+
"te",
3151+
"age",
3152+
"expires",
3153+
"date",
3154+
"location",
3155+
"retry-after",
3156+
"vary",
3157+
"warning",
3158+
"content-encoding",
3159+
"content-type",
3160+
"content-range",
3161+
"trailer"};
3162+
3163+
detail::case_ignore::unordered_set<std::string> declared_trailers;
3164+
auto trailer_header = response->get_header_value("Trailer");
3165+
if (!trailer_header.empty()) {
3166+
auto lenh = trailer_header.size();
3167+
detail::split(trailer_header.c_str(), trailer_header.c_str() + lenh, ',',
3168+
[&](const char *b, const char *e) {
3169+
const char *kbeg = b;
3170+
const char *kend = e;
3171+
while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
3172+
++kbeg;
3173+
}
3174+
while (kend > kbeg &&
3175+
(kend[-1] == ' ' || kend[-1] == '\t')) {
3176+
--kend;
3177+
}
3178+
std::string key(kbeg, static_cast<size_t>(kend - kbeg));
3179+
if (!key.empty() && prohibited_trailers.find(key) ==
3180+
prohibited_trailers.end()) {
3181+
declared_trailers.insert(key);
3182+
}
3183+
});
3184+
}
3185+
3186+
size_t trailer_header_count = 0;
3187+
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
3188+
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { break; }
3189+
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { break; }
3190+
3191+
constexpr auto line_terminator_len = 2;
3192+
auto line_beg = line_reader.ptr();
3193+
auto line_end =
3194+
line_reader.ptr() + line_reader.size() - line_terminator_len;
3195+
3196+
const char *colon = std::find(line_beg, line_end, ':');
3197+
if (colon != line_end) {
3198+
const char *kbeg = line_beg;
3199+
const char *kend = colon;
3200+
while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
3201+
++kbeg;
3202+
}
3203+
while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) {
3204+
--kend;
3205+
}
3206+
const std::string key(kbeg, static_cast<size_t>(kend - kbeg));
3207+
3208+
const char *vbeg = colon + 1;
3209+
const char *vend = line_end;
3210+
while (vbeg < vend && (*vbeg == ' ' || *vbeg == '\t')) {
3211+
++vbeg;
3212+
}
3213+
while (vend > vbeg && (vend[-1] == ' ' || vend[-1] == '\t')) {
3214+
--vend;
3215+
}
3216+
const std::string val(vbeg, static_cast<size_t>(vend - vbeg));
3217+
3218+
if (!key.empty() &&
3219+
declared_trailers.find(key) != declared_trailers.end()) {
3220+
response->trailers.emplace(key, val);
3221+
trailer_header_count++;
3222+
}
3223+
}
3224+
3225+
if (!line_reader.getline()) { break; }
3226+
}
3227+
}
3228+
29803229
// ----------------------------------------------------------------------------
29813230

29823231
/*
@@ -7393,6 +7642,8 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) {
73937642

73947643
if (chunk_size == 0) {
73957644
// Final chunk
7645+
// Final chunk
7646+
fprintf(stderr, "[DEBUG] BodyReader::read: final chunk encountered\n");
73967647
eof = true;
73977648
return 0;
73987649
}

0 commit comments

Comments
 (0)