@@ -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
29353069inline 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