/*******************************************************************************
* Copyright (c) 2014, Jan Koester jan.koester@gmx.net
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
*			notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
*			notice, this list of conditions and the following disclaimer in the
*			documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
*			names of its contributors may be used to endorse or promote products
*			derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#include <algorithm>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <cstring>
#include <iterator>
#include <map>
#include <set>
#include <mutex>
#include <iomanip>
#include <random>

#include "config.h"

#include "http.h"
#include "hpack.h"
#include "qpack.h"

#include "exception.h"

#include <netplus/crypto/base64.h>

#include <netplus/connection.h>
#include <netplus/exception.h>

libhttppp::HttpUrl::HttpUrl(){
  _protocol=HTTP;
  _port=80;
  _path="/";
}

libhttppp::HttpUrl::HttpUrl(const std::string& url, bool http3) {
    HTTPException exp;

    // Get the C-style pointer for string operations
    const char* c_url = url.c_str();
    const char* p = c_url;

    if (http3) {
        _protocol = HttpProtocol::HTTP3;
        _port = 443;
        // Strip scheme prefix before host parsing
        if (url.rfind("https://", 0) == 0) {
            p += 8;
        } else if (url.rfind("http://", 0) == 0) {
            p += 7;
        }
    }
    // Update c_url pointer based on scheme matching
    else if (url.rfind("https://", 0) == 0) { // Using rfind for string prefix check
        _protocol = HTTPS;
        _port = 443;
        p += 8; // Advance pointer past "https://"
    }
    else if (url.rfind("http://", 0) == 0) {
        _protocol = HTTP;
        _port = 80;
        p += 7; // Advance pointer past "http://"
    }
    // Note: The "https" and "http" (without ://) checks are less robust but kept for compatibility
    else if (url.rfind("https", 0) == 0) {
        _protocol = HTTPS;
        _port = 443;
    }
    else if (url.rfind("http", 0) == 0) {
        _protocol = HTTP;
        _port = 80;
    }
    else {
        exp[HTTPException::Error] << "Httpurl: protocol not supported!";
        throw exp;
    }
    _path = "/";

    const char* host_start = p;

    // Use the length of the original string to prevent reading past the end
    size_t url_len = url.length();
    size_t offset = p - c_url;

    // Iterate through the remaining part of the string
    while (offset < url_len && *p && *p != ':' && *p != '/') {
        ++p;
        ++offset;
    }

    // Assign the host part
    _host.assign(host_start, p - host_start);

    if (offset < url_len && *p == ':') {
        ++p;
        ++offset;
        const char* digits = p;

        // Iterate through the port digits
        while (offset < url_len && *p && std::isdigit((unsigned char)*p)) {
            ++p;
            ++offset;
        }

        if (p == digits || (p - digits) > 5) {
            exp[HTTPException::Error] << "Httpurl: Port range are Wrong !";
            throw exp;
        }
        _port = std::atoi(std::string(digits, p - digits).c_str());
    }

    // Check for the path part
    if (offset < url_len && *p == '/') {
        // Use string methods or assign from pointer
        // Here we use std::string::substr for clarity and safety with the remaining part of the URL
        _path = url.substr(p - c_url);
    }
}

libhttppp::HttpUrl::HttpUrl(const HttpUrl& src){
  _protocol=src._protocol;
  _host=src._host;
  _port=src._port;
  _path=src._path;
}

libhttppp::HttpUrl::~HttpUrl(){
}

void libhttppp::HttpUrl::clear(){
  _protocol=HTTP;
  _host.clear();
  _port=80;
  _path="/";
}

bool libhttppp::HttpUrl::operator==(const HttpUrl& other) const {
        return (_protocol == other._protocol &&
                _host == other._host &&
                _port == other._port &&
                _path == other._path);
}

bool libhttppp::HttpUrl::operator<(const HttpUrl& other) const {
    if (print().length() != other.print().length()) {
        return print().length() < other.print().length();
    }

    if (_protocol != other._protocol) {
        return _protocol < other._protocol;
    }

    if (_host != other._host) {
        return _host < other._host;
    }

    if (_port != other._port) {
        return _port < other._port;
    }
    return _path < other._path;
}

std::string libhttppp::HttpUrl::print() const{
  std::stringstream ss;
  if(_protocol==HTTP){
    ss << "http://";
  }else{
    ss << "https://";
  }
  ss << _host << ":" << _port << _path;
  return ss.str();
}


int libhttppp::HttpUrl::getProtocol() const{
  return _protocol;
}

const std::string & libhttppp::HttpUrl::getHost() const{
  return _host;
}

int libhttppp::HttpUrl::getPort() const{
  return _port;
}

const std::string & libhttppp::HttpUrl::getPath() const{
  return _path;
}

// --- TLS Session Cache (process-wide singleton) ---
netplus::TlsSessionCache& libhttppp::HttpClient::tlsSessionCache() {
    static netplus::TlsSessionCache cache;
    return cache;
}

libhttppp::HttpClient::HttpClient(const HttpUrl& desturl, int vers)
: _url(desturl){
    try {
      if(vers == 3) {
        if (!tryHttp3First()) {
          // HTTP/3 failed — fallback to HTTP/1.1 or HTTP/2
          resetConnection();
        }
      } else {
        resetConnection();
      }
    } catch (netplus::NetException &e) {
        HTTPException ex;
        ex[HTTPException::Error] << "HttpClient ctor connect failed: " << e.what();
        throw ex; // !!! IMPORTANT: throw HTTPException, nicht NetException
    }
}

// Process-wide cache of host:port where QUIC/HTTP3 failed
static std::mutex s_h3FailMutex;
static std::set<std::string> s_h3FailedHosts;

bool libhttppp::HttpClient::tryHttp3First(){
  std::string hostKey = _url.getHost() + ":" + std::to_string(_url.getPort());
  {
    std::lock_guard<std::mutex> lk(s_h3FailMutex);
    if (s_h3FailedHosts.count(hostKey))
      return false;
  }
  try {
    auto q = std::make_unique<netplus::quic>();
    q->connect(_url.getHost(), _url.getPort(), false);
    q->setNonBlock();

    // Verify the QUIC handshake actually completes within a short timeout.
    // UDP connect() always succeeds, so we must probe the handshake.
    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
    while (!q->getHandshakeDone()) {
      if (std::chrono::steady_clock::now() >= deadline) {
        // Handshake did not complete — server likely doesn't support QUIC
        std::lock_guard<std::mutex> lk(s_h3FailMutex);
        s_h3FailedHosts.insert(hostKey);
        return false;
      }
      try {
        q->pumpNetwork(MSG_DONTWAIT);
      } catch (netplus::NetException &e) {
        if (e.getErrorType() != netplus::NetException::Note) {
          std::lock_guard<std::mutex> lk(s_h3FailMutex);
          s_h3FailedHosts.insert(hostKey);
          return false;
        }
      }
      _sw.waitRead(*q, 50);
    }

    _cltsock = std::move(q);
    _isH2 = false;
    return true;
  } catch (netplus::NetException&) {
    _cltsock.reset();
    _isH2 = false;
    std::lock_guard<std::mutex> lk(s_h3FailMutex);
    s_h3FailedHosts.insert(hostKey);
    return false;
  }
}

void libhttppp::HttpClient::resetConnection(){
  // Reset HTTP/2 connection state on any new connection
  _h2PrefaceSent = false;
  _h2NextStreamId = 1;
  _h2Decoder.reset();
  if (_url.getProtocol() == HttpUrl::HTTP3) {
    auto q = std::make_unique<netplus::quic>();
    q->connect(_url.getHost(), _url.getPort(), false);
    q->setNonBlock();
    // Wait for QUIC handshake to complete before returning
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
    while (!q->getHandshakeDone()) {
      if (std::chrono::steady_clock::now() >= deadline) {
        HTTPException ee;
        ee[HTTPException::Error] << "HTTP/3 QUIC handshake timeout";
        throw ee;
      }
      try {
        q->pumpNetwork(MSG_DONTWAIT);
      } catch (netplus::NetException &e) {
        if (e.getErrorType() != netplus::NetException::Note) throw;
      }
      _sw.waitRead(*q, 50);
    }
    _cltsock = std::move(q);
    _isH2 = false;
  } else if (_url.getProtocol() == HttpUrl::HTTPS) {
    std::map<std::string, netplus::ssl::CertificateBundle> certs;
    auto sslsock = std::make_unique<netplus::ssl>(certs,-1);

    // Advertise h2 and http/1.1 via ALPN
    sslsock->getTls().client_alpn_protocols =
      std::string("\x02h2\x08http/1.1", 12);

    // --- TLS session resumption: attach cache and load saved session ---
    netplus::tls& t = sslsock->getTls();
    t.session_cache = &tlsSessionCache();

    // Build host:port key for the cache lookup
    std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort());
    netplus::TlsSessionCacheEntry saved;
    if (tlsSessionCache().lookup_by_host(cacheKey, saved)) {
        t.attempt_resumption = true;
        t.resumption_session_id    = saved.session_id;
        t.resumption_master_secret = saved.master_secret;
        t.resumption_cipher        = saved.cipher_suite;
    }

    _cltsock = std::move(sslsock);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();

    // Check negotiated ALPN after TLS handshake
    const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn();
    _isH2 = (alpn == "h2");

    // --- Save session for future resumption ---
    {
        netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls();
        if (tc.handshakeDone && !tc.is_tls13
            && !tc.client_session_id.empty() && !tc.masterSecret.empty()) {
            tlsSessionCache().store_by_host(cacheKey,
                tc.client_session_id, tc.masterSecret, tc.chosenSuite);
        }
    }
  } else {
    _cltsock = std::make_unique<netplus::tcp>(-1);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();
    _isH2 = false;
  }
}


void libhttppp::HttpClient::reconnect(){
  // Reset HTTP/2 and HTTP/3 connection state on reconnect
  _h2PrefaceSent = false;
  _h2NextStreamId = 1;
  _h2Decoder.reset();
  if (_url.getProtocol() == HttpUrl::HTTP3) {
    auto q = std::make_unique<netplus::quic>();
    q->connect(_url.getHost(), _url.getPort(), false);
    q->setNonBlock();
    // Wait for QUIC handshake to complete before returning
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
    while (!q->getHandshakeDone()) {
      if (std::chrono::steady_clock::now() >= deadline) {
        HTTPException ee;
        ee[HTTPException::Error] << "HTTP/3 QUIC handshake timeout";
        throw ee;
      }
      try {
        q->pumpNetwork(MSG_DONTWAIT);
      } catch (netplus::NetException &e) {
        if (e.getErrorType() != netplus::NetException::Note) throw;
      }
      _sw.waitRead(*q, 50);
    }
    _cltsock = std::move(q);
    _isH2 = false;
  } else if(_url.getProtocol() == HttpUrl::HTTPS) {
    std::map<std::string, netplus::ssl::CertificateBundle> certs;
    auto sslsock = std::make_unique<netplus::ssl>(certs,-1);

    // Advertise h2 and http/1.1 via ALPN
    sslsock->getTls().client_alpn_protocols =
      std::string("\x02h2\x08http/1.1", 12);

    // --- TLS session resumption: attach cache and load saved session ---
    netplus::tls& t = sslsock->getTls();
    t.session_cache = &tlsSessionCache();

    std::string cacheKey = _url.getHost() + ":" + std::to_string(_url.getPort());
    netplus::TlsSessionCacheEntry saved;
    if (tlsSessionCache().lookup_by_host(cacheKey, saved)) {
        t.attempt_resumption = true;
        t.resumption_session_id    = saved.session_id;
        t.resumption_master_secret = saved.master_secret;
        t.resumption_cipher        = saved.cipher_suite;
    }

    _cltsock = std::move(sslsock);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();

    const std::string &alpn = dynamic_cast<netplus::ssl*>(_cltsock.get())->getSelectedAlpn();
    _isH2 = (alpn == "h2");

    // --- Save session for future resumption ---
    {
        netplus::tls& tc = dynamic_cast<netplus::ssl*>(_cltsock.get())->getTls();
        if (tc.handshakeDone && !tc.is_tls13
            && !tc.client_session_id.empty() && !tc.masterSecret.empty()) {
            tlsSessionCache().store_by_host(cacheKey,
                tc.client_session_id, tc.masterSecret, tc.chosenSuite);
        }
    }
  } else {
    _cltsock = std::make_unique<netplus::tcp>(-1);
    _cltsock->connect(_url.getHost(), _url.getPort(), false);
    _cltsock->setNonBlock();
    _isH2 = false;
  }
}

void libhttppp::HttpClient::setTimeout(int timeout_sec) {
    _recvTimeoutSec = timeout_sec;
    _sendTimeoutSec = timeout_sec;
}

static inline std::string tolower_copy(std::string s) {
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c){ return (char)std::tolower(c); });
    return s;
}

// Check if existing connection is still alive; only reconnect if necessary.
// Avoids expensive TCP/TLS handshakes on every request.
void libhttppp::HttpClient::_ensureConnected() {
    if (_cltsock && _cltsock->fd() >= 0) {
        if (_sw.isDisconnected(*_cltsock)) {
            resetConnection();
        }
        return;
    }
    resetConnection();
}

// Efficient recv: uses socketwait::waitRead() instead of busy-waiting with sleep
// Returns 0 on connection closure. Throws on hard errors.
// Hard timeout to prevent infinite hangs on silently-dead connections.
size_t libhttppp::HttpClient::_recvBlocking(netplus::buffer &b, int timeout_sec) {
    if (timeout_sec <= 0)
        timeout_sec = _recvTimeoutSec;
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
    for (;;) {
        try {
            return _cltsock->recvData(b, 0);
        } catch (netplus::NetException &e) {
            if (e.getErrorType() == netplus::NetException::Note) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "recv timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitRead(*_cltsock, 500);
                continue;
            }
            throw;
        }
    }
}

// Non-blocking recv: returns 0 immediately if no data available (EAGAIN)
size_t libhttppp::HttpClient::_recvNonBlocking(netplus::buffer &b) {
    try {
        return _cltsock->recvData(b, 0);
    } catch (netplus::NetException &e) {
        if (e.getErrorType() == netplus::NetException::Note) {
            return 0;   // EAGAIN — no data yet
        }
        throw;
    }
}

// Efficient send: uses socketwait::waitWrite() instead of busy-waiting
void libhttppp::HttpClient::_sendAll(const char *data, size_t len) {
    int send_sec = _sendTimeoutSec > 0 ? _sendTimeoutSec : 30;
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(send_sec);
    size_t off = 0;
    while (off < len) {
        netplus::buffer buf(data + off, len - off);
        try {
            size_t n = _cltsock->sendData(buf, 0);
            if (n == 0) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "send timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitWrite(*_cltsock, 500);
                continue;
            }
            off += n;
        } catch (netplus::NetException &e) {
            if (e.getErrorType() == netplus::NetException::Note) {
                if (std::chrono::steady_clock::now() >= deadline) {
                    HTTPException ee;
                    ee[HTTPException::Error] << "send timeout: connection may be dead";
                    throw ee;
                }
                _sw.waitWrite(*_cltsock, 500);
                continue;
            }
            throw;
        }
    }
}

void libhttppp::HttpClient::_sendAll(const std::string &data) {
    _sendAll(data.c_str(), data.size());
}

// Shared HTTP/1.x response reader: reads headers, parses transfer mode, reads body.
// Eliminates ~1000 lines of duplicated code across Get/Post/Put/Delete.
std::vector<char> libhttppp::HttpClient::_h1ReadResponse(const std::string &label) {
    std::vector<char> ret;

    // 1) Read until we have full header (\r\n\r\n)
    std::vector<char> raw;
    raw.reserve(16384);

    size_t header_end = std::string::npos;
    size_t search_from = 0;   // avoid re-scanning from 0 on every recv (O(n²) → O(n))

    for (;;) {
        if (raw.size() >= 4) {
            size_t start = (search_from >= 3) ? search_from - 3 : 0;
            for (size_t i = start; i + 3 < raw.size(); ++i) {
                if (raw[i] == '\r' && raw[i+1] == '\n' &&
                    raw[i+2] == '\r' && raw[i+3] == '\n')
                {
                    header_end = i + 4;
                    break;
                }
            }
            search_from = raw.size();
        }
        if (header_end != std::string::npos)
            break;

        netplus::buffer buf(CHUNKSIZE);
        // Short timeout for initial server response — if the server doesn't
        // start responding within 10s, the connection is likely dead.
        size_t n = _recvBlocking(buf, 10);
        if (n == 0) {
            netplus::NetException ne;
            ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading header";
            throw ne;
        }
        raw.insert(raw.end(), buf.data.buf, buf.data.buf + n);
    }

    // 2) Parse header
    libhttppp::HttpResponse res;
    size_t parsed_hsize = res.parse(raw.data(), raw.size());

    if (parsed_hsize == 0 || parsed_hsize > raw.size()) {
        libhttppp::HTTPException he;
        he[libhttppp::HTTPException::Error] << "HTTP " << label << ": response header parse failed";
        throw he;
    }

    size_t body_off = parsed_hsize;

    // 3) Determine transfer mode
    bool chunked = false;
    ptrdiff_t content_len = -1;

    try {
        for (libhttppp::HttpHeader::HeaderData::Values *v = res.getTransferEncoding();
             v; v = v->nextvalue())
        {
            std::string val = tolower_copy(v->getvalue());
            if (val == "chunked") {
                chunked = true;
                break;
            }
        }
    } catch (...) {}

    if (!chunked) {
        try {
            content_len = res.getContentLength();
            if (content_len < 0) content_len = -1;
        } catch (...) {
            content_len = -1;
        }
    }

    auto append_from_raw = [&](size_t& take_pos, size_t take_len) {
        if (take_len == 0) return;
        ret.insert(ret.end(), raw.begin() + (ptrdiff_t)take_pos,
                              raw.begin() + (ptrdiff_t)(take_pos + take_len));
        take_pos += take_len;
    };

    // 4A) BODY: Content-Length
    if (!chunked && content_len >= 0) {
        size_t remaining = (size_t)content_len;
        ret.reserve(remaining);
        size_t pos = body_off;

        size_t can = (std::min)(remaining, raw.size() - pos);
        append_from_raw(pos, can);
        remaining -= can;

        while (remaining > 0) {
            netplus::buffer buf(CHUNKSIZE);
            size_t n = _recvBlocking(buf);
            if (n == 0) {
                netplus::NetException ne;
                ne[netplus::NetException::Error] << "HTTP " << label << ": EOF before Content-Length complete";
                throw ne;
            }
            size_t take = (std::min)(remaining, n);
            ret.insert(ret.end(), buf.data.buf, buf.data.buf + take);
            remaining -= take;
        }
        return ret;
    }

    // 4B) BODY: chunked
    if (chunked) {
        size_t pos = body_off;

        auto ensure_bytes = [&](size_t need) {
            while (raw.size() - pos < need) {
                netplus::buffer buf(CHUNKSIZE);
                size_t n = _recvBlocking(buf);
                if (n == 0) {
                    netplus::NetException ne;
                    ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading chunked body";
                    throw ne;
                }
                raw.insert(raw.end(), buf.data.buf, buf.data.buf + n);
            }
        };

        auto read_line = [&]() -> std::string {
            for (;;) {
                for (size_t i = pos; i + 1 < raw.size(); ++i) {
                    if (raw[i] == '\r' && raw[i+1] == '\n') {
                        std::string line(raw.begin() + (ptrdiff_t)pos,
                                         raw.begin() + (ptrdiff_t)i);
                        pos = i + 2;
                        return line;
                    }
                }
                netplus::buffer buf(CHUNKSIZE);
                size_t n = _recvBlocking(buf);
                if (n == 0) {
                    netplus::NetException ne;
                    ne[netplus::NetException::Error] << "HTTP " << label << ": EOF while reading chunk size line";
                    throw ne;
                }
                raw.insert(raw.end(), buf.data.buf, buf.data.buf + n);
            }
        };

        for (;;) {
            std::string szline = read_line();
            auto sem = szline.find(';');
            if (sem != std::string::npos) szline.resize(sem);

            size_t chunk_size = 0;
            try {
                chunk_size = (size_t)std::stoul(szline, nullptr, 16);
            } catch (...) {
                libhttppp::HTTPException he;
                he[libhttppp::HTTPException::Error] << "HTTP " << label << ": invalid chunk size line: " << szline;
                throw he;
            }

            if (chunk_size == 0) {
                // Consume trailers (limit to 128 to prevent unbounded
                // memory growth from a malicious server).
                for (int trailer_i = 0; trailer_i < 128; ++trailer_i) {
                    std::string tline = read_line();
                    if (tline.empty()) break;
                }
                break;
            }

            ensure_bytes(chunk_size + 2);

            ret.insert(ret.end(),
                       raw.begin() + (ptrdiff_t)pos,
                       raw.begin() + (ptrdiff_t)(pos + chunk_size));
            pos += chunk_size;

            if (raw[pos] != '\r' || raw[pos+1] != '\n') {
                libhttppp::HTTPException he;
                he[libhttppp::HTTPException::Error] << "HTTP " << label << ": bad chunk terminator";
                throw he;
            }
            pos += 2;
        }

        return ret;
    }

    // 4C) BODY: no length, not chunked -> read until EOF
    {
        size_t pos = body_off;
        if (raw.size() > pos) {
            ret.insert(ret.end(), raw.begin() + (ptrdiff_t)pos, raw.end());
        }
        for (;;) {
            netplus::buffer buf(CHUNKSIZE);
            size_t n = 0;
            try {
                n = _recvBlocking(buf);
            } catch (...) {
                throw;
            }
            if (n == 0) break;
            ret.insert(ret.end(), buf.data.buf, buf.data.buf + n);
        }
    }

    return ret;
}

// Forward declarations for H2/H3 helpers (defined later in this file)
static size_t h3EncodeVarInt(uint64_t value, uint8_t *out);
static uint64_t h3DecodeVarInt(const uint8_t *data, size_t len, size_t &bytes_read);
static void h3AppendFrame(std::vector<uint8_t> &out, uint64_t type, const std::vector<uint8_t> &payload);

static constexpr uint8_t H2C_FRAME_DATA         = 0x00;
static constexpr uint8_t H2C_FRAME_HEADERS      = 0x01;
static constexpr uint8_t H2C_FRAME_SETTINGS     = 0x04;
static constexpr uint8_t H2C_FRAME_PING         = 0x06;
static constexpr uint8_t H2C_FRAME_GOAWAY       = 0x07;
static constexpr uint8_t H2C_FRAME_WINDOW_UPDATE = 0x08;

static constexpr uint8_t H2C_FLAG_END_STREAM  = 0x01;
static constexpr uint8_t H2C_FLAG_ACK         = 0x01;
static constexpr uint8_t H2C_FLAG_END_HEADERS = 0x04;

static constexpr size_t  H2C_FRAME_HEADER_LEN = 9;
static constexpr size_t  H2C_MAX_FRAME_SIZE   = 16384;

static const char H2C_CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
static constexpr size_t H2C_CLIENT_PREFACE_LEN = 24;

static std::string h2cBuildFrame(uint8_t type, uint8_t flags, uint32_t stream_id,
                                 const std::string &payload);
static std::string h2cBuildSettings();
static std::string h2cBuildSettingsAck();
static std::string h2cBuildWindowUpdate(uint32_t stream_id, uint32_t increment);
static std::string h2cConnectionWindowBoost();

// ── Streaming API ──────────────────────────────────────────────────────

bool libhttppp::HttpClient::isStreaming() const {
    return _streamMode != STREAM_NONE;
}

bool libhttppp::HttpClient::waitReadable(int timeout_ms) {
    if (!_cltsock) return false;
    return _sw.waitRead(*_cltsock, timeout_ms);
}

libhttppp::HttpResponse libhttppp::HttpClient::GetStream(libhttppp::HttpRequest &nreq) {
    try {
        _ensureConnected();

        // Reset any previous streaming state
        _streamMode = STREAM_NONE;
        _streamBuf.clear();
        _streamBufPos = 0;
        _streamRemaining = 0;
        _streamChunkDone = false;
        _streamChunkRemaining = 0;
        _streamH2Sid = 0;
        _streamH2EndStream = false;
        _streamH2Raw.clear();
        _streamH3Sid = 0;
        _streamH3EndStream = false;
        _streamH3Raw.clear();
        _streamH3Body.clear();
        _streamH3BodyPos = 0;
        _streamH3InDataFrame = false;
        _streamH3DataRemaining = 0;

        std::stringstream host;
        host << _url.getHost() << ":" << _url.getPort();

        nreq.setHeaderData("host")->push_back(host.str());
        nreq.setRequestType(GETREQUEST);

        if (nreq.getRequestURL().empty())
            nreq.setRequestURL(_url.getPath());

        // ── HTTP/3 (QUIC) streaming path ──
        if (auto *q = dynamic_cast<netplus::quic*>(_cltsock.get())) {
            std::string path = nreq.getRequestURL();
            if (path.empty()) path = _url.getPath();
            std::string scheme = (_url.getProtocol() == HttpUrl::HTTP) ? "http" : "https";
            std::stringstream authority;
            authority << _url.getHost() << ":" << _url.getPort();

            std::vector<qpack::HeaderField> extra;
            for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &key = hd->getkey();
                if (key.empty() || key[0] == ':') continue;
                if (key == "host" || key == "connection") continue;
                for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue())
                    extra.push_back({key, v->getvalue(), false});
            }

            std::vector<uint8_t> headers = qpack::Encoder::encodeRequestHeaders(
                "GET", scheme, authority.str(), path, extra, false, 0);

            std::vector<uint8_t> stream_payload;
            h3AppendFrame(stream_payload, 0x01, headers);

            uint64_t stream_id = q->openStream(true);
            q->sendStreamData(stream_id, stream_payload, true);

            // Read until we have the HEADERS frame
            libhttppp::HttpResponse res;
            bool got_headers = false;
            _streamH3Sid = stream_id;

            auto h3_pump = [&]() {
                for (int pump_i = 0; pump_i < 16; ++pump_i) {
                    try {
                        q->pumpNetwork(MSG_DONTWAIT);
                    } catch (netplus::NetException &e) {
                        if (e.getErrorType() != netplus::NetException::Note)
                            throw;
                        break;
                    }
                }
            };

            auto h3_header_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
            while (!got_headers) {
                // Drain all currently available stream data
                {
                    uint8_t rbuf[65536];
                    bool got_any = false;
                    for (;;) {
                        size_t n = q->recvStreamData(stream_id, rbuf, sizeof(rbuf));
                        if (n == 0) break;
                        _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                        got_any = true;
                    }
                    if (!got_any) {
                        h3_pump();
                        for (;;) {
                            size_t n = q->recvStreamData(stream_id, rbuf, sizeof(rbuf));
                            if (n == 0) break;
                            _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                            got_any = true;
                        }
                        if (!got_any) {
                            if (q->isStreamComplete(stream_id) && !q->hasStreamData(stream_id))
                                break;
                            if (std::chrono::steady_clock::now() >= h3_header_deadline) {
                                HTTPException ee;
                                ee[HTTPException::Error] << "HTTP/3 header timeout";
                                throw ee;
                            }
                            _sw.waitRead(*_cltsock, 50);
                            continue;
                        }
                    }
                }

                // Try to parse frames (incremental: handles partial DATA frames)
                size_t pos = 0;
                // First: consume partial DATA frame continuation
                if (_streamH3InDataFrame && !_streamH3Raw.empty()) {
                    size_t avail = _streamH3Raw.size();
                    size_t take = (std::min)(avail, static_cast<size_t>(_streamH3DataRemaining));
                    _streamH3Body.insert(_streamH3Body.end(),
                        _streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(take));
                    pos = take;
                    _streamH3DataRemaining -= take;
                    if (_streamH3DataRemaining == 0)
                        _streamH3InDataFrame = false;
                }
                while (pos < _streamH3Raw.size() && !_streamH3InDataFrame) {
                    size_t frame_start = pos;
                    size_t bytes = 0;
                    uint64_t frame_type = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                    if (bytes == 0) break;
                    pos += bytes;
                    if (pos >= _streamH3Raw.size()) { pos = frame_start; break; }

                    uint64_t frame_len = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                    if (bytes == 0) { pos = frame_start; break; }
                    pos += bytes;

                    if (frame_type == 0x00) { // DATA — handle incrementally
                        size_t avail = _streamH3Raw.size() - pos;
                        size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
                        _streamH3Body.insert(_streamH3Body.end(),
                            _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos),
                            _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos + take));
                        pos += take;
                        if (take < frame_len) {
                            _streamH3InDataFrame = true;
                            _streamH3DataRemaining = frame_len - take;
                        }
                    } else if (pos + frame_len > _streamH3Raw.size()) {
                        pos = frame_start; break; // need more data for non-DATA frame
                    } else if (frame_type == 0x01) { // HEADERS
                        auto decoded = qpack::Decoder::decode(_streamH3Raw.data() + pos, frame_len);
                        for (const auto &h : decoded) {
                            if (h.name == ":status") {
                                try {
                                    res.setState(std::string("HTTP/3 ") + h.value);
                                } catch (...) {}
                            } else if (h.name == "content-length") {
                                try {
                                    res.setContentLength(static_cast<size_t>(std::stoul(h.value)));
                                    _streamRemaining = static_cast<size_t>(std::stoul(h.value));
                                } catch (...) {}
                            } else if (h.name == "content-type") {
                                res.setContentType(h.value);
                            } else {
                                res.setHeaderData(h.name.c_str())->push_back(h.value);
                            }
                        }
                        got_headers = true;
                        pos += frame_len;
                    } else {
                        pos += frame_len; // skip unknown frame types
                    }
                }
                if (pos > 0)
                    _streamH3Raw.erase(_streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos));
            }

            _streamMode = STREAM_H3;
            _streamH3EndStream = q->isStreamComplete(stream_id) && _streamH3Raw.empty() && !q->hasStreamData(stream_id);
            return res;
        }

        // ── HTTP/2 streaming path ──
        if (_isH2) {
            if (!_h2PrefaceSent) {
                std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN);
                preface += h2cBuildSettings();
                preface += h2cConnectionWindowBoost();
                _sendAll(preface);
                _h2PrefaceSent = true;
            }

            std::string path = nreq.getRequestURL();
            if (path.empty()) path = _url.getPath();
            std::string scheme = (_url.getProtocol() == HttpUrl::HTTPS) ? "https" : "http";
            std::stringstream auth;
            auth << _url.getHost() << ":" << _url.getPort();

            std::vector<hpack::HeaderField> extra;
            for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
                const std::string &key = hd->getkey();
                if (key.empty() || key[0] == ':') continue;
                if (key == "host") continue;
                for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue())
                    extra.push_back({key, v->getvalue(), false});
            }

            std::string hpack_block = hpack::Encoder::encodeRequestHeaders(
                "GET", path, scheme, auth.str(), extra);

            uint32_t stream_id = _h2NextStreamId;
            _h2NextStreamId += 2;
            uint8_t hdr_flags = H2C_FLAG_END_HEADERS | H2C_FLAG_END_STREAM;
            std::string headers_frame = h2cBuildFrame(H2C_FRAME_HEADERS, hdr_flags,
                                                       stream_id, hpack_block);
            _sendAll(headers_frame);

            if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>();

            // Read until we get response HEADERS for our stream
            libhttppp::HttpResponse res;
            bool got_headers = false;
            _streamH2Sid = stream_id;
            _streamH2EndStream = false;

            size_t h2_off = 0;

            auto h2_ensure_bytes = [&](size_t need) {
                while (_streamH2Raw.size() - h2_off < need) {
                    netplus::buffer buf(CHUNKSIZE);
                    size_t n = _recvBlocking(buf, 15);
                    if (n == 0) {
                        HTTPException he;
                        he[HTTPException::Error] << "HTTP/2 GetStream: EOF while reading frames";
                        throw he;
                    }
                    _streamH2Raw.insert(_streamH2Raw.end(),
                        reinterpret_cast<uint8_t*>(buf.data.buf),
                        reinterpret_cast<uint8_t*>(buf.data.buf) + n);
                }
            };

            while (!got_headers) {
                h2_ensure_bytes(H2C_FRAME_HEADER_LEN);

                uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                                   | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                                   | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
                uint8_t frame_type = _streamH2Raw[h2_off+3];
                uint8_t frame_flags = _streamH2Raw[h2_off+4];
                uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                      | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                      | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                      | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

                h2_ensure_bytes(H2C_FRAME_HEADER_LEN + frame_len);

                const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;

                switch (frame_type) {
                    case H2C_FRAME_SETTINGS: {
                        if (!(frame_flags & H2C_FLAG_ACK))
                            _sendAll(h2cBuildSettingsAck());
                        break;
                    }
                    case H2C_FRAME_HEADERS: {
                        if (frame_stream == stream_id) {
                            auto headers = _h2Decoder->decode(payload, frame_len);
                            for (const auto &h : headers) {
                                if (h.name == ":status") {
                                    try {
                                        res.setState(std::string("HTTP/2 ") + h.value);
                                    } catch (...) {}
                                } else if (h.name == "content-length") {
                                    try {
                                        res.setContentLength(static_cast<size_t>(std::stoul(h.value)));
                                        _streamRemaining = static_cast<size_t>(std::stoul(h.value));
                                    } catch (...) {}
                                } else if (h.name == "content-type") {
                                    res.setContentType(h.value);
                                } else {
                                    res.setHeaderData(h.name.c_str())->push_back(h.value);
                                }
                            }
                            got_headers = true;
                            if (frame_flags & H2C_FLAG_END_STREAM)
                                _streamH2EndStream = true;
                        }
                        break;
                    }
                    case H2C_FRAME_DATA: {
                        if (frame_stream == stream_id) {
                            // Body data arrived with headers — save for readBodyChunk
                            _streamBuf.insert(_streamBuf.end(), payload, payload + frame_len);
                            if (frame_len > 0) {
                                std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                               + h2cBuildWindowUpdate(stream_id, frame_len);
                                _sendAll(wu);
                            }
                            if (frame_flags & H2C_FLAG_END_STREAM)
                                _streamH2EndStream = true;
                        }
                        break;
                    }
                    case H2C_FRAME_PING: {
                        if (!(frame_flags & H2C_FLAG_ACK)) {
                            std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                            _sendAll(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                        }
                        break;
                    }
                    case H2C_FRAME_GOAWAY:
                        _streamH2EndStream = true;
                        got_headers = true;
                        break;
                    default:
                        break;
                }

                h2_off += H2C_FRAME_HEADER_LEN + frame_len;
            }

            // Single compact: remove all consumed bytes at once
            if (h2_off > 0) {
                _streamH2Raw.erase(_streamH2Raw.begin(),
                    _streamH2Raw.begin() + static_cast<ptrdiff_t>(h2_off));
            }

            _streamMode = STREAM_H2;
            _streamBufPos = 0;
            return res;
        }

        // ── HTTP/1.x streaming path ──
        if (nreq.getRequestVersion().empty())
            nreq.setRequestVersion(HTTPVERSION(1.1));

        // Send request header
        {
            std::string header;
            nreq.printHeader(header);
            if (!_cltsock || _cltsock->fd() < 0)
                resetConnection();
            _sendAll(header);
        }

        // Read until we have full response header (\r\n\r\n)
        std::vector<char> raw;
        raw.reserve(16384);
        size_t header_end = std::string::npos;
        size_t search_from = 0;

        for (;;) {
            if (raw.size() >= 4) {
                size_t start = (search_from >= 3) ? search_from - 3 : 0;
                for (size_t i = start; i + 3 < raw.size(); ++i) {
                    if (raw[i] == '\r' && raw[i+1] == '\n' &&
                        raw[i+2] == '\r' && raw[i+3] == '\n')
                    {
                        header_end = i + 4;
                        break;
                    }
                }
                search_from = raw.size();
            }
            if (header_end != std::string::npos)
                break;

            netplus::buffer buf(CHUNKSIZE);
            size_t n = _recvBlocking(buf, 10);
            if (n == 0) {
                netplus::NetException ne;
                ne[netplus::NetException::Error] << "HTTP GetStream: EOF while reading header";
                throw ne;
            }
            raw.insert(raw.end(), buf.data.buf, buf.data.buf + n);
        }

        // Parse header
        libhttppp::HttpResponse res;
        size_t parsed_hsize = res.parse(raw.data(), raw.size());

        if (parsed_hsize == 0 || parsed_hsize > raw.size()) {
            libhttppp::HTTPException he;
            he[libhttppp::HTTPException::Error] << "HTTP GetStream: response header parse failed";
            throw he;
        }

        // Save leftover body data that was read together with headers
        size_t body_off = parsed_hsize;
        if (raw.size() > body_off) {
            _streamBuf.assign(raw.begin() + (ptrdiff_t)body_off, raw.end());
        }
        _streamBufPos = 0;

        // Determine transfer mode
        bool chunked = false;
        ptrdiff_t content_len = -1;

        try {
            for (libhttppp::HttpHeader::HeaderData::Values *v = res.getTransferEncoding();
                 v; v = v->nextvalue())
            {
                std::string val = tolower_copy(v->getvalue());
                if (val == "chunked") {
                    chunked = true;
                    break;
                }
            }
        } catch (...) {}

        if (!chunked) {
            try {
                content_len = res.getContentLength();
                if (content_len < 0) content_len = -1;
            } catch (...) {
                content_len = -1;
            }
        }

        if (chunked) {
            _streamMode = STREAM_CHUNKED;
            _streamChunkDone = false;
            _streamChunkRemaining = 0;
        } else if (content_len >= 0) {
            _streamMode = STREAM_CONTENT_LENGTH;
            _streamRemaining = (size_t)content_len;
            // Subtract what's already in the leftover buffer
            size_t have = _streamBuf.size() - _streamBufPos;
            if (have > _streamRemaining) {
                // More data than content-length (shouldn't happen, but be safe)
                _streamBuf.resize(_streamBufPos + _streamRemaining);
            }
        } else {
            _streamMode = STREAM_EOF;
        }

        return res;

    } catch (netplus::NetException &e) {
        libhttppp::HTTPException ee;
        ee[libhttppp::HTTPException::Error] << e.what();
        throw ee;
    }
}

size_t libhttppp::HttpClient::readBodyChunk(char *buf, size_t bufsize) {
    if (_streamMode == STREAM_NONE || bufsize == 0)
        return 0;

    // Helper: drain from internal leftover buffer
    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        // Compact buffer when fully consumed
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    // Helper: receive more data into internal buffer
    auto recvMore = [&]() -> size_t {
        netplus::buffer nb(CHUNKSIZE);
        size_t n = _recvBlocking(nb);
        if (n > 0) {
            // Compact only when consumed portion exceeds half the buffer
            // to amortize the O(n) memmove cost
            if (_streamBufPos > 0 && _streamBufPos >= _streamBuf.size() / 2) {
                _streamBuf.erase(_streamBuf.begin(),
                                 _streamBuf.begin() + (ptrdiff_t)_streamBufPos);
                _streamBufPos = 0;
            }
            _streamBuf.insert(_streamBuf.end(), nb.data.buf, nb.data.buf + n);
        }
        return n;
    };

    // ── Content-Length mode ──
    if (_streamMode == STREAM_CONTENT_LENGTH) {
        if (_streamRemaining == 0) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        size_t want = (std::min)(bufsize, _streamRemaining);

        // First try leftover buffer
        size_t got = drainBuf(want);
        if (got > 0) {
            _streamRemaining -= got;
            if (_streamRemaining == 0)
                _streamMode = STREAM_NONE;
            return got;
        }

        // Read from socket
        netplus::buffer nb(CHUNKSIZE);
        size_t n = _recvBlocking(nb);
        if (n == 0) {
            _streamMode = STREAM_NONE;
            return 0;
        }
        size_t take = (std::min)({n, want, bufsize});
        std::memcpy(buf, nb.data.buf, take);
        _streamRemaining -= take;

        // Save excess data for next call
        if (take < n) {
            _streamBuf.assign(nb.data.buf + take, nb.data.buf + n);
            _streamBufPos = 0;
        }

        if (_streamRemaining == 0)
            _streamMode = STREAM_NONE;
        return take;
    }

    // ── Chunked mode ──
    if (_streamMode == STREAM_CHUNKED) {
        if (_streamChunkDone) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        // Helper: read a line from the stream buffer (refilling as needed)
        auto readLine = [&]() -> std::string {
            for (;;) {
                for (size_t i = _streamBufPos; i + 1 < _streamBuf.size(); ++i) {
                    if (_streamBuf[i] == '\r' && _streamBuf[i+1] == '\n') {
                        std::string line(_streamBuf.begin() + (ptrdiff_t)_streamBufPos,
                                         _streamBuf.begin() + (ptrdiff_t)i);
                        _streamBufPos = i + 2;
                        return line;
                    }
                }
                // Need more data
                if (recvMore() == 0) {
                    _streamChunkDone = true;
                    return {};
                }
            }
        };

        // Iterative loop: avoids tail-recursion when a new chunk is parsed
        // but no data is returned yet (chunk header consumed, data follows).
        for (;;) {

        // If we're in the middle of a chunk, continue reading chunk data
        if (_streamChunkRemaining > 0) {
            size_t want = (std::min)(bufsize, _streamChunkRemaining);
            size_t got = drainBuf(want);
            if (got > 0) {
                _streamChunkRemaining -= got;
                if (_streamChunkRemaining == 0) {
                    // Skip chunk terminator \r\n
                    while (_streamBuf.size() - _streamBufPos < 2)
                        recvMore();
                    _streamBufPos += 2;
                }
                return got;
            }
            // Read from socket directly
            netplus::buffer nb(CHUNKSIZE);
            size_t n = _recvBlocking(nb);
            if (n == 0) {
                _streamChunkDone = true;
                _streamMode = STREAM_NONE;
                return 0;
            }
            size_t take = (std::min)({n, want, bufsize});
            std::memcpy(buf, nb.data.buf, take);
            _streamChunkRemaining -= take;
            if (take < n) {
                _streamBuf.assign(nb.data.buf + take, nb.data.buf + n);
                _streamBufPos = 0;
            }
            if (_streamChunkRemaining == 0) {
                // Skip chunk terminator \r\n
                while (_streamBuf.size() - _streamBufPos < 2)
                    recvMore();
                _streamBufPos += 2;
            }
            return take;
        }

        // Read next chunk size line
        std::string szline = readLine();
        if (szline.empty() && _streamChunkDone) {
            _streamMode = STREAM_NONE;
            return 0;
        }
        auto sem = szline.find(';');
        if (sem != std::string::npos) szline.resize(sem);

        size_t chunk_size = 0;
        try {
            chunk_size = (size_t)std::stoul(szline, nullptr, 16);
        } catch (...) {
            _streamChunkDone = true;
            _streamMode = STREAM_NONE;
            return 0;
        }

        if (chunk_size == 0) {
            // Final chunk — consume trailers (limit to 128 to prevent
            // unbounded memory growth from a malicious server).
            for (int trailer_i = 0; trailer_i < 128; ++trailer_i) {
                std::string tline = readLine();
                if (tline.empty()) break;
            }
            _streamChunkDone = true;
            _streamMode = STREAM_NONE;
            return 0;
        }

        _streamChunkRemaining = chunk_size;
        // Loop back to read data from this new chunk (iterative instead of
        // recursive to avoid stack overflow on many consecutive chunks).
        continue;
        } // end for(;;)
    }

    // ── EOF mode (no content-length, not chunked) ──
    if (_streamMode == STREAM_EOF) {
        // First drain leftover buffer
        size_t got = drainBuf(bufsize);
        if (got > 0) return got;

        // Read from socket; EOF signals end of body
        netplus::buffer nb(CHUNKSIZE);
        size_t n = 0;
        try {
            n = _recvBlocking(nb);
        } catch (...) {
            _streamMode = STREAM_NONE;
            return 0;
        }
        if (n == 0) {
            _streamMode = STREAM_NONE;
            return 0;
        }
        size_t take = (std::min)(n, bufsize);
        std::memcpy(buf, nb.data.buf, take);
        if (take < n) {
            _streamBuf.assign(nb.data.buf + take, nb.data.buf + n);
            _streamBufPos = 0;
        }
        return take;
    }

    // ── HTTP/2 streaming mode ──
    if (_streamMode == STREAM_H2) {
        // First drain any body data buffered during GetStream
        size_t got = drainBuf(bufsize);
        if (got > 0) return got;

        if (_streamH2EndStream) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        // Read more H2 frames until we get DATA for our stream
        size_t h2_off = 0;   // track consumed position instead of erasing per frame

        auto h2_ensure_bytes = [&](size_t need) {
            while (_streamH2Raw.size() - h2_off < need) {
                netplus::buffer fbuf(CHUNKSIZE);
                size_t n = _recvBlocking(fbuf);
                if (n == 0) {
                    _streamH2EndStream = true;
                    return;
                }
                _streamH2Raw.insert(_streamH2Raw.end(),
                    reinterpret_cast<uint8_t*>(fbuf.data.buf),
                    reinterpret_cast<uint8_t*>(fbuf.data.buf) + n);
            }
        };

        while (_streamBuf.empty() && !_streamH2EndStream) {
            h2_ensure_bytes(H2C_FRAME_HEADER_LEN);
            if (_streamH2EndStream) break;

            uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                               | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                               | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
            uint8_t frame_type = _streamH2Raw[h2_off+3];
            uint8_t frame_flags = _streamH2Raw[h2_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                  | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

            h2_ensure_bytes(H2C_FRAME_HEADER_LEN + frame_len);
            if (_streamH2EndStream) break;

            const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;

            switch (frame_type) {
                case H2C_FRAME_SETTINGS: {
                    if (!(frame_flags & H2C_FLAG_ACK))
                        _sendAll(h2cBuildSettingsAck());
                    break;
                }
                case H2C_FRAME_DATA: {
                    if (frame_stream == _streamH2Sid) {
                        // Compact buffer before appending: remove already-consumed bytes
                        if (_streamBufPos > 0) {
                            _streamBuf.erase(_streamBuf.begin(),
                                _streamBuf.begin() + static_cast<ptrdiff_t>(_streamBufPos));
                            _streamBufPos = 0;
                        }
                        _streamBuf.insert(_streamBuf.end(), payload, payload + frame_len);
                        if (frame_len > 0) {
                            // Batch both WINDOW_UPDATE frames into a single send
                            std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                           + h2cBuildWindowUpdate(_streamH2Sid, frame_len);
                            _sendAll(wu);
                        }
                        if (frame_flags & H2C_FLAG_END_STREAM)
                            _streamH2EndStream = true;
                    }
                    break;
                }
                case H2C_FRAME_HEADERS: {
                    // Trailing headers
                    if (frame_stream == _streamH2Sid && (frame_flags & H2C_FLAG_END_STREAM))
                        _streamH2EndStream = true;
                    break;
                }
                case H2C_FRAME_PING: {
                    if (!(frame_flags & H2C_FLAG_ACK)) {
                        std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                        _sendAll(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                    }
                    break;
                }
                case H2C_FRAME_GOAWAY:
                    _streamH2EndStream = true;
                    break;
                default:
                    break;
            }

            h2_off += H2C_FRAME_HEADER_LEN + frame_len;
        }

        // Single compact: remove all consumed bytes at once
        if (h2_off > 0) {
            _streamH2Raw.erase(_streamH2Raw.begin(),
                _streamH2Raw.begin() + static_cast<ptrdiff_t>(h2_off));
        }

        // Drain any newly received body data
        got = drainBuf(bufsize);
        if (got > 0) return got;

        if (_streamH2EndStream) {
            _streamMode = STREAM_NONE;
        }
        return 0;
    }

    // ── HTTP/3 streaming mode ──
    if (_streamMode == STREAM_H3) {
        // First drain any body data buffered during GetStream / previous reads
        if (_streamH3BodyPos < _streamH3Body.size()) {
            size_t have = _streamH3Body.size() - _streamH3BodyPos;
            size_t take = (std::min)(have, bufsize);
            std::memcpy(buf, _streamH3Body.data() + _streamH3BodyPos, take);
            _streamH3BodyPos += take;
            if (_streamH3BodyPos == _streamH3Body.size()) {
                _streamH3Body.clear();
                _streamH3BodyPos = 0;
            }
            return take;
        }

        if (_streamH3EndStream) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        auto *q = dynamic_cast<netplus::quic*>(_cltsock.get());
        if (!q) {
            _streamMode = STREAM_NONE;
            return 0;
        }

        // Pump network and read stream data
        auto h3_pump = [&]() {
            for (int pump_i = 0; pump_i < 256; ++pump_i) {
                try {
                    q->pumpNetwork(MSG_DONTWAIT);
                } catch (netplus::NetException &e) {
                    if (e.getErrorType() != netplus::NetException::Note)
                        throw;
                    break;
                }
            }
        };

        // Block until data arrives or stream completes (with timeout).
        // Unlike the non-blocking variant, readBodyChunk must not return 0
        // when data is simply not available yet — that would cause the caller
        // to think the stream is done.
        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);

        while (_streamH3BodyPos >= _streamH3Body.size()) {
            // Drain all available stream data
            {
                uint8_t rbuf[65536];
                bool got_any = false;
                for (;;) {
                    size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
                    if (n == 0) break;
                    _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                    got_any = true;
                }
                if (!got_any) {
                    h3_pump();
                    for (;;) {
                        size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
                        if (n == 0) break;
                        _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                        got_any = true;
                    }
                }
                if (!got_any) {
                    if (q->isStreamComplete(_streamH3Sid) && _streamH3Raw.empty()
                        && !q->hasStreamData(_streamH3Sid)) {
                        _streamH3EndStream = true;
                        _streamMode = STREAM_NONE;
                        return 0;
                    }
                    if (std::chrono::steady_clock::now() >= deadline) {
                        HTTPException ee;
                        ee[HTTPException::Error] << "HTTP/3 stream read timeout";
                        throw ee;
                    }
                    _sw.waitRead(*_cltsock, 100);
                    continue;
                }
            }

            // Parse H3 frames (incremental: handles partial DATA frames)
            size_t pos = 0;
            if (_streamH3InDataFrame && !_streamH3Raw.empty()) {
                size_t avail = _streamH3Raw.size();
                size_t take = (std::min)(avail, static_cast<size_t>(_streamH3DataRemaining));
                _streamH3Body.insert(_streamH3Body.end(),
                    _streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(take));
                pos = take;
                _streamH3DataRemaining -= take;
                if (_streamH3DataRemaining == 0)
                    _streamH3InDataFrame = false;
            }
            while (pos < _streamH3Raw.size() && !_streamH3InDataFrame) {
                size_t frame_start = pos;
                size_t bytes = 0;
                uint64_t frame_type = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                if (bytes == 0) break;
                pos += bytes;
                if (pos >= _streamH3Raw.size()) { pos = frame_start; break; }

                uint64_t frame_len = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
                if (bytes == 0) { pos = frame_start; break; }
                pos += bytes;

                if (frame_type == 0x00) { // DATA — handle incrementally
                    size_t avail = _streamH3Raw.size() - pos;
                    size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
                    _streamH3Body.insert(_streamH3Body.end(),
                        _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos),
                        _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos + take));
                    pos += take;
                    if (take < frame_len) {
                        _streamH3InDataFrame = true;
                        _streamH3DataRemaining = frame_len - take;
                    }
                } else if (pos + frame_len > _streamH3Raw.size()) {
                    pos = frame_start; break;
                } else {
                    pos += frame_len;
                }
            }
            if (pos > 0)
                _streamH3Raw.erase(_streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos));

            // Check stream completion
            if (q->isStreamComplete(_streamH3Sid) && _streamH3Raw.empty()
                && _streamH3BodyPos >= _streamH3Body.size()
                && !_streamH3InDataFrame && !q->hasStreamData(_streamH3Sid)) {
                _streamH3EndStream = true;
                _streamMode = STREAM_NONE;
                return 0;
            }
        }

        // Return decoded body data
        size_t have = _streamH3Body.size() - _streamH3BodyPos;
        size_t take = (std::min)(have, bufsize);
        std::memcpy(buf, _streamH3Body.data() + _streamH3BodyPos, take);
        _streamH3BodyPos += take;
        if (_streamH3BodyPos == _streamH3Body.size()) {
            _streamH3Body.clear();
            _streamH3BodyPos = 0;
        }
        return take;
    }

    return 0;
}

size_t libhttppp::HttpClient::readBodyChunkNonBlocking(char *buf, size_t bufsize) {
    // Stream finished: signal completion with (size_t)-1
    if (_streamMode == STREAM_NONE || bufsize == 0)
        return static_cast<size_t>(-1);

    // Helper: drain from internal leftover buffer
    auto drainBuf = [&](size_t maxBytes) -> size_t {
        size_t have = _streamBuf.size() - _streamBufPos;
        if (have == 0) return 0;
        size_t take = (std::min)(have, maxBytes);
        std::memcpy(buf, _streamBuf.data() + _streamBufPos, take);
        _streamBufPos += take;
        if (_streamBufPos == _streamBuf.size()) {
            _streamBuf.clear();
            _streamBufPos = 0;
        }
        return take;
    };

    // ── Content-Length mode ──
    if (_streamMode == STREAM_CONTENT_LENGTH) {
        if (_streamRemaining == 0) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        size_t want = (std::min)(bufsize, _streamRemaining);

        // First try leftover buffer
        size_t got = drainBuf(want);
        if (got > 0) {
            _streamRemaining -= got;
            if (_streamRemaining == 0)
                _streamMode = STREAM_NONE;
            return got;
        }

        // Non-blocking read from socket
        netplus::buffer nb(CHUNKSIZE);
        size_t n = _recvNonBlocking(nb);
        if (n == 0)
            return 0;  // No data yet — caller should retry later

        size_t take = (std::min)({n, want, bufsize});
        std::memcpy(buf, nb.data.buf, take);
        _streamRemaining -= take;

        if (take < n) {
            _streamBuf.assign(nb.data.buf + take, nb.data.buf + n);
            _streamBufPos = 0;
        }

        if (_streamRemaining == 0)
            _streamMode = STREAM_NONE;
        return take;
    }

    // ── HTTP/3 non-blocking streaming ──
    if (_streamMode == STREAM_H3) {
        // Drain already-decoded body data first
        if (_streamH3BodyPos < _streamH3Body.size()) {
            size_t have = _streamH3Body.size() - _streamH3BodyPos;
            size_t take = (std::min)(have, bufsize);
            std::memcpy(buf, _streamH3Body.data() + _streamH3BodyPos, take);
            _streamH3BodyPos += take;
            if (_streamH3BodyPos == _streamH3Body.size()) {
                _streamH3Body.clear();
                _streamH3BodyPos = 0;
            }
            return take;
        }

        if (_streamH3EndStream) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        auto *q = dynamic_cast<netplus::quic*>(_cltsock.get());
        if (!q) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        // Non-blocking pump and drain all available stream data
        for (int pump_i = 0; pump_i < 256; ++pump_i) {
            try {
                q->pumpNetwork(MSG_DONTWAIT);
            } catch (netplus::NetException &e) {
                if (e.getErrorType() != netplus::NetException::Note)
                    throw;
                break;
            }
        }

        {
            uint8_t rbuf[65536];
            bool got_any = false;
            for (;;) {
                size_t n = q->recvStreamData(_streamH3Sid, rbuf, sizeof(rbuf));
                if (n == 0) break;
                _streamH3Raw.insert(_streamH3Raw.end(), rbuf, rbuf + n);
                got_any = true;
            }
            if (!got_any && _streamH3Raw.empty() && !_streamH3InDataFrame) {
                if (q->isStreamComplete(_streamH3Sid)
                    && !q->hasStreamData(_streamH3Sid)
                    && _streamH3BodyPos >= _streamH3Body.size()) {
                    _streamH3EndStream = true;
                    _streamMode = STREAM_NONE;
                    return static_cast<size_t>(-1);
                }
                return 0;  // No data yet — caller should retry later
            }
        }

        // Parse H3 frames (incremental: handles partial DATA frames)
        size_t pos = 0;
        if (_streamH3InDataFrame && !_streamH3Raw.empty()) {
            size_t avail = _streamH3Raw.size();
            size_t take = (std::min)(avail, static_cast<size_t>(_streamH3DataRemaining));
            _streamH3Body.insert(_streamH3Body.end(),
                _streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(take));
            pos = take;
            _streamH3DataRemaining -= take;
            if (_streamH3DataRemaining == 0)
                _streamH3InDataFrame = false;
        }
        while (pos < _streamH3Raw.size() && !_streamH3InDataFrame) {
            size_t frame_start = pos;
            size_t bytes = 0;
            uint64_t frame_type = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
            if (bytes == 0) break;
            pos += bytes;
            if (pos >= _streamH3Raw.size()) { pos = frame_start; break; }

            uint64_t frame_len = h3DecodeVarInt(_streamH3Raw.data() + pos, _streamH3Raw.size() - pos, bytes);
            if (bytes == 0) { pos = frame_start; break; }
            pos += bytes;

            if (frame_type == 0x00) { // DATA — handle incrementally
                size_t avail = _streamH3Raw.size() - pos;
                size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
                _streamH3Body.insert(_streamH3Body.end(),
                    _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos),
                    _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos + take));
                pos += take;
                if (take < frame_len) {
                    _streamH3InDataFrame = true;
                    _streamH3DataRemaining = frame_len - take;
                }
            } else if (pos + frame_len > _streamH3Raw.size()) {
                pos = frame_start; break;
            } else {
                pos += frame_len;
            }
        }
        if (pos > 0)
            _streamH3Raw.erase(_streamH3Raw.begin(), _streamH3Raw.begin() + static_cast<ptrdiff_t>(pos));

        if (q->isStreamComplete(_streamH3Sid) && _streamH3Raw.empty()
            && _streamH3BodyPos >= _streamH3Body.size()
            && !_streamH3InDataFrame && !q->hasStreamData(_streamH3Sid))
            _streamH3EndStream = true;

        if (_streamH3BodyPos < _streamH3Body.size()) {
            size_t have = _streamH3Body.size() - _streamH3BodyPos;
            size_t take = (std::min)(have, bufsize);
            std::memcpy(buf, _streamH3Body.data() + _streamH3BodyPos, take);
            _streamH3BodyPos += take;
            if (_streamH3BodyPos == _streamH3Body.size()) {
                _streamH3Body.clear();
                _streamH3BodyPos = 0;
            }
            return take;
        }

        if (_streamH3EndStream) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }
        return 0;
    }

    // ── HTTP/2 non-blocking streaming ──
    if (_streamMode == STREAM_H2) {
        // First drain any body data buffered during GetStream / previous reads
        size_t got = drainBuf(bufsize);
        if (got > 0) return got;

        if (_streamH2EndStream) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }

        // Non-blocking read from socket — always try, append to _streamH2Raw
        {
            netplus::buffer nbuf(CHUNKSIZE);
            size_t n = _recvNonBlocking(nbuf);
            if (n > 0) {
                _streamH2Raw.insert(_streamH2Raw.end(),
                    reinterpret_cast<uint8_t*>(nbuf.data.buf),
                    reinterpret_cast<uint8_t*>(nbuf.data.buf) + n);
            }
        }

        // Process all complete H2 frames in _streamH2Raw
        size_t h2_off = 0;
        while (_streamH2Raw.size() - h2_off >= H2C_FRAME_HEADER_LEN) {
            uint32_t frame_len = (static_cast<uint32_t>(_streamH2Raw[h2_off]) << 16)
                               | (static_cast<uint32_t>(_streamH2Raw[h2_off+1]) << 8)
                               | static_cast<uint32_t>(_streamH2Raw[h2_off+2]);
            uint8_t frame_type = _streamH2Raw[h2_off+3];
            uint8_t frame_flags = _streamH2Raw[h2_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(_streamH2Raw[h2_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+6]) << 16)
                                  | (static_cast<uint32_t>(_streamH2Raw[h2_off+7]) << 8)
                                  | static_cast<uint32_t>(_streamH2Raw[h2_off+8]);

            if (_streamH2Raw.size() - h2_off < H2C_FRAME_HEADER_LEN + frame_len)
                break;  // incomplete frame, wait for more data

            const uint8_t *payload = _streamH2Raw.data() + h2_off + H2C_FRAME_HEADER_LEN;

            switch (frame_type) {
                case H2C_FRAME_SETTINGS: {
                    if (!(frame_flags & H2C_FLAG_ACK))
                        _sendAll(h2cBuildSettingsAck());
                    break;
                }
                case H2C_FRAME_DATA: {
                    if (frame_stream == _streamH2Sid) {
                        if (_streamBufPos > 0) {
                            _streamBuf.erase(_streamBuf.begin(),
                                _streamBuf.begin() + static_cast<ptrdiff_t>(_streamBufPos));
                            _streamBufPos = 0;
                        }
                        _streamBuf.insert(_streamBuf.end(), payload, payload + frame_len);
                        if (frame_len > 0) {
                            std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                           + h2cBuildWindowUpdate(_streamH2Sid, frame_len);
                            _sendAll(wu);
                        }
                        if (frame_flags & H2C_FLAG_END_STREAM)
                            _streamH2EndStream = true;
                    }
                    break;
                }
                case H2C_FRAME_HEADERS: {
                    if (frame_stream == _streamH2Sid && (frame_flags & H2C_FLAG_END_STREAM))
                        _streamH2EndStream = true;
                    break;
                }
                case H2C_FRAME_WINDOW_UPDATE:
                    // Server-side window update: nothing to do for client reads
                    break;
                case H2C_FRAME_PING: {
                    if (!(frame_flags & H2C_FLAG_ACK)) {
                        std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                        _sendAll(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                    }
                    break;
                }
                case H2C_FRAME_GOAWAY:
                    _streamH2EndStream = true;
                    break;
                default:
                    break;
            }

            h2_off += H2C_FRAME_HEADER_LEN + frame_len;
        }

        // Single compact: remove all consumed bytes at once
        if (h2_off > 0) {
            _streamH2Raw.erase(_streamH2Raw.begin(),
                _streamH2Raw.begin() + static_cast<ptrdiff_t>(h2_off));
        }

        // Drain newly received body data
        got = drainBuf(bufsize);
        if (got > 0) return got;

        if (_streamH2EndStream) {
            _streamMode = STREAM_NONE;
            return static_cast<size_t>(-1);
        }
        return 0;
    }

    // For chunked/EOF modes, fall back to blocking readBodyChunk
    return readBodyChunk(buf, bufsize);
}

static bool is_redirect_status(int code) {
  return code == 301 || code == 302 || code == 303 || code == 307 || code == 308;
}

static std::string normalize_path(const std::string &location, const std::string &base_path) {
  if (location.empty()) return base_path.empty() ? std::string("/") : base_path;
  if (location[0] == '/') return location;
  if (location[0] == '?') {
    std::string base = base_path.empty() ? std::string("/") : base_path;
    return base + location;
  }
  std::string base = base_path.empty() ? std::string("/") : base_path;
  size_t slash = base.find_last_of('/');
  if (slash == std::string::npos) return std::string("/") + location;
  return base.substr(0, slash + 1) + location;
}

  // --------------- HTTP/3 client frame helpers ---------------
  static size_t h3EncodeVarInt(uint64_t value, uint8_t *out) {
    if (value < 64) {
      out[0] = static_cast<uint8_t>(value);
      return 1;
    }
    if (value < 16384) {
      out[0] = static_cast<uint8_t>(0x40 | (value >> 8));
      out[1] = static_cast<uint8_t>(value & 0xff);
      return 2;
    }
    if (value < 1073741824) {
      out[0] = static_cast<uint8_t>(0x80 | (value >> 24));
      out[1] = static_cast<uint8_t>((value >> 16) & 0xff);
      out[2] = static_cast<uint8_t>((value >> 8) & 0xff);
      out[3] = static_cast<uint8_t>(value & 0xff);
      return 4;
    }
    out[0] = static_cast<uint8_t>(0xc0 | (value >> 56));
    out[1] = static_cast<uint8_t>((value >> 48) & 0xff);
    out[2] = static_cast<uint8_t>((value >> 40) & 0xff);
    out[3] = static_cast<uint8_t>((value >> 32) & 0xff);
    out[4] = static_cast<uint8_t>((value >> 24) & 0xff);
    out[5] = static_cast<uint8_t>((value >> 16) & 0xff);
    out[6] = static_cast<uint8_t>((value >> 8) & 0xff);
    out[7] = static_cast<uint8_t>(value & 0xff);
    return 8;
  }

  static uint64_t h3DecodeVarInt(const uint8_t *data, size_t len, size_t &bytes_read) {
    if (len == 0) {
      bytes_read = 0;
      return 0;
    }
    uint8_t prefix = data[0] >> 6;
    uint64_t value = data[0] & 0x3f;
    if (prefix == 0) {
      bytes_read = 1;
      return value;
    }
    if (prefix == 1) {
      if (len < 2) { bytes_read = 0; return 0; }
      bytes_read = 2;
      return (value << 8) | data[1];
    }
    if (prefix == 2) {
      if (len < 4) { bytes_read = 0; return 0; }
      bytes_read = 4;
      value = (value << 8) | data[1];
      value = (value << 8) | data[2];
      value = (value << 8) | data[3];
      return value;
    }
    if (len < 8) { bytes_read = 0; return 0; }
    bytes_read = 8;
    for (int i = 1; i < 8; ++i) {
      value = (value << 8) | data[i];
    }
    return value;
  }

  static void h3AppendFrame(std::vector<uint8_t> &out, uint64_t type, const std::vector<uint8_t> &payload) {
    uint8_t buf[8];
    size_t tlen = h3EncodeVarInt(type, buf);
    out.insert(out.end(), buf, buf + tlen);
    size_t plen = h3EncodeVarInt(payload.size(), buf);
    out.insert(out.end(), buf, buf + plen);
    out.insert(out.end(), payload.begin(), payload.end());
  }

// --------------- HTTP/2 client frame helpers (definitions) ---------------

static std::string h2cBuildFrame(uint8_t type, uint8_t flags, uint32_t stream_id,
                                 const std::string &payload) {
    std::string f(9 + payload.size(), '\0');
    uint32_t len = static_cast<uint32_t>(payload.size());
    f[0] = static_cast<char>((len >> 16) & 0xff);
    f[1] = static_cast<char>((len >> 8) & 0xff);
    f[2] = static_cast<char>(len & 0xff);
    f[3] = static_cast<char>(type);
    f[4] = static_cast<char>(flags);
    f[5] = static_cast<char>((stream_id >> 24) & 0x7f);
    f[6] = static_cast<char>((stream_id >> 16) & 0xff);
    f[7] = static_cast<char>((stream_id >> 8) & 0xff);
    f[8] = static_cast<char>(stream_id & 0xff);
    if (!payload.empty())
        std::memcpy(&f[9], payload.data(), payload.size());
    return f;
}

// RFC 7540: SETTINGS_INITIAL_WINDOW_SIZE — use 16 MB so both large
// downloads and uploads don't stall on the default 65535-byte window.
static constexpr uint32_t H2C_INITIAL_WINDOW_SIZE = 16u * 1024 * 1024;

static std::string h2cBuildSettings() {
    // SETTINGS_INITIAL_WINDOW_SIZE (0x04) = H2C_INITIAL_WINDOW_SIZE
    std::string p(6, '\0');
    p[0] = 0x00; p[1] = 0x04;
    p[2] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >> 24) & 0xff);
    p[3] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >> 16) & 0xff);
    p[4] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE >>  8) & 0xff);
    p[5] = static_cast<char>((H2C_INITIAL_WINDOW_SIZE      ) & 0xff);
    return h2cBuildFrame(H2C_FRAME_SETTINGS, 0, 0, p);
}

static std::string h2cBuildSettingsAck() {
    return h2cBuildFrame(H2C_FRAME_SETTINGS, H2C_FLAG_ACK, 0, "");
}

// Connection-level WINDOW_UPDATE to raise the default 65535 connection
// window to match the per-stream INITIAL_WINDOW_SIZE (RFC 7540 §6.9.2).
static std::string h2cConnectionWindowBoost() {
    uint32_t boost = H2C_INITIAL_WINDOW_SIZE - 65535u;
    if (boost == 0) return "";
    return h2cBuildWindowUpdate(0, boost);
}

static std::string h2cBuildWindowUpdate(uint32_t stream_id, uint32_t increment) {
    std::string p(4, '\0');
    p[0] = static_cast<char>((increment >> 24) & 0x7f);
    p[1] = static_cast<char>((increment >> 16) & 0xff);
    p[2] = static_cast<char>((increment >> 8) & 0xff);
    p[3] = static_cast<char>(increment & 0xff);
    return h2cBuildFrame(H2C_FRAME_WINDOW_UPDATE, 0, stream_id, p);
}

// Generic HTTP/2 client request (GET or POST)
const std::vector<char> libhttppp::HttpClient::_h2Request(
    const std::string &method,
    HttpRequest &nreq,
    const std::vector<char> *postBody)
{
    std::vector<char> ret;

    try {
        // Helper: blocking recv using socketwait::waitRead()
        auto recv_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
        auto recv_blocking = [&](netplus::buffer& b) -> size_t {
            for (;;) {
                try {
                    return _cltsock->recvData(b, 0);
                } catch (netplus::NetException& e) {
                    if (e.getErrorType() == netplus::NetException::Note) {
                        if (std::chrono::steady_clock::now() >= recv_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 recv timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitRead(*_cltsock, 5000);
                        continue;
                    }
                    throw;
                }
            }
        };

        auto send_all = [&](const std::string &data) {
            auto send_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
            size_t off = 0;
            while (off < data.size()) {
                netplus::buffer buf(data.c_str() + off, data.size() - off);
                try {
                    size_t n = _cltsock->sendData(buf, 0);
                    if (n == 0) {
                        if (std::chrono::steady_clock::now() >= send_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 send timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitWrite(*_cltsock, 500);
                        continue;
                    }
                    off += n;
                } catch (netplus::NetException& e) {
                    if (e.getErrorType() == netplus::NetException::Note) {
                        if (std::chrono::steady_clock::now() >= send_deadline) {
                            HTTPException ee;
                            ee[HTTPException::Error] << "H2 send timeout: connection may be dead";
                            throw ee;
                        }
                        _sw.waitWrite(*_cltsock, 500);
                        continue;
                    }
                    throw;
                }
            }
        };

        // 1) Send connection preface + SETTINGS (only once per connection)
        if (!_h2PrefaceSent) {
            std::string preface(H2C_CLIENT_PREFACE, H2C_CLIENT_PREFACE_LEN);
            preface += h2cBuildSettings();
            preface += h2cConnectionWindowBoost();
            send_all(preface);
            _h2PrefaceSent = true;
        }

        // 2) Build HEADERS frame for stream 1
        std::string path = nreq.getRequestURL();
        if (path.empty()) path = _url.getPath();

        std::string scheme = (_url.getProtocol() == HttpUrl::HTTPS) ? "https" : "http";

        std::stringstream auth;
        auth << _url.getHost() << ":" << _url.getPort();

        // Collect extra headers from the request
        bool has_content_length = false;
        std::vector<hpack::HeaderField> extra;
        for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
            const std::string &key = hd->getkey();
            if (key.empty() || key[0] == ':') continue;
            if (key == "host") continue; // :authority replaces host in H2
          if (key == "content-length") has_content_length = true;
            for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue()) {
                extra.push_back({key, v->getvalue(), false});
            }
        }

        // Add content-length for POST body (servers may rely on it)
        if (postBody && !has_content_length) {
          extra.push_back({"content-length", std::to_string(postBody->size()), false});
        }

        std::string hpack_block = hpack::Encoder::encodeRequestHeaders(
            method, path, scheme, auth.str(), extra);

        uint32_t stream_id = _h2NextStreamId;
        _h2NextStreamId += 2;  // Client streams use odd IDs: 1, 3, 5, ...
        bool end_stream = (postBody == nullptr || postBody->empty());
        uint8_t hdr_flags = H2C_FLAG_END_HEADERS;
        if (end_stream) hdr_flags |= H2C_FLAG_END_STREAM;

        std::string headers_frame = h2cBuildFrame(H2C_FRAME_HEADERS, hdr_flags,
                                                   stream_id, hpack_block);
        send_all(headers_frame);

        // 3) Send DATA frames for POST body with flow control (RFC 7540 §6.9)
        // raw is shared between POST-send and response-read phases
        std::vector<uint8_t> raw;
        raw.reserve(16384);

        if (postBody && !postBody->empty()) {
            // Track the peer's (server's) flow control window.
            // Initial value is 65535 until we receive the server's SETTINGS.
            int32_t peer_conn_window = 65535;
            int32_t peer_stream_window = 65535;

            // Helper: parse buffered frames for SETTINGS / WINDOW_UPDATE / PING
            auto process_fc_frames = [&]() {
                size_t foff = 0;
                while (foff + H2C_FRAME_HEADER_LEN <= raw.size()) {
                    uint32_t flen = (static_cast<uint32_t>(raw[foff]) << 16)
                                  | (static_cast<uint32_t>(raw[foff+1]) << 8)
                                  | static_cast<uint32_t>(raw[foff+2]);
                    uint8_t ftype = raw[foff+3];
                    uint8_t fflags = raw[foff+4];
                    uint32_t fsid = ((static_cast<uint32_t>(raw[foff+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(raw[foff+6]) << 16)
                                  | (static_cast<uint32_t>(raw[foff+7]) << 8)
                                  | static_cast<uint32_t>(raw[foff+8]);
                    if (foff + H2C_FRAME_HEADER_LEN + flen > raw.size()) break;
                    const uint8_t *fp = raw.data() + foff + H2C_FRAME_HEADER_LEN;

                    if (ftype == H2C_FRAME_SETTINGS && !(fflags & H2C_FLAG_ACK)) {
                        for (size_t si = 0; si + 6 <= flen; si += 6) {
                            uint16_t setting_id = (static_cast<uint16_t>(fp[si]) << 8)
                                                | fp[si+1];
                            uint32_t setting_val = (static_cast<uint32_t>(fp[si+2]) << 24)
                                                 | (static_cast<uint32_t>(fp[si+3]) << 16)
                                                 | (static_cast<uint32_t>(fp[si+4]) << 8)
                                                 | fp[si+5];
                            if (setting_id == 0x04) { // SETTINGS_INITIAL_WINDOW_SIZE
                                int32_t delta = static_cast<int32_t>(setting_val)
                                              - peer_stream_window;
                                peer_stream_window += delta;
                            }
                        }
                        send_all(h2cBuildSettingsAck());
                    } else if (ftype == H2C_FRAME_WINDOW_UPDATE && flen >= 4) {
                        uint32_t inc = ((static_cast<uint32_t>(fp[0]) & 0x7f) << 24)
                                     | (static_cast<uint32_t>(fp[1]) << 16)
                                     | (static_cast<uint32_t>(fp[2]) << 8)
                                     | fp[3];
                        if (inc > 0) {
                            if (fsid == 0)
                                peer_conn_window += static_cast<int32_t>(inc);
                            else if (fsid == stream_id)
                                peer_stream_window += static_cast<int32_t>(inc);
                        }
                    } else if (ftype == H2C_FRAME_PING && !(fflags & H2C_FLAG_ACK)
                               && flen == 8) {
                        std::string pp(reinterpret_cast<const char*>(fp), flen);
                        send_all(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, pp));
                    }
                    // Leave HEADERS / DATA in the buffer for the response phase
                    // but only consume control frames here.
                    foff += H2C_FRAME_HEADER_LEN + flen;
                }
                if (foff > 0)
                    raw.erase(raw.begin(), raw.begin() + static_cast<ptrdiff_t>(foff));
            };

            // Try a non-blocking read to pick up the server's SETTINGS
            // (which usually arrives right after TLS handshake).
            {
                netplus::buffer nbuf(CHUNKSIZE);
                size_t n = 0;
                try { n = _cltsock->recvData(nbuf, 0); } catch (...) {}
                if (n > 0) {
                    raw.insert(raw.end(),
                        reinterpret_cast<uint8_t*>(nbuf.data.buf),
                        reinterpret_cast<uint8_t*>(nbuf.data.buf) + n);
                    process_fc_frames();
                }
            }

            size_t offset = 0;
            while (offset < postBody->size()) {
                int32_t allowed = std::min(peer_conn_window, peer_stream_window);

                // If window exhausted, block-read for WINDOW_UPDATE
                while (allowed <= 0) {
                    netplus::buffer nbuf(CHUNKSIZE);
                    size_t n = recv_blocking(nbuf);
                    if (n == 0) {
                        HTTPException he;
                        he[HTTPException::Error] << "HTTP/2: EOF waiting for WINDOW_UPDATE";
                        throw he;
                    }
                    raw.insert(raw.end(),
                        reinterpret_cast<uint8_t*>(nbuf.data.buf),
                        reinterpret_cast<uint8_t*>(nbuf.data.buf) + n);
                    process_fc_frames();
                    allowed = std::min(peer_conn_window, peer_stream_window);
                }

                size_t chunk = std::min({postBody->size() - offset,
                                         static_cast<size_t>(allowed),
                                         H2C_MAX_FRAME_SIZE});
                bool last = (offset + chunk >= postBody->size());
                uint8_t flags = last ? H2C_FLAG_END_STREAM : 0;
                std::string payload(postBody->data() + offset, chunk);
                send_all(h2cBuildFrame(H2C_FRAME_DATA, flags, stream_id, payload));
                peer_conn_window -= static_cast<int32_t>(chunk);
                peer_stream_window -= static_cast<int32_t>(chunk);
                offset += chunk;
            }
        }

        // 4) Read response frames
        bool got_end_stream = false;

        // Use connection-level decoder to maintain HPACK dynamic table state
        if (!_h2Decoder) _h2Decoder = std::make_unique<hpack::Decoder>();
        hpack::Decoder &decoder = *_h2Decoder;

        // Read enough data into raw buffer
        auto ensure_bytes = [&](size_t need) {
            while (raw.size() < need) {
                netplus::buffer buf(CHUNKSIZE);
                size_t n = recv_blocking(buf);
                if (n == 0) {
                    HTTPException he;
                    he[HTTPException::Error] << "HTTP/2: EOF while reading frames";
                    throw he;
                }
                raw.insert(raw.end(),
                           reinterpret_cast<uint8_t*>(buf.data.buf),
                           reinterpret_cast<uint8_t*>(buf.data.buf) + n);
            }
        };

        // Overall response deadline: prevents infinite loops when a
        // malicious server sends endless non-terminal frames (e.g. PINGs).
        auto h2_response_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(120);

        size_t raw_off = 0;   // track consumed position to avoid per-frame erase

        while (!got_end_stream) {
            if (std::chrono::steady_clock::now() >= h2_response_deadline) {
                HTTPException ee;
                ee[HTTPException::Error] << "HTTP/2 response timeout: no END_STREAM received";
                throw ee;
            }
            // Read frame header (9 bytes)
            ensure_bytes(raw_off + H2C_FRAME_HEADER_LEN);

            uint32_t frame_len = (static_cast<uint32_t>(raw[raw_off]) << 16)
                               | (static_cast<uint32_t>(raw[raw_off+1]) << 8)
                               | static_cast<uint32_t>(raw[raw_off+2]);
            uint8_t frame_type = raw[raw_off+3];
            uint8_t frame_flags = raw[raw_off+4];
            uint32_t frame_stream = ((static_cast<uint32_t>(raw[raw_off+5]) & 0x7f) << 24)
                                  | (static_cast<uint32_t>(raw[raw_off+6]) << 16)
                                  | (static_cast<uint32_t>(raw[raw_off+7]) << 8)
                                  | static_cast<uint32_t>(raw[raw_off+8]);

            // Read frame payload
            ensure_bytes(raw_off + H2C_FRAME_HEADER_LEN + frame_len);

            const uint8_t *payload = raw.data() + raw_off + H2C_FRAME_HEADER_LEN;

            switch (frame_type) {
                case H2C_FRAME_SETTINGS: {
                    if (!(frame_flags & H2C_FLAG_ACK)) {
                        // Server SETTINGS — acknowledge it
                        send_all(h2cBuildSettingsAck());
                    }
                    break;
                }

                case H2C_FRAME_HEADERS: {
                    if (frame_stream == stream_id) {
                        // Decode HPACK response headers
                        auto headers = decoder.decode(payload, frame_len);
                        (void)headers; // We don't need to inspect status for now
                        if (frame_flags & H2C_FLAG_END_STREAM) {
                            got_end_stream = true;
                        }
                    }
                    break;
                }

                case H2C_FRAME_DATA: {
                    if (frame_stream == stream_id) {
                        ret.insert(ret.end(), payload, payload + frame_len);
                        if (frame_flags & H2C_FLAG_END_STREAM) {
                            got_end_stream = true;
                        }
                        // Send WINDOW_UPDATE for connection and stream (batched)
                        if (frame_len > 0) {
                            std::string wu = h2cBuildWindowUpdate(0, frame_len)
                                           + h2cBuildWindowUpdate(stream_id, frame_len);
                            send_all(wu);
                        }
                    }
                    break;
                }

                case H2C_FRAME_WINDOW_UPDATE:
                case H2C_FRAME_PING: {
                    if (frame_type == H2C_FRAME_PING && !(frame_flags & H2C_FLAG_ACK)) {
                        // Respond to PING
                        std::string ping_payload(reinterpret_cast<const char*>(payload), frame_len);
                        send_all(h2cBuildFrame(H2C_FRAME_PING, H2C_FLAG_ACK, 0, ping_payload));
                    }
                    break;
                }

                case H2C_FRAME_GOAWAY: {
                    got_end_stream = true;
                    break;
                }

                default:
                    // Ignore unknown frame types
                    break;
            }

            raw_off += H2C_FRAME_HEADER_LEN + frame_len;
        }

        return ret;

    } catch (netplus::NetException &e) {
        HTTPException ee;
        ee[HTTPException::Error] << e.what();
        throw ee;
    }
}

  // Generic HTTP/3 client request (GET/POST/PUT/DELETE)
  const std::vector<char> libhttppp::HttpClient::_h3Request(
    const std::string &method,
    HttpRequest &nreq,
    const std::vector<char> *postBody)
  {
    std::vector<char> ret;

    auto *q = dynamic_cast<netplus::quic*>(_cltsock.get());
    if (!q) {
      HTTPException ee;
      ee[HTTPException::Error] << "HTTP/3 requested on non-QUIC socket";
      throw ee;
    }

    const int kMaxRedirects = 1;
    int redirects = 0;

    for (;;) {
      std::string path = nreq.getRequestURL();
      if (path.empty()) path = _url.getPath();

      std::string scheme = (_url.getProtocol() == HttpUrl::HTTP) ? "http" : "https";

      std::stringstream authority;
      authority << _url.getHost() << ":" << _url.getPort();

      bool include_content_length = false;
      size_t content_length = 0;

      std::vector<qpack::HeaderField> extra;
      for (HttpHeader::HeaderData *hd = nreq.getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
        const std::string &key = hd->getkey();
        if (key.empty() || key[0] == ':') continue;
        if (key == "host" || key == "connection") continue;

        for (HttpHeader::HeaderData::Values *v = hd->getfirstValue(); v; v = v->nextvalue()) {
          if (key == "content-length") {
            include_content_length = true;
            content_length = static_cast<size_t>(std::stoul(v->getvalue()));
          } else {
            extra.push_back({key, v->getvalue(), false});
          }
        }
      }

      if (postBody) {
        include_content_length = true;
        content_length = postBody->size();
      }

      std::vector<uint8_t> headers = qpack::Encoder::encodeRequestHeaders(
        method, scheme, authority.str(), path, extra, include_content_length, content_length);

      std::vector<uint8_t> stream_payload;
      h3AppendFrame(stream_payload, 0x01, headers);

      if (postBody && !postBody->empty()) {
        std::vector<uint8_t> body(postBody->begin(), postBody->end());
        h3AppendFrame(stream_payload, 0x00, body);
      }

      uint64_t stream_id = q->openStream(true);
      q->sendStreamData(stream_id, stream_payload, true);

      std::vector<uint8_t> raw;
      std::vector<char> body;
      bool got_headers = false;
      size_t expected_body = static_cast<size_t>(-1);
      int status_code = 0;
      std::string location;
      bool in_data_frame = false;
      uint64_t data_remaining = 0;
      size_t raw_off = 0; // consumed offset into raw (avoids erase-from-front)

      auto last_data = std::chrono::steady_clock::now();
      auto last_activity = last_data;
      const auto idle_timeout = std::chrono::seconds(15);
      const auto response_timeout = std::chrono::seconds(60);

      // Helper: drain all available stream data into raw
      auto drainStream = [&]() {
        uint8_t buf[65536];
        bool got_any = false;
        for (;;) {
          size_t n = q->recvStreamData(stream_id, buf, sizeof(buf));
          if (n == 0) break;
          raw.insert(raw.end(), buf, buf + n);
          got_any = true;
        }
        if (got_any) {
          auto now = std::chrono::steady_clock::now();
          last_data = now;
          last_activity = now;
        }
        return got_any;
      };

      // Helper: pump QUIC network layer (process incoming UDP datagrams)
      auto pumpQuic = [&]() {
        bool pumped_any = false;
        for (int pump_i = 0; pump_i < 256; ++pump_i) {
          try {
            q->pumpNetwork(MSG_DONTWAIT);
            pumped_any = true;
          } catch (netplus::NetException &e) {
            if (e.getErrorType() != netplus::NetException::Note) {
              HTTPException ee;
              ee[HTTPException::Error] << e.what();
              throw ee;
            }
            break;
          }
        }
        return pumped_any;
      };

      for (;;) {
        // 1) Drain all currently buffered stream data
        if (!drainStream()) {
          // No data available — pump network and try again
          pumpQuic();
          drainStream();
        }

        // 2) Parse frames from raw buffer (using offset to avoid erase-from-front)
        size_t pos = raw_off;

        // Consume partial DATA frame continuation
        if (in_data_frame && pos < raw.size()) {
          size_t avail = raw.size() - pos;
          size_t take = (std::min)(avail, static_cast<size_t>(data_remaining));
          body.insert(body.end(), raw.begin() + static_cast<ptrdiff_t>(pos),
                raw.begin() + static_cast<ptrdiff_t>(pos + take));
          pos += take;
          data_remaining -= take;
          if (data_remaining == 0)
            in_data_frame = false;
        }
        while (pos < raw.size() && !in_data_frame) {
          size_t frame_start = pos;
          size_t bytes = 0;
          uint64_t frame_type = h3DecodeVarInt(raw.data() + pos, raw.size() - pos, bytes);
          if (bytes == 0) break;
          pos += bytes;
          if (pos >= raw.size()) { pos = frame_start; break; }

          uint64_t frame_len = h3DecodeVarInt(raw.data() + pos, raw.size() - pos, bytes);
          if (bytes == 0) { pos = frame_start; break; }
          pos += bytes;

          if (frame_type == 0x00) { // DATA — handle incrementally
            size_t avail = raw.size() - pos;
            size_t take = (std::min)(avail, static_cast<size_t>(frame_len));
            body.insert(body.end(), raw.begin() + static_cast<ptrdiff_t>(pos),
                  raw.begin() + static_cast<ptrdiff_t>(pos + take));
            pos += take;
            if (take < frame_len) {
              in_data_frame = true;
              data_remaining = frame_len - take;
            }
          } else if (pos + frame_len > raw.size()) {
            pos = frame_start; break;
          } else if (frame_type == 0x01) {
            auto decoded = qpack::Decoder::decode(raw.data() + pos, frame_len);
            for (const auto &h : decoded) {
              if (h.name == ":status") {
                try { status_code = std::stoi(h.value); } catch (...) { status_code = 0; }
              } else if (h.name == "location") {
                location = h.value;
              } else if (h.name == "content-length") {
                try {
                  expected_body = static_cast<size_t>(std::stoul(h.value));
                  body.reserve(expected_body);
                } catch (...) {}
              }
            }
            got_headers = true;
            pos += frame_len;
          } else {
            pos += frame_len;
          }
        }

        // 3) Compact: drop consumed bytes only when offset is large
        raw_off = pos;
        if (raw_off > 0 && (raw_off >= raw.size() || raw_off > 65536)) {
          raw.erase(raw.begin(), raw.begin() + static_cast<ptrdiff_t>(raw_off));
          raw_off = 0;
        }

        // 4) Check completion conditions
        if (got_headers && expected_body != static_cast<size_t>(-1) && body.size() >= expected_body) {
          ret = std::move(body);
          break;
        }

        if (got_headers && expected_body == static_cast<size_t>(-1)
            && q->isStreamComplete(stream_id)
            && !q->hasStreamData(stream_id)
            && raw_off >= raw.size()) {
          ret = std::move(body);
          break;
        }

        auto now = std::chrono::steady_clock::now();
        if (now - last_activity > idle_timeout) {
          HTTPException ee;
          ee[HTTPException::Error] << "HTTP/3 connection idle timeout";
          throw ee;
        }
        if (now - last_data > response_timeout) {
          HTTPException ee;
          ee[HTTPException::Error] << "HTTP/3 response timeout";
          throw ee;
        }

        // Wait for socket readability only when truly idle
        if (!q->hasStreamData(stream_id) && !q->isStreamComplete(stream_id)) {
          _sw.waitRead(*_cltsock, 50);
        }
      }

      if (is_redirect_status(status_code) && redirects < kMaxRedirects && !location.empty()) {
        ++redirects;
        if (location.rfind("http://", 0) == 0 || location.rfind("https://", 0) == 0) {
          _url = HttpUrl(location, false);
          nreq.setRequestURL(_url.getPath());
        } else {
          std::string base = nreq.getRequestURL().empty() ? _url.getPath() : nreq.getRequestURL();
          nreq.setRequestURL(normalize_path(location, base));
        }

        resetConnection();
        q = dynamic_cast<netplus::quic*>(_cltsock.get());
        if (!q) {
          return ret;
        }
        continue;
      }

      return ret;
    }
  }

const std::vector<char> libhttppp::HttpClient::Get(libhttppp::HttpRequest &nreq, size_t maxTries)
{
    size_t tries = 0;
    for (;;) {
        try {
            // Reuse existing connection if possible (avoid expensive TLS handshakes)
            _ensureConnected();

            for (;;) {
                std::stringstream host;
                host << _url.getHost() << ":" << _url.getPort();

                nreq.setHeaderData("host")->push_back(host.str());
                nreq.setRequestType(GETREQUEST);

                if (nreq.getRequestURL().empty())
                    nreq.setRequestURL(_url.getPath());

                if (dynamic_cast<netplus::quic*>(_cltsock.get())) {
                  return _h3Request("GET", nreq);
                }

                if (_isH2) {
                    return _h2Request("GET", nreq);
                }

                // HTTP/1.x path
                if (nreq.getRequestVersion().empty())
                    nreq.setRequestVersion(HTTPVERSION(1.1));

                // Send request header
                {
                    std::string header;
                    nreq.printHeader(header);
                    if (!_cltsock || _cltsock->fd() < 0)
                        resetConnection();
                    _sendAll(header);
                }

                // Read and parse response using shared helper
                std::vector<char> ret = _h1ReadResponse("GET");

                return ret;
            }

        } catch (netplus::NetException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries) {
                libhttppp::HTTPException ee;
                ee[libhttppp::HTTPException::Error] << e.what();
                throw ee;
            }
            resetConnection();
        } catch (HTTPException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries)
                throw;
            resetConnection();
        }
    }
}

const std::vector<char> libhttppp::HttpClient::Post(libhttppp::HttpRequest &nreq,
                                                    const std::vector<char> &post, size_t maxTries)
{
    size_t tries = 0;
    for (;;) {
        try {
            // Reuse existing connection if possible
            _ensureConnected();

            std::stringstream host;
            host << _url.getHost() << ":" << _url.getPort();

            nreq.setHeaderData("host")->push_back(host.str());
            nreq.setRequestType(POSTREQUEST);
            if (nreq.getRequestURL().empty())
                nreq.setRequestURL(_url.getPath());

            nreq.setHeaderData("content-length")->push_back(std::to_string(post.size()));

            if (dynamic_cast<netplus::quic*>(_cltsock.get())) {
                return _h3Request("POST", nreq, &post);
            }

            if (_isH2) {
                return _h2Request("POST", nreq, &post);
            }

            // HTTP/1.x path
            if (nreq.getRequestVersion().empty())
                nreq.setRequestVersion(HTTPVERSION(1.1));

            // Send header + body together for best TCP segment utilization
            {
                std::string header;
                nreq.printHeader(header);
                if (!_cltsock || _cltsock->fd() < 0)
                    resetConnection();

                std::vector<char> combined;
                combined.reserve(header.size() + post.size());
                combined.insert(combined.end(), header.begin(), header.end());
                combined.insert(combined.end(), post.begin(), post.end());
                _sendAll(combined.data(), combined.size());
            }

            return _h1ReadResponse("POST");

        } catch (netplus::NetException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries) {
                libhttppp::HTTPException ee;
                ee[libhttppp::HTTPException::Error] << e.what();
                throw ee;
            }
            resetConnection();
        } catch (HTTPException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries)
                throw;
            resetConnection();
        }
    }
}

const std::vector<char> libhttppp::HttpClient::Delete(libhttppp::HttpRequest &nreq, size_t maxTries)
{
    size_t tries = 0;
    for (;;) {
        try {
            // Reuse existing connection if possible
            _ensureConnected();

            std::stringstream host;
            host << _url.getHost() << ":" << _url.getPort();

            nreq.setHeaderData("host")->push_back(host.str());
            nreq.setRequestType(DELETEREQUEST);

            if (nreq.getRequestURL().empty())
                nreq.setRequestURL(_url.getPath());

            if (dynamic_cast<netplus::quic*>(_cltsock.get())) {
                return _h3Request("DELETE", nreq);
            }

            if (_isH2) {
                return _h2Request("DELETE", nreq);
            }

            // HTTP/1.x path
            if (nreq.getRequestVersion().empty())
                nreq.setRequestVersion(HTTPVERSION(1.1));

            {
                std::string header;
                nreq.printHeader(header);
                if (!_cltsock || _cltsock->fd() < 0)
                    resetConnection();
                _sendAll(header);
            }

            return _h1ReadResponse("DELETE");

        } catch (netplus::NetException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries) {
                libhttppp::HTTPException ee;
                ee[libhttppp::HTTPException::Error] << e.what();
                throw ee;
            }
            resetConnection();
        } catch (HTTPException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries)
                throw;
            resetConnection();
        }
    }
}

const std::vector<char> libhttppp::HttpClient::Put(libhttppp::HttpRequest &nreq,
                                                   const std::vector<char> &put, size_t maxTries)
{
    size_t tries = 0;
    for (;;) {
        try {
            _ensureConnected();

            std::stringstream host;
            host << _url.getHost() << ":" << _url.getPort();

            nreq.setHeaderData("host")->push_back(host.str());
            nreq.setRequestType(PUTREQUEST);
            if (nreq.getRequestURL().empty())
                nreq.setRequestURL(_url.getPath());

            nreq.setHeaderData("content-length")->push_back(std::to_string(put.size()));

            if (dynamic_cast<netplus::quic*>(_cltsock.get())) {
                return _h3Request("PUT", nreq, &put);
            }

            if (_isH2) {
                return _h2Request("PUT", nreq, &put);
            }

            // HTTP/1.x path
            if (nreq.getRequestVersion().empty())
                nreq.setRequestVersion(HTTPVERSION(1.1));

            // Send header + body together
            {
                std::string header;
                nreq.printHeader(header);
                if (!_cltsock || _cltsock->fd() < 0)
                    resetConnection();

                std::vector<char> combined;
                combined.reserve(header.size() + put.size());
                combined.insert(combined.end(), header.begin(), header.end());
                combined.insert(combined.end(), put.begin(), put.end());
                _sendAll(combined.data(), combined.size());
            }

            return _h1ReadResponse("PUT");

        } catch (netplus::NetException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries) {
                libhttppp::HTTPException ee;
                ee[libhttppp::HTTPException::Error] << e.what();
                throw ee;
            }
            resetConnection();
        } catch (HTTPException &e) {
            ++tries;
            if (maxTries != 0 && tries >= maxTries)
                throw;
            resetConnection();
        }
    }
}

int libhttppp::HttpClient::readchunk(const char *data, int datasize, int &pos)
{
    // 1) Skip CRLF left after previous chunk
    while (pos < datasize && (data[pos] == '\r' || data[pos] == '\n')) {
        ++pos;
    }

    // 2) pos is now at first hex digit of size line (or end)
    int start = pos;

    // Find end of size line (up to CR)
    while (pos < datasize && data[pos] != '\r') {
        ++pos;
    }

    if (pos >= datasize) {
        libhttppp::HTTPException ee;
        ee[libhttppp::HTTPException::Error]
            << "nginxfiler: invalid chunked encoding (incomplete size line)";
        throw ee;
    }

    int len = pos - start;
    if (len <= 0 || len >= 512) {
        libhttppp::HTTPException ee;
        ee[libhttppp::HTTPException::Error]
            << "nginxfiler: invalid chunk size line length: " << len;
        throw ee;
    }

    char value[512];
    std::memcpy(value, data + start, len);
    value[len] = '\0';

    // Strip chunk extensions: "d57;foo=bar" -> "d57"
    if (char *semi = std::strchr(value, ';')) {
        *semi = '\0';
    }

    char *endptr = nullptr;
    long result = std::strtol(value, &endptr, 16);
    if (endptr == value || result < 0) {
        libhttppp::HTTPException ee;
        ee[libhttppp::HTTPException::Error]
            << "nginxfiler: invalid hex chunk size: '" << value << "'";
        throw ee;
    }

    // 3) Skip CRLF after size line
    if (pos + 1 >= datasize || data[pos] != '\r' || data[pos + 1] != '\n') {
        libhttppp::HTTPException ee;
        ee[libhttppp::HTTPException::Error]
            << "nginxfiler: invalid chunked encoding (missing CRLF after size)";
        throw ee;
    }
    pos += 2;  // NOW pos points at FIRST BYTE OF CHUNK DATA

    return static_cast<int>(result); // 0 for final "0\r\n\r\n"
}



libhttppp::HttpHeader::HttpHeader(){
  _firstHeaderData=nullptr;
  _lastHeaderData=nullptr;
}

void libhttppp::HttpHeader::HeaderData::clear(){
  _firstValue.reset();
}


libhttppp::HttpHeader::HeaderData* libhttppp::HttpHeader::getfirstHeaderData(){
  return _firstHeaderData.get();
}

libhttppp::HttpHeader::HeaderData * libhttppp::HttpHeader::HeaderData::nextHeaderData(){
  return _nextHeaderData.get();
}

libhttppp::HttpHeader::HeaderData *libhttppp::HttpHeader::getHeaderData(const std ::string &key) const{
  HeaderData *curdat =_firstHeaderData.get();
  while(curdat){
    if(curdat->_Key==key){
      return curdat;
    }
    curdat=curdat->_nextHeaderData.get();
  }
  return nullptr;
}

libhttppp::HttpHeader::HeaderData * libhttppp::HttpHeader::setHeaderData(const std ::string &key){
  HeaderData *curdat =_firstHeaderData.get();
  if(!curdat){
    _firstHeaderData=std::make_unique<HeaderData>(key);
    _lastHeaderData=_firstHeaderData.get();
    curdat=_lastHeaderData;
  }else{
    while(curdat){
      if(curdat->_Key==key){
        curdat->clear();
        curdat->_Key=key;
        break;
      }
      curdat=curdat->_nextHeaderData.get();
    }
    if(!curdat){
      _lastHeaderData->_nextHeaderData=std::make_unique<HeaderData>(key);
      _lastHeaderData=_lastHeaderData->_nextHeaderData.get();
      curdat=_lastHeaderData;
    }
  }

  return curdat;
}

void libhttppp::HttpHeader::deldata(const std::string& key){
  std::unique_ptr<HeaderData>* link = &_firstHeaderData;

  while (link && link->get()) {
    HeaderData* node = link->get();
    if (node->_Key == key) {
      // unlink
      *link = std::move(node->_nextHeaderData);
      _lastHeaderData = _firstHeaderData.get();
      for (HeaderData* cur = _lastHeaderData; cur && cur->_nextHeaderData; cur = cur->_nextHeaderData.get())
        _lastHeaderData = cur;
      continue;
    }
    link = &node->_nextHeaderData;
  }
  if (!_firstHeaderData) _lastHeaderData = nullptr;
}


void libhttppp::HttpHeader::deldata(libhttppp::HttpHeader::HeaderData* pos){
  if (!pos) return;
  std::unique_ptr<HeaderData>* link = &_firstHeaderData;
  while (link && link->get()) {
    if (link->get() == pos) {
      *link = std::move(pos->_nextHeaderData);
      break;
    }
    link = &link->get()->_nextHeaderData;
  }
  _lastHeaderData = _firstHeaderData.get();
  for (HeaderData* cur = _lastHeaderData; cur && cur->_nextHeaderData; cur = cur->_nextHeaderData.get())
    _lastHeaderData = cur;
  if (!_firstHeaderData) _lastHeaderData = nullptr;
}



size_t libhttppp::HttpHeader::getElements(){
  size_t elements = 0;
  for (HeaderData* cur = _firstHeaderData.get(); cur; cur = cur->_nextHeaderData.get()) {
    ++elements;
  }
  return elements;
}

size_t libhttppp::HttpHeader::getHeaderSize(){
  size_t hsize = 0;
  for (HeaderData* cur = _firstHeaderData.get(); cur; cur = cur->_nextHeaderData.get()) {
    hsize += cur->_Key.length();
    for (HeaderData::Values* v = cur->_firstValue.get(); v; v = v->nextvalue()) {
      hsize += v->getvalue().length();
    }
  }
  return hsize;
}

void libhttppp::HttpHeader::clear(){
  _firstHeaderData.reset();
  _lastHeaderData=nullptr;
}

void libhttppp::HttpHeader::HeaderData::erase(int /*pos*/){
}

void libhttppp::HttpHeader::HeaderData::push_back(const char* val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}

void libhttppp::HttpHeader::HeaderData::push_back(const Values& val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}


void libhttppp::HttpHeader::HeaderData::push_back(const std::string &val){
  if(_firstValue==nullptr){
    _firstValue=std::make_unique<Values>(val);
    _lastValue=_firstValue.get();
  }else{
    _lastValue->_nextvalue=std::make_unique<Values>(val);
    _lastValue=_lastValue->_nextvalue.get();
  }
}

void libhttppp::HttpHeader::HeaderData::push_back(int val){
  std::stringstream ss;
  ss << val;
  push_back(ss.str());
}

void libhttppp::HttpHeader::HeaderData::push_back(size_t val){
  std::stringstream ss;
  ss << val;
  push_back(ss.str());
}



bool libhttppp::HttpHeader::HeaderData::empty(){
  if(!_firstValue && _Key.empty())
    return true;
  return false;
}

const std ::string &libhttppp::HttpHeader::HeaderData::getkey(){
  return _Key;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::at(int pos){
  size_t cpos=0;
  for(Values *cval=_firstValue.get(); cval; cval=cval->nextvalue()){
    if(cpos==static_cast<size_t>(pos))
      return *cval;
    ++cpos;
  }
  HTTPException excep;
  excep[HTTPException::Error] << "no headerdata at:" << pos <<" out of memory";
  throw excep;
}

libhttppp::HttpHeader::HeaderData::Values & libhttppp::HttpHeader::HeaderData::operator[](int pos){
  return at(pos);
}

libhttppp::HttpHeader::HeaderData::HeaderData(const std ::string &key){
  HTTPException excep;
  if(key.empty()){
    excep[HTTPException::Error] << "no headerdata key set can't do this";
    throw excep;
  }
  for(size_t i=0; i<key.length(); ++i){
    _Key.push_back(tolower(key[i]));
  }
  _nextHeaderData=nullptr;
  _firstValue=nullptr;
  _lastValue=nullptr;
}

libhttppp::HttpHeader::HeaderData::Values * libhttppp::HttpHeader::HeaderData::getfirstValue(){
  return _firstValue.get();
}

libhttppp::HttpHeader::HeaderData::Values * libhttppp::HttpHeader::HeaderData::Values::nextvalue(){
  return _nextvalue.get();
}

libhttppp::HttpHeader::HeaderData::Values& libhttppp::HttpHeader::HeaderData::Values::operator<<(const std::string &value) {
  if (value.empty())
    return *this;
  _value.append(value);
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator<<(size_t value){
  _value.append(std::to_string(value));
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator<<(int value){
  _value.append(std::to_string(value));
  return *this;
}

const std::string& libhttppp::HttpHeader::HeaderData::Values::getvalue(){
  return _value;
}


int libhttppp::HttpHeader::HeaderData::Values::getIntvalue(){
  std::istringstream iss(_value);
  int size;
  iss >> size;
  return size;
}

size_t libhttppp::HttpHeader::HeaderData::Values::getSizetValue(){
  std::istringstream iss(_value);
  size_t size;
  iss >> size;
  return size;
}


libhttppp::HttpHeader::HeaderData::Values& libhttppp::HttpHeader::HeaderData::Values::operator=(const std::string &val) {
  _value=val;
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator=(size_t val){
  std::stringstream ss;
  ss << val;
  *this=ss.str();
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values &libhttppp::HttpHeader::HeaderData::Values::operator=(int val){
  std::stringstream ss;
  ss << val;
  *this=ss.str();
  return *this;
}

libhttppp::HttpHeader::HeaderData::Values & libhttppp::HttpHeader::HeaderData::Values::operator=(const Values& val){
  if (this == &val) return *this;
  _value = val._value;
  if (val._nextvalue) {
    _nextvalue = std::make_unique<Values>(*val._nextvalue);
  } else {
    _nextvalue.reset();
  }
  return *this;
}


libhttppp::HttpHeader::HeaderData::Values::Values(const std::string& val) {
  _value=val;
}

libhttppp::HttpHeader::HeaderData::Values::Values(const Values& val)
: _value(val._value)
{
  if (val._nextvalue) {
    _nextvalue = std::make_unique<Values>(*val._nextvalue);
  }
}

libhttppp::HttpResponse::HttpResponse() : HttpHeader(){
  setState(HTTP200);
  _StatusCode = 200;
  _ContentType=nullptr;
  _ContentLength=setHeaderData("content-length");
  _ContentLength->push_back(0);
  _Connection=setHeaderData("connection");
  _Connection->push_back("keep-alive");
  _TransferEncoding=nullptr;
}

libhttppp::HttpResponse::HttpResponse(const HttpResponse &src) : HttpResponse(){
  _State = src._State;
  _Version = src._Version;
  _StatusCode = src._StatusCode;
  for(HttpHeader::HeaderData *curdat=src._firstHeaderData.get(); curdat; curdat=curdat->nextHeaderData()){
    HttpHeader::HeaderData  *newdat=setHeaderData(curdat->getkey());
    for(HttpHeader::HeaderData::Values *curval=curdat->getfirstValue(); curval; curval=curval->nextvalue()){
      newdat->push_back(curval->getvalue());
    }
  }
}

void libhttppp::HttpResponse::setState(const std ::string &httpstate){
  HTTPException httpexception;
  if(httpstate.empty()){
    httpexception[HTTPException::Error] << "http state not set don't do that !!!";
    throw httpexception;
  }
  size_t hlen=httpstate.length();
  if(hlen>254){
    httpexception[HTTPException::Error] << "http state with over 255 signs sorry your are drunk !";
    throw httpexception;
  }
  _State=httpstate;
}

void libhttppp::HttpResponse::setContentLength(size_t len){
  _ContentLength->at(0)=len;
}

void libhttppp::HttpResponse::setContentType(const std::string  &type){
  _ContentType=setHeaderData("content-type");
  _ContentType->push_back(type);

}

void libhttppp::HttpResponse::setConnection(const std::string  & type){
  _Connection=setHeaderData("connection");
  _Connection->push_back(type);
}

const std ::string &libhttppp::HttpResponse::getState() const{
  return _State;
}

int libhttppp::HttpResponse::getStatusCode() const{
  return _StatusCode;
}

size_t libhttppp::HttpResponse::getContentLength() const{
  if(!_ContentLength || _ContentLength->empty())
    return 0;

  return _ContentLength->at(0).getSizetValue();
}

const std ::string &libhttppp::HttpResponse::getContentType() const{
  static const std::string empty;
  if (!_ContentType) {
    return empty;
  }
  auto *first = _ContentType->getfirstValue();
  if (!first) {
    return empty;
  }

  const std::string &raw = first->getvalue();
  if (raw.empty()) {
    return raw;
  }

  size_t end = raw.find(';');
  if (end == std::string::npos) {
    end = raw.size();
  }
  size_t start = 0;
  while (start < end && std::isspace(static_cast<unsigned char>(raw[start]))) {
    ++start;
  }
  while (end > start && std::isspace(static_cast<unsigned char>(raw[end - 1]))) {
    --end;
  }

  _ContentTypeCache.assign(raw, start, end - start);
  std::transform(_ContentTypeCache.begin(), _ContentTypeCache.end(), _ContentTypeCache.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  return _ContentTypeCache;
}

size_t libhttppp::HttpResponse::printHeader(std::vector<char> &buffer){

  // Pre-estimate header size to avoid repeated reallocations
  size_t estimatedSize = _Version.size() + 1 + _State.size() + 2;
  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    estimatedSize += curdat->getkey().size() + 2; // "key: "
    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      estimatedSize += cval->getvalue().size() + 2; // "value; "
    }
    estimatedSize += 2; // "\r\n"
  }
  estimatedSize += 2; // trailing "\r\n"
  buffer.reserve(buffer.size() + estimatedSize);

  auto append=[&buffer](const std::string &data){
    buffer.insert(buffer.end(), data.begin(), data.end());
  };

  append(_Version);
  append(" ");
  append(_State);
  append("\r\n");

  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    append(curdat->getkey());
    append(": ");

    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      append(cval->getvalue());
      if(cval->nextvalue())
        append("; ");
    }
    append("\r\n");
  }

  append("\r\n");
  return buffer.size();
}

bool libhttppp::HttpResponse::_storeResponseInfo(netplus::con &curconnection, int datalen) {
  auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection);
  if (!req || (req->_httpProtocol != 1 && req->_httpProtocol != 2))
    return false;

  // Store response pseudo-headers and metadata into request's _firstHeaderData
  // using :res-status, :res-content-type, :res-content-length, :res-valid keys

  auto *hValid = req->setHeaderData(":res-valid");
  if (hValid) hValid->push_back("1");

  // Extract status code from state string (e.g. "200 OK" -> 200)
  auto *hStatus = req->setHeaderData(":res-status");
  if (hStatus) hStatus->push_back(_State);

  // Content type
  if (_ContentType) {
    auto *v = _ContentType->getfirstValue();
    if (v) {
      auto *hCT = req->setHeaderData(":res-content-type");
      if (hCT) hCT->push_back(v->getvalue());
    }
  }

  // Content length
  {
    size_t cl = (datalen >= 0) ? static_cast<size_t>(datalen) : getContentLength();
    auto *hCL = req->setHeaderData(":res-content-length");
    if (hCL) hCL->push_back(cl);
  }

  // Collect extra headers (skip ones handled separately or invalid for H2/H3)
  for (HeaderData *hd = getfirstHeaderData(); hd; hd = hd->nextHeaderData()) {
    const std::string &key = hd->getkey();
    if (key == "content-type" || key == "content-length" ||
        key == "connection" || key == "transfer-encoding") continue;
    std::string rkey = ":res-" + key;
    auto *rhd = req->setHeaderData(rkey);
    if (rhd) {
      for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) {
        rhd->push_back(v->getvalue());
      }
    }
  }

  return true;
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const std::string &data, int datalen){
  if(datalen==0)
    datalen=data.size();

  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly,
  // write only body data to SendData (no H1 text round-trip)
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0)
      curconnection.SendData.append(data.data(), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0)
    curconnection.SendData.append(data.data(),datalen);
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const unsigned  char *data,int datalen){
  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0 && data)
      curconnection.SendData.append(reinterpret_cast<const char*>(data), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0 && data) {
    curconnection.SendData.append(reinterpret_cast<const char*>(data), datalen);
  }
}

void libhttppp::HttpResponse::send(netplus::con &curconnection,const std::vector<char> &data, int datalen){

  if(datalen==0)
    datalen=data.size();

  if(datalen>=0){
    setContentLength(datalen);
  }

  if (auto *req = dynamic_cast<libhttppp::HttpRequest*>(&curconnection)) {
    const std::string &reqver = req->getRequestVersion();
    if (!reqver.empty()) {
      _Version = reqver;
    }
  }

  // For HTTP/2 and HTTP/3: store structured response info directly
  if (_storeResponseInfo(curconnection, datalen)) {
    if (datalen > 0)
      curconnection.SendData.append(data.data(), datalen);
    return;
  }

  std::vector<char> header;

  printHeader(header);

  curconnection.SendData.append(header.data(),header.size());

  if (datalen > 0)
    curconnection.SendData.append(data.data(),datalen);
}

size_t libhttppp::HttpResponse::parse(const char *data, size_t inlen) {
    HTTPException excep;

    if (inlen < 16) {
        excep[HTTPException::Error] << "HttpResponse header too small aborting!";
        throw excep;
    }

    // --- helpers: trim ---
    auto ltrim_inplace = [](std::string& s) {
        size_t i = 0;
        while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
        if (i) s.erase(0, i);
    };
    auto rtrim_inplace = [](std::string& s) {
        size_t i = s.size();
        while (i && std::isspace(static_cast<unsigned char>(s[i - 1]))) --i;
        if (i != s.size()) s.erase(i);
    };
    auto trim_inplace = [&](std::string& s) {
        rtrim_inplace(s);
        ltrim_inplace(s);
    };

    // --- 1) find end of header: "\r\n\r\n" ---
    const char* header_end_marker = "\r\n\r\n";
    const char* end_ptr = data + inlen - 4;
    const char* header_end_pos = nullptr;

    for (const char* p = data; p < end_ptr; ++p) {
        if (std::equal(p, p + 4, header_end_marker)) {
            header_end_pos = p;
            break;
        }
    }

    if (!header_end_pos) {
        excep[HTTPException::Error] << "HttpHeader end not found!";
        throw excep;
    }

    size_t helen = static_cast<size_t>(header_end_pos - data);
    size_t bytes_consumed = helen + 4; // include "\r\n\r\n"

    std::string header_str(data, helen);

    // --- 2) parse status line ---
    size_t line_end = header_str.find("\r\n");
    if (line_end == std::string::npos) {
        excep[HTTPException::Error] << "Can't find end of Status Line!";
        throw excep;
    }

    std::string status_line = header_str.substr(0, line_end);

    size_t v_start = 0;
    size_t v_end = status_line.find(' ', v_start);
    if (v_end == std::string::npos || status_line.substr(0, 5) != "HTTP/") {
        excep[HTTPException::Error] << "HttpResponse no Version Tag found or malformed Status Line!";
        throw excep;
    }

    _Version = status_line.substr(v_start, v_end - v_start);

    // status code
    v_start = v_end + 1;
    v_end = status_line.find(' ', v_start);
    if (v_end == std::string::npos) {
        v_end = status_line.length();
    }
    std::string status_code_str = status_line.substr(v_start, v_end - v_start);
    try {
      _StatusCode = std::stoi(status_code_str);
    } catch (...) {
      _StatusCode = 0;
    }

    // reason phrase
    v_start = v_end + 1;
    std::string reason_phrase;
    if (v_start < status_line.length()) {
        reason_phrase = status_line.substr(v_start);
    } else {
        reason_phrase.clear();
    }
    _State = reason_phrase;

    // --- 3) parse headers ---
    size_t cursor = line_end + 2; // after "\r\n"

    while (cursor < helen) {
        size_t next_line = header_str.find("\r\n", cursor);
        if (next_line == std::string::npos) {
            next_line = helen;
        }

        std::string hline = header_str.substr(cursor, next_line - cursor);
        cursor = next_line + 2;

        if (hline.empty())
            continue;

        size_t colon = hline.find(':');
        if (colon == std::string::npos) {
            // malformed; skip
            continue;
        }

        std::string key   = hline.substr(0, colon);
        std::string value = (colon + 1 < hline.size()) ? hline.substr(colon + 1) : std::string();

        trim_inplace(key);
        trim_inplace(value);

        // lowercase key
        for (size_t i = 0; i < key.size(); ++i) {
            key[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(key[i])));
        }

        HeaderData* ncontent = setHeaderData(key.c_str());
        if (!ncontent)
            continue;

        // Split value on commas (HTTP list headers: TE, Accept, etc.)
        size_t oldiv = 0;
        size_t iv = 0;

        while ((iv = value.find(',', oldiv)) != std::string::npos) {
            std::string token = value.substr(oldiv, iv - oldiv);
            trim_inplace(token);
            if (!token.empty()) {
                ncontent->push_back(token);
            }
            oldiv = iv + 1;
        }

        // last (or only) token
        std::string final_token = value.substr(oldiv);
        trim_inplace(final_token);
        if (!final_token.empty()) {
            ncontent->push_back(final_token);
        }
    }

    // --- 4) populate cached header pointers ---
    _ContentLength    = getHeaderData("content-length");
    _Connection       = getHeaderData("connection");
    _ContentType      = getHeaderData("content-type");
    _TransferEncoding = getHeaderData("transfer-encoding");

    return bytes_consumed;
}

void libhttppp::HttpResponse::setTransferEncoding(const std ::string &enc){
  _TransferEncoding=setHeaderData("transfer-encoding");
  _TransferEncoding->push_back(enc);
}

libhttppp::HttpHeader::HeaderData::Values *libhttppp::HttpResponse::getTransferEncoding() const{
  if(_TransferEncoding)
    return _TransferEncoding->getfirstValue();
  return nullptr;
}


libhttppp::HttpResponse::~HttpResponse() {

}

libhttppp::HttpRequest::HttpRequest() : HttpHeader() {
  _RequestType=PARSEREQUEST;
  _MaxUploadSize=DEFAULT_UPLOADSIZE;
};

libhttppp::HttpRequest::~HttpRequest() = default;

libhttppp::HttpRequest::HttpRequest(netplus::eventapi *evapi) : HttpHeader(), con(evapi){
  _RequestType=PARSEREQUEST;
  _MaxUploadSize=DEFAULT_UPLOADSIZE;
}

void libhttppp::HttpRequest::clear(){
  HttpHeader::clear();
  _RequestType=PARSEREQUEST;
  _cachedRequestURL.clear();
  _cachedRequest.clear();
  _cachedRequestVersion.clear();
  _httpProtocol = 0;
  // Reset H2 state: release the heap block entirely so that
  // any corrupted deque/map internals are freed as a unit.
  _h2.reset();
}

size_t libhttppp::HttpRequest::parse() {
  switch (_httpProtocol) {
    case 1: // HTTP/2: already parsed via parseH2()
    case 2: // HTTP/3: already parsed via parseH3()
      return 1;
    default:
      return parseH1();
  }
}

std::string libhttppp::HttpRequest::extractPath(const std::string &target) {
  // Strip query
  const size_t q = target.find('?');
  const std::string no_query = (q == std::string::npos) ? target : target.substr(0, q);

  if (no_query.empty())
    return "/";

  // origin-form: starts with "/"
  if (no_query[0] == '/')
    return no_query;

  // asterisk-form: "*"
  if (no_query == "*")
    return "*";

  // absolute-form: "scheme://authority/path"
  const size_t scheme = no_query.find("://");
  if (scheme != std::string::npos) {
    const size_t after_scheme = scheme + 3;
    const size_t slash = no_query.find('/', after_scheme);
    if (slash == std::string::npos)
      return "/";
    return no_query.substr(slash);
  }

  return "/";
}

size_t libhttppp::HttpRequest::parseH2(const std::vector<hpack::HeaderField> &headers,
                                       uint32_t stream_id) {
  clear();
  _httpProtocol = 1;
  auto &h2 = h2state();
  h2.streamId = stream_id;
  h2.headersSent = false;
  h2.bodyBytesSent = 0;
  SendData.pos = 0;

  // Store ALL headers (including pseudo-headers) in _firstHeaderData.
  // Split values by ';' (matching parseH1 behaviour) so that cookie
  // parsing and other code that iterates individual Values works.
  auto trim = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
    if (i) s.erase(0, i);
    size_t e = s.size();
    while (e && (s[e-1] == ' ' || s[e-1] == '\t')) --e;
    if (e != s.size()) s.erase(e);
  };

  for (const auto &h : headers) {
    // Use getHeaderData first so that duplicate headers (e.g. multiple
    // cookie fields per RFC 7540 §8.1.2.5) APPEND values instead of
    // clearing them.  Only create a new entry when the key is new.
    auto *hd = getHeaderData(h.name);
    if (!hd) hd = setHeaderData(h.name);
    if (!hd) continue;
    const std::string &val = h.value;
    size_t oldiv = 0, iv = 0;
    while (iv < val.size()) {
      if (val[iv] == ';') {
        std::string token = val.substr(oldiv, iv - oldiv);
        trim(token);
        if (!token.empty()) hd->push_back(token);
        ++iv;
        while (iv < val.size() && val[iv] == ' ') ++iv;
        oldiv = iv;
      } else {
        ++iv;
      }
    }
    std::string token = val.substr(oldiv);
    trim(token);
    if (!token.empty()) hd->push_back(token);
  }

  // Store :authority as host header too (RFC 7540 §8.1.2.3)
  auto *auth = getHeaderData(":authority");
  if (auth && auth->getfirstValue()) {
    auto *host = setHeaderData("host");
    if (host) host->push_back(auth->getfirstValue()->getvalue());
  }

  // Derive cached fields and request type from stored headers
  auto *methodHd = getHeaderData(":method");
  std::string method = (methodHd && methodHd->getfirstValue()) ? methodHd->getfirstValue()->getvalue() : "GET";
  if (method == "GET")          _RequestType = GETREQUEST;
  else if (method == "POST")    _RequestType = POSTREQUEST;
  else if (method == "PUT")     _RequestType = PUTREQUEST;
  else if (method == "DELETE")  _RequestType = DELETEREQUEST;
  else                          _RequestType = GETREQUEST;

  auto *pathHd = getHeaderData(":path");
  std::string path = (pathHd && pathHd->getfirstValue()) ? pathHd->getfirstValue()->getvalue() : "/";
  _cachedRequest = path;
  _cachedRequestURL = extractPath(path);
  _cachedRequestVersion = "HTTP/2";

  return 1;
}

size_t libhttppp::HttpRequest::parseH3(const std::vector<qpack::HeaderField> &headers) {
  clear();
  _httpProtocol = 2;
  SendData.pos = 0;

  // Store ALL headers (including pseudo-headers) in _firstHeaderData.
  // Split values by ';' (matching parseH1 behaviour) so that cookie
  // parsing and other code that iterates individual Values works.
  auto trim = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
    if (i) s.erase(0, i);
    size_t e = s.size();
    while (e && (s[e-1] == ' ' || s[e-1] == '\t')) --e;
    if (e != s.size()) s.erase(e);
  };

  for (const auto &h : headers) {
    // Use getHeaderData first so that duplicate headers (e.g. multiple
    // cookie fields per RFC 9114 §4.3.1) APPEND values instead of
    // clearing them.  Only create a new entry when the key is new.
    auto *hd = getHeaderData(h.name);
    if (!hd) hd = setHeaderData(h.name);
    if (!hd) continue;
    const std::string &val = h.value;
    size_t oldiv = 0, iv = 0;
    while (iv < val.size()) {
      if (val[iv] == ';') {
        std::string token = val.substr(oldiv, iv - oldiv);
        trim(token);
        if (!token.empty()) hd->push_back(token);
        ++iv;
        while (iv < val.size() && val[iv] == ' ') ++iv;
        oldiv = iv;
      } else {
        ++iv;
      }
    }
    std::string token = val.substr(oldiv);
    trim(token);
    if (!token.empty()) hd->push_back(token);
  }

  // Store :authority as host header too (RFC 9114 §4.3.1)
  auto *auth = getHeaderData(":authority");
  if (auth && auth->getfirstValue()) {
    auto *host = setHeaderData("host");
    if (host) host->push_back(auth->getfirstValue()->getvalue());
  }

  // Derive cached fields and request type from stored headers
  auto *methodHd = getHeaderData(":method");
  std::string method = (methodHd && methodHd->getfirstValue()) ? methodHd->getfirstValue()->getvalue() : "GET";
  if (method == "GET")          _RequestType = GETREQUEST;
  else if (method == "POST")    _RequestType = POSTREQUEST;
  else if (method == "PUT")     _RequestType = PUTREQUEST;
  else if (method == "DELETE")  _RequestType = DELETEREQUEST;
  else                          _RequestType = GETREQUEST;

  auto *pathHd = getHeaderData(":path");
  std::string path = (pathHd && pathHd->getfirstValue()) ? pathHd->getfirstValue()->getvalue() : "/";
  _cachedRequest = path;
  _cachedRequestURL = extractPath(path);
  _cachedRequestVersion = "HTTP/3";

  return 1;
}

size_t libhttppp::HttpRequest::parseH1() {
  HTTPException excep;
  std::vector<char> header;

  // small helpers
  auto ltrim_inplace = [](std::string& s) {
    size_t i = 0;
    while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
    if (i) s.erase(0, i);
  };
  auto rtrim_inplace = [](std::string& s) {
    size_t i = s.size();
    while (i && std::isspace(static_cast<unsigned char>(s[i-1]))) --i;
    if (i != s.size()) s.erase(i);
  };
  auto trim_inplace = [&](std::string& s) { rtrim_inplace(s); ltrim_inplace(s); };

  try {
    // 1) Find end of headers: "\r\n\r\n"
    size_t endpos = RecvData.search("\r\n\r\n");
    if (endpos == std::string::npos) {
      // Not enough data yet
      _RequestType = PARSEREQUEST;
      return 0;
    }

    // 2) Copy header bytes (include the first \r\n of the separator so
    //    every header line, including the last one, ends with \r\n)
    header.assign(RecvData.begin(), RecvData.begin() + endpos + 2);

    // 3) Consume header (and the delimiter) from RecvData
    endpos += 4;            // include "\r\n\r\n"
    RecvData.erase(RecvData.begin(),RecvData.begin()+endpos);

    // 4) Parse the request line (first line until CRLF)
    size_t line_end = 0;
    while (line_end < header.size() && header[line_end] != '\r') ++line_end;
    if (line_end == 0 || line_end >= header.size()) {
      excep[HTTPException::Error] << "can't parse http head";
      throw excep;
    }

    // request line as string
    std::string reqline(header.begin(), header.begin() + line_end);
    // advance past "\r\n"
    size_t cursor = line_end + 2;

    // split request line into method, target, version
    std::string method, target, version;
    {
      size_t a = 0;
      // method
      size_t sp1 = reqline.find(' ', a);
      if (sp1 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (no spaces)";
        throw excep;
      }
      method = reqline.substr(a, sp1 - a);

      // target
      size_t sp2 = reqline.find(' ', sp1 + 1);
      if (sp2 == std::string::npos) {
        excep[HTTPException::Error] << "invalid request line (missing HTTP version)";
        throw excep;
      }
      target = reqline.substr(sp1 + 1, sp2 - (sp1 + 1));

      // version
      version = reqline.substr(sp2 + 1);
      trim_inplace(version);
    }

    // 5) Set request type
    if (method == "GET")          _RequestType = GETREQUEST;
    else if (method == "POST")    _RequestType = POSTREQUEST;
    else if (method == "PUT")     _RequestType = PUTREQUEST;
    else if (method == "DELETE")  _RequestType = DELETEREQUEST;
    else                          _RequestType = PARSEREQUEST; // or a dedicated enum for others

    // 6) Store pseudo-headers in _firstHeaderData (unified storage)
    {
      auto *hMethod = setHeaderData(":method");
      if (hMethod) hMethod->push_back(method);

      auto *hPath = setHeaderData(":path");
      if (hPath) hPath->push_back(target);

      auto *hVersion = setHeaderData(":version");
      if (hVersion) hVersion->push_back(version);
    }

    // 7) Derive cached fields from stored pseudo-headers
    _cachedRequest = target;
    _cachedRequestVersion = version;
    _cachedRequestURL = extractPath(target);

    size_t line_start = cursor;

    while (line_start < header.size()) {
      // find CR
      size_t cr = line_start;
      while (cr < header.size() && header[cr] != '\r') ++cr;

      // bounds check: ensure we have at least \r\n
      if (cr >= header.size()) {
        // no complete line found, stop parsing
        break;
      }

      // empty line guard (shouldn't occur inside since we cut at \r\n\r\n, but just in case)
      if (cr == line_start) {
        if (cr + 1 < header.size()) {
          line_start = cr + 2; // skip "\r\n"
        } else {
          break;
        }
        continue;
      }

      // extract raw line (without CR)
      std::string hline(header.begin() + line_start, header.begin() + cr);

      // move to next line (skip "\r\n")
      if (cr + 1 < header.size() && header[cr + 1] == '\n') {
        line_start = cr + 2;
      } else if (cr + 1 == header.size()) {
        // only CR at end, no LF
        break;
      } else {
        line_start = cr + 1; // tolerant
      }

      // find delimiter ':'
      size_t colon = hline.find(':');
      if (colon == std::string::npos) {
        // malformed header line; skip or treat as error
        // We'll be tolerant and skip
        continue;
      }

      std::string key = hline.substr(0, colon);
      std::string value = (colon + 1 < hline.size()) ? hline.substr(colon + 1) : std::string();

      // trim spaces around key/value
      trim_inplace(key);
      trim_inplace(value);

      // lowercase key safely
      for (size_t it = 0; it < key.size(); ++it) {
        key[it] = static_cast<char>(std::tolower(static_cast<unsigned char>(key[it])));
      }

      // store header: split value on ';' into tokens (preserving case)
      HeaderData* ncontent = setHeaderData(key);
      if (!ncontent) continue;

      size_t oldiv = 0, iv = 0;
      while (iv < value.size()) {
        if (value[iv] == ';') {
          // substring [oldiv, iv)
          std::string token = value.substr(oldiv, iv - oldiv);
          trim_inplace(token);             // remove leading/trailing spaces
          if (!token.empty()) {
            ncontent->push_back(token);
          }

          ++iv;                            // skip ';'
          while (iv < value.size() && value[iv] == ' ')
            ++iv;                        // skip spaces after ';'
          oldiv = iv;
        } else {
          ++iv;
        }
      }

      // last token (after last ';' or whole value if no ';')
      std::string token = value.substr(oldiv);
      trim_inplace(token);
      if (!token.empty()) {
        ncontent->push_back(token);
      }

    }

  } catch (netplus::NetException& e) {
    if (e.getErrorType() != netplus::NetException::Note) {
      RecvData.clear();
    }
    excep[HTTPException::Error] << "netplus error: " << e.what();
    throw excep;
  } catch (...) {
    // keep behavior similar to your original: on parse error, clean up input
    RecvData.clear();
    throw;
  }

  return header.size();
}

void libhttppp::HttpRequest::printHeader(std::string &buffer){
  if(_RequestType==GETREQUEST)
    buffer="GET ";
  else if(_RequestType==POSTREQUEST)
    buffer="POST ";
  else if(_RequestType==PUTREQUEST)
    buffer="PUT ";
  else if(_RequestType==DELETEREQUEST)
    buffer="DELETE ";

  buffer.append(_cachedRequest);
  buffer.append(" ");
  buffer.append(_cachedRequestVersion);
  buffer.append("\r\n");

  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    // Skip pseudo-headers and response pseudo-headers in H1 output
    const std::string &key = curdat->getkey();
    if (!key.empty() && key[0] == ':') continue;

    buffer.append(key);
    buffer.append(": ");

    for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
      buffer.append(cval->getvalue());
      if(cval->nextvalue())
        buffer.append("; ");
    }
    buffer.append("\r\n");
  }

  buffer.append("\r\n");
}

int libhttppp::HttpRequest::getRequestType(){
  return _RequestType;
}

const std::string &libhttppp::HttpRequest::getRequestURL(){
  if (_cachedRequestURL.empty() && !_cachedRequest.empty()) {
    _cachedRequestURL = extractPath(_cachedRequest);
  }
  if (_cachedRequestURL.empty()) {
    auto *hPath = getHeaderData(":path");
    if (hPath && hPath->getfirstValue()) {
      _cachedRequest = hPath->getfirstValue()->getvalue();
      _cachedRequestURL = extractPath(_cachedRequest);
    }
  }
  return _cachedRequestURL;
}

const std::string &libhttppp::HttpRequest::getRequest(){
  return _cachedRequest;
}

size_t libhttppp::HttpRequest::getRequestLength() {
  return _cachedRequest.length();
}

const std::string & libhttppp::HttpRequest::getRequestVersion(){
  return _cachedRequestVersion;
}

const std::string &libhttppp::HttpRequest::getHost(){
  if(_cachedHost.empty()){
    auto *hHost = getHeaderData("host");
    if(!hHost)
      hHost = getHeaderData(":authority");
    if(hHost && hHost->getfirstValue())
      _cachedHost = hHost->getfirstValue()->getvalue();
  }
  return _cachedHost;
}

size_t libhttppp::HttpRequest::getContentLength(){
  HttpHeader::HeaderData *clen=getHeaderData("content-length");
  if(!clen)
    return 0;
  return clen->at(0).getSizetValue();
}

size_t libhttppp::HttpRequest::getMaxUploadSize(){
  return _MaxUploadSize;
}

bool libhttppp::HttpRequest::isMobile(){
  for(HeaderData *curdat=getfirstHeaderData(); curdat; curdat=curdat->nextHeaderData()){
    if(curdat->getkey()=="user-agent"){
      for(HeaderData::Values *cval=curdat->getfirstValue(); cval; cval=cval->nextvalue()){
        std::string search;
        std::transform(cval->getvalue().begin(), cval->getvalue().end(), std::back_inserter(search),
                       [](unsigned char c){ return std::tolower(c); });
        if(search.find("mobi") != std::string::npos){
          return true;
        }
      }
      break;
    }
  }
  return false;
}

void libhttppp::HttpRequest::setRequestData(const std::string &data, size_t len){
  _cachedRequest.clear();
  _cachedRequest.resize(len);
  _cachedRequest.insert(0,data);
  _cachedRequestURL = extractPath(_cachedRequest);
  // Update :path pseudo-header in _firstHeaderData
  auto *hPath = setHeaderData(":path");
  if (hPath) { hPath->clear(); hPath->push_back(_cachedRequest); }
}

void libhttppp::HttpRequest::setMaxUploadSize(size_t upsize){
  _MaxUploadSize=upsize;
}


void libhttppp::HttpRequest::setRequestVersion(const std::string& version){
  if(!version.empty())
    _cachedRequestVersion=version;
  else
    _cachedRequestVersion.clear();
  // Update :version pseudo-header in _firstHeaderData
  auto *hVer = setHeaderData(":version");
  if (hVer) { hVer->clear(); hVer->push_back(_cachedRequestVersion); }
}

void libhttppp::HttpRequest::send(const HttpUrl &dest,
                                 std::unique_ptr<netplus::socket> &sock)
{
    std::string header;
    printHeader(header);

    if(!sock || sock->fd()<0){
        if(!sock){
            if(dest.getProtocol()==HttpUrl::HTTPS) {
                std::map<std::string, netplus::ssl::CertificateBundle> certs;
                sock=std::make_unique<netplus::ssl>(certs,-1);
            } else {
                sock=std::make_unique<netplus::tcp>(-1);
            }
        }
        sock->connect(dest.getHost(),dest.getPort(),true);
    }
    netplus::buffer buf(header.c_str(),header.length());
    sock->sendData(buf,0);
}

void libhttppp::HttpRequest::setRequestType(int req){
  if(req == POSTREQUEST || req == GETREQUEST || req == PUTREQUEST || req == DELETEREQUEST){
    _RequestType=req;
    return;
  }
  HTTPException excep;
  excep[HTTPException::Error] << "setRequestType: " << "Unknown Request will not set !";
  throw excep;
}

void libhttppp::HttpRequest::setRequestURL(const std::string &url){
  if(!url.empty()){
    _cachedRequest = url;
    _cachedRequestURL = extractPath(url);
  }else{
    _cachedRequest.clear();
    _cachedRequestURL.clear();
  }
  // Update :path pseudo-header in _firstHeaderData with the full target
  auto *hPath = setHeaderData(":path");
  if (hPath) { hPath->clear(); hPath->push_back(_cachedRequest); }
}


void libhttppp::HttpForm::parse(libhttppp::HttpRequest &request){
  // Clear previous results so re-parsing doesn't duplicate entries
  _multipartEntries.clear();
  _urlEntries.clear();
  _boundary.clear();
  _contentType.clear();

  if(request.getRequestType()==POSTREQUEST || request.getRequestType()==PUTREQUEST){
    HttpHeader::HeaderData *ctype=request.getHeaderData("content-type");

    if(!ctype){
      HTTPException excep;
      excep[HTTPException::Error] << "Http Form Parse Error: content type";
      throw excep;
    }

    // Use content-length if available, otherwise fall back to RecvData size
    // (HTTP/2 and HTTP/3 may omit Content-Length)
    size_t bodyLen = request.getContentLength();
    if(bodyLen == 0)
      bodyLen = request.RecvData.size();

    for(auto *cval=ctype->getfirstValue(); cval; cval=cval->nextvalue()){
      if(bodyLen <= request.RecvData.size()){
        if(cval->getvalue().find("multipart/form-data") != std::string::npos){
          _contentType = "multipart/form-data";
          for(auto *cbod=ctype->getfirstValue(); cbod; cbod=cbod->nextvalue()){
            size_t fcbod = cbod->getvalue().find("boundary=");
            if(fcbod != std::string::npos){
              fcbod += 9;
              std::string bval = cbod->getvalue().substr(fcbod);
              if(auto sc = bval.find(';'); sc != std::string::npos)
                bval.resize(sc);
              // trim
              bval.erase(0, bval.find_first_not_of(" \t"));
              bval.erase(bval.find_last_not_of(" \t") + 1);
              // remove quotes
              if(bval.size() >= 2 && bval.front() == '"' && bval.back() == '"')
                bval = bval.substr(1, bval.size() - 2);
              _boundary = std::move(bval);
              _parseMultipart(request.RecvData.data(), request.RecvData.size());
              break;
            }
          }
          break;
        }
        if(cval->getvalue().find("application/x-www-form-urlencoded") != std::string::npos){
          _contentType = "application/x-www-form-urlencoded";
          _parseUrlDecode(request.RecvData.data(), bodyLen);
        }
      }
    }
  }

  // Parse URL query string
  const auto &req = request.getRequest();
  if(auto qpos = req.find('?'); qpos != std::string::npos && qpos + 1 < req.size()){
    _parseUrlDecode(req.c_str() + qpos + 1, req.size() - qpos - 1);
  }
}

void libhttppp::HttpForm::urlEncode(const std::string &in, std::string &out) {
  // Note: Use 'static const' to define the map once if the function is called repeatedly.
  static const std::map<char, std::string_view> codetable = {
    // ... (All your encoding pairs) ...
    // ----------------------------------------------------
    // Unsafe/Form-Encoded Characters (Commonly Replaced)
    // ----------------------------------------------------
    {' ',  "+"},

    // ----------------------------------------------------
    // General Delimiters (Must be encoded if used literally)
    // ----------------------------------------------------
    {'#',  "%23"},
    {'%',  "%25"},
    {'&',  "%26"},
    {'+',  "%2B"},
    {'/',  "%2F"},
    {':',  "%3A"},
    {';',  "%3B"},
    {'=',  "%3D"},
    {'?',  "%3F"},
    {'@',  "%40"},

    // ----------------------------------------------------
    // Sub-Delimiters (Must be encoded if used literally)
    // ----------------------------------------------------
    {'$',  "%24"},
    {',',  "%2C"},
    {'(',  "%28"},
    {')',  "%29"},
    {'*',  "%2A"},
    {'\'', "%27"},

    // ----------------------------------------------------
    // Other Unsafe/Control Characters
    // ----------------------------------------------------
    {'"', "%22"},
    {'<',  "%3C"},
    {'>',  "%3E"},
    {'[',  "%5B"},
    {']',  "%5D"},
    {'^',  "%5E"},
    {'`',  "%60"},
    {'{',  "%7B"},
    {'|',  "%7C"},
    {'}',  "%7D"},
  };

  // Clear the output string and reserve space
  out.clear();
  out.reserve(in.length() * 3); // Reserve space for potential growth

  for (char c : in) {
    // Use map::find to efficiently search for the character 'c'
    auto it = codetable.find(c);

    if (it != codetable.end()) {
      // Character found in the map
      const std::string_view& encoded_value = it->second;

      // --- CORRECTION HERE ---
      // Append the ENTIRE encoded string_view (it->second)
      // if you need the debug print: std::cout << encoded_value;
      out += encoded_value;

    } else {
      // Character not found (unreserved), append it as is
      out += c;
    }
  }
}

void libhttppp::HttpForm::urlDecode(const std::string &in,std::string &out){
    for (auto i = in.begin(), nd = in.end(); i < nd; ++i)
    {
        auto c = ( *i );

        switch(c)
        {
            case '%':
                if ( ( i + 2 < nd ) && ( std::isxdigit(*(i + 1)) && std::isxdigit(*(i + 2)) ) ) {
                    char hs[]{ i[1], i[2], '\0' };
                    char decoded_char = static_cast<char>(strtol(hs, nullptr, 16));

                    if (decoded_char == '\0') {
                        HTTPException e;
                        e[HTTPException::Error] << "Url decoded result contained null character ('%00')!";
                        throw e;
                    }

                    out.push_back(decoded_char);
                    i += 2;
                }else{
                    HTTPException e;
                    e[HTTPException::Error] << "Wrong Decoded Url!";
                    throw e;
                }
                break;
            case '+':
                out.push_back(' ');
                break;
            default:
                out.push_back(c);
        }
    }
}

void libhttppp::HttpForm::_parseMultipart(const char *data, size_t len) {
  // Build "--" + boundary
  const std::string delim = "--" + _boundary;
  const size_t B = delim.size();

  auto matchAt = [&](size_t pos) -> bool {
    if (pos + B > len) return false;
    return std::memcmp(data + pos, delim.c_str(), B) == 0;
  };

  size_t i = 0;
  size_t partStart = std::string::npos;

  // Find first boundary (may appear at offset 0 or after CRLF preamble)
  if (matchAt(0)) {
    size_t p = B;
    if (p + 1 < len && data[p] == '-' && data[p + 1] == '-') return;
    if (p + 1 >= len || data[p] != '\r' || data[p + 1] != '\n') return;
    i = p + 2;
    partStart = i;
  } else {
    while (true) {
      size_t crlf = std::string::npos;
      for (size_t k = i; k + 1 < len; ++k) {
        if (data[k] == '\r' && data[k + 1] == '\n') { crlf = k; break; }
      }
      if (crlf == std::string::npos) return;
      size_t bpos = crlf + 2;
      if (!matchAt(bpos)) { i = bpos; continue; }

      size_t p = bpos + B;
      if (p + 1 < len && data[p] == '-' && data[p + 1] == '-') return;
      if (p + 1 >= len || data[p] != '\r' || data[p + 1] != '\n') return;
      i = p + 2;
      partStart = i;
      break;
    }
  }

  // Now iterate over parts: each ends just before "\r\n--boundary" or "\r\n--boundary--"
  while (true) {
    // Find next delimiter line: look for "\r\n--boundary" from i onward
    size_t delimPos = std::string::npos;
    size_t closePos = std::string::npos;
    // scan for CRLF then boundary
    for (size_t k = i; k + 2 + B <= len; ++k) {
      if (data[k] == '\r' && data[k + 1] == '\n' && matchAt(k + 2)) {
        size_t after = k + 2 + B;
        if (after + 1 < len && data[after] == '-' && data[after + 1] == '-') {
          closePos = k; // boundary that closes the multipart
          break;        // closing takes precedence
        } else {
          delimPos = k; // normal part delimiter
          break;
        }
      }
    }

    if (delimPos == std::string::npos && closePos == std::string::npos) {
      // No terminating boundary found → malformed
      return;
    }

    size_t partEnd = (closePos != std::string::npos) ? closePos : delimPos; // points to CR of the CRLF before boundary

    if (partStart != std::string::npos && partEnd >= partStart) {
      _parseMultiSection(data, len, partStart, partEnd);
    }

    if (closePos != std::string::npos) {
      // Reached closing boundary (“--boundary--”), done
      return;
    }

    // Move i to start of next part: skip CRLF + "--boundary" + CRLF
    size_t after = delimPos + 2 + B;
    if (after + 1 >= len || data[after] != '\r' || data[after + 1] != '\n') {
      // malformed separator
      return;
    }
    i = after + 2;          // start of next part headers
    partStart = i;
  }
}

void libhttppp::HttpForm::_parseMultiSection(const char *data, size_t /*len*/, size_t start, size_t end) {
  if (!data || end <= start) return;

  // Find header/body separator (\r\n\r\n or \n\n)
  size_t headerEnd = std::string::npos;
  size_t sepSize = 4;

  for (size_t i = start; i + 3 < end; ++i) {
    if (data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '\r' && data[i+3] == '\n') {
      headerEnd = i;
      break;
    }
  }

  if (headerEnd == std::string::npos) {
    sepSize = 2;
    for (size_t i = start; i + 1 < end; ++i) {
      if (data[i] == '\n' && data[i+1] == '\n') {
        headerEnd = i;
        break;
      }
    }
  }

  if (headerEnd == std::string::npos || headerEnd + sepSize >= end) return;

  MultipartEntry entry;

  // Parse MIME headers
  size_t lineStart = start;
  while (lineStart < headerEnd) {
    while (lineStart < headerEnd && (data[lineStart] == ' ' || data[lineStart] == '\r' || data[lineStart] == '\n'))
      ++lineStart;
    if (lineStart >= headerEnd) break;

    size_t lineEnd = lineStart;
    while (lineEnd < headerEnd && data[lineEnd] != '\r' && data[lineEnd] != '\n')
      ++lineEnd;

    size_t colon = lineStart;
    while (colon < lineEnd && data[colon] != ':') ++colon;

    if (colon < lineEnd) {
      MultipartEntry::Header hdr;
      hdr.key.assign(data + lineStart, data + colon);
      std::transform(hdr.key.begin(), hdr.key.end(), hdr.key.begin(),
                     [](unsigned char c) { return static_cast<char>(std::tolower(c)); });

      size_t valStart = colon + 1;
      while (valStart < lineEnd && data[valStart] == ' ') ++valStart;
      hdr.value.assign(data + valStart, data + lineEnd);

      entry.headers.push_back(std::move(hdr));
    }

    lineStart = lineEnd;
  }

  // Parse Content-Disposition parameters
  for (const auto &hdr : entry.headers) {
    if (hdr.key != "content-disposition") continue;

    const std::string &dval = hdr.value;
    size_t pos = dval.find("form-data");
    if (pos == std::string::npos) break;

    size_t sc = pos + 9;
    while (sc < dval.size()) {
      while (sc < dval.size() && (dval[sc] == ';' || dval[sc] == ' ')) ++sc;
      if (sc >= dval.size()) break;

      size_t keyStart = sc;
      while (sc < dval.size() && dval[sc] != '=' && dval[sc] != ' ' && dval[sc] != ';') ++sc;
      if (keyStart == sc) break;

      MultipartEntry::Disposition dispo;
      dispo.key = dval.substr(keyStart, sc - keyStart);

      if (sc < dval.size() && dval[sc] == '=') {
        ++sc;
        if (sc < dval.size() && dval[sc] == '"') {
          ++sc;
          size_t valStart = sc;
          while (sc < dval.size() && dval[sc] != '"') ++sc;
          dispo.value = dval.substr(valStart, sc - valStart);
          if (sc < dval.size()) ++sc;
        } else {
          size_t valStart = sc;
          while (sc < dval.size() && dval[sc] != ';' && dval[sc] != ' ') ++sc;
          dispo.value = dval.substr(valStart, sc - valStart);
        }
      }

      entry.dispositions.push_back(std::move(dispo));
    }
    break;
  }

  // Copy body data
  size_t bodyStart = headerEnd + sepSize;
  if (bodyStart < end) {
    entry.value.assign(data + bodyStart, data + end);
  }

  _multipartEntries.push_back(std::move(entry));
}

void libhttppp::HttpForm::_parseUrlDecode(const char *data, size_t csize) {
  size_t fdatstpos = 0;
  size_t keyendpos = 0;

  for (size_t fdatpos = 0; fdatpos <= csize; ++fdatpos) {
    if (fdatpos == csize || data[fdatpos] == '&') {
      if (keyendpos > fdatstpos && keyendpos < fdatpos) {
        std::string ukey(data + fdatstpos, data + keyendpos);
        std::string key;
        urlDecode(ukey, key);

        std::string value;
        size_t vlstpos = keyendpos + 1;
        if (vlstpos <= fdatpos) {
          std::string uvalue(data + vlstpos, data + fdatpos);
          urlDecode(uvalue, value);
        }

        _urlEntries.push_back({std::move(key), std::move(value)});
      }
      fdatstpos = fdatpos + 1;
      keyendpos = 0;
    } else if (data[fdatpos] == '=') {
      keyendpos = fdatpos;
    }
  }
}

libhttppp::HttpCookie::CookieData::~CookieData(){
}

libhttppp::HttpCookie::CookieData * libhttppp::HttpCookie::CookieData::nextCookieData() const{
  return _nextCookieData.get();
}

const std::string &libhttppp::HttpCookie::CookieData::getKey() const{
  return _Key;
}

const std::string &libhttppp::HttpCookie::CookieData::getValue() const{
  return _Value;
}



libhttppp::HttpCookie::HttpCookie(){
  _firstCookieData=nullptr;
  _lastCookieData=nullptr;
}

libhttppp::HttpCookie::~HttpCookie(){
}


void libhttppp::HttpCookie::setcookie(libhttppp::HttpResponse &curresp,
                                      const std::string & key, const std::string &value,
                                      const std::string &comment,const std::string &domain,
                                      int maxage, const std::string & path,
                                      bool secure,const std::string &version,const std::string &samesite,bool httponly){
  HTTPException httpexception;
  if(key.empty()){
    httpexception[HTTPException::Note] << "no key or value set in cookie!";
    throw httpexception;
  }
  HttpHeader::HeaderData *dat=curresp.setHeaderData("set-cookie");

  std::string buf=key;
  buf+="=";
  buf+=value;
  dat->push_back(buf);

  if(!comment.empty()){
    buf="comment=";
    buf+=comment;
    dat->push_back(buf);
  }
  if(!domain.empty()){
    buf="domain=";
    buf+=domain;
    dat->push_back(buf);
  }
  if(maxage>=0){
    std::stringstream ss;
    ss << "max-age=";
    ss << maxage;
    dat->push_back(ss.str());
  }
  if(!path.empty()){
    buf="path=";
    buf+=path;
    dat->push_back(buf);
  }
  if(!samesite.empty()){
    buf="sameSite=";
    buf+=samesite;
    dat->push_back(buf);
  }else{
    buf="sameSite=Lax";
    dat->push_back(buf);
  }
  if(secure){
    buf="secure";
    buf+=secure;
    dat->push_back(buf);
  }
  if(!version.empty()){
    buf="version=";
    buf+=version;
    dat->push_back(buf);
  }

  if(httponly){
    dat->push_back("httponly");
  }
}

void libhttppp::HttpCookie::parse(libhttppp::HttpRequest &curreq){
    HttpHeader::HeaderData *hc = curreq.getHeaderData("cookie");

    if(!hc){
        HTTPException httpexception;
        httpexception[HTTPException::Note] << "no cookies are set!";
        throw httpexception;
    }

    std::string raw_cookie_string = hc->getfirstValue()->getvalue();

    // Helper to trim leading/trailing whitespace
    auto trim = [](std::string& s) {
        s.erase(0, s.find_first_not_of(" \t"));
        s.erase(s.find_last_not_of(" \t") + 1);
    };

    // Loop through the raw string, splitting by semicolon (';')
    for(HttpHeader::HeaderData::Values *curval=curreq.getHeaderData("cookie")->getfirstValue(); curval; curval=curval->nextvalue()){
        std::string cpair = curval->getvalue();
        trim(cpair); // Trim whitespace around the entire pair

        size_t eq_pos = cpair.find("=");

        if (eq_pos != std::string::npos) {
            std::string key = cpair.substr(0, eq_pos);
            std::string value = cpair.substr(eq_pos + 1);

            trim(key);
            // NOTE: Cookie values are generally NOT trimmed, but if you expect quotes,
            // you should handle them here. Trimming spaces for safety.
            trim(value);

            if (!key.empty()) {
                // Assuming CookieData* cdat=addCookieData() exists and works
                CookieData *cdat = addCookieData();
                cdat->_Key = key;
                cdat->_Value = value;
            }
        }
    }
}

libhttppp::HttpCookie::CookieData *libhttppp::HttpCookie::getfirstCookieData(){
  return _firstCookieData.get();
}

libhttppp::HttpCookie::CookieData *libhttppp::HttpCookie::getlastCookieData(){
  return _lastCookieData;
}


libhttppp::HttpCookie::CookieData  *libhttppp::HttpCookie::addCookieData(){
  if(!_firstCookieData){
    _firstCookieData= std::make_unique<CookieData>();
    _lastCookieData=_firstCookieData.get();
  }else{
    _lastCookieData->_nextCookieData=std::make_unique<CookieData>();
    _lastCookieData=_lastCookieData->_nextCookieData.get();
  }
  return _lastCookieData;
}

libhttppp::HttpAuth::HttpAuth(){
  _Authtype=BASICAUTH;
}

libhttppp::HttpAuth::~HttpAuth(){
}


void libhttppp::HttpAuth::parse(libhttppp::HttpRequest &curreq) {
  HttpHeader::HeaderData *hau = curreq.getHeaderData("authorization");
  if (!hau) return;

  auto starts_with_icase = [](const std::string& s, const char* p){
    size_t n = std::strlen(p); if (s.size() < n) return false;
    for (size_t i = 0; i < n; ++i)
      if (std::tolower((unsigned char)s[i]) != std::tolower((unsigned char)p[i])) return false;
    return true;
  };

  std::string token_data;            // raw (un-lowercased) token/params
  netplus::base64 base64str;
  _Authtype = -1;

  for (auto *cval = hau->getfirstValue(); cval; cval = cval->nextvalue()) {
    const std::string& raw = cval->getvalue();

    // trim leading spaces
    size_t i = 0; while (i < raw.size() && std::isspace((unsigned char)raw[i])) ++i;
    std::string s = (i ? raw.substr(i) : raw);

    if (starts_with_icase(s, "Basic ")) {
      _Authtype = BASICAUTH;
      token_data = s.substr(6);
      // trim left spaces of token
      size_t j = 0; while (j < token_data.size() && token_data[j] == ' ') ++j;
      token_data.erase(0, j);
      base64str.setEncodedData(token_data);
      break;
    }
    if (starts_with_icase(s, "Digest ")) {
      _Authtype = DIGESTAUTH;
      token_data = s.substr(7);
      size_t j = 0; while (j < token_data.size() && token_data[j] == ' ') ++j;
      token_data.erase(0, j);
      break;
    }
  }

  auto parseDigestParameters = [this](const std::string& params) {
    size_t p = 0;
    while (p < params.size()) {
      p = params.find_first_not_of(" ,", p);
      if (p == std::string::npos) break;
      size_t eq = params.find('=', p);
      if (eq == std::string::npos) break;
      std::string key = params.substr(p, eq - p);
      key.erase(std::remove_if(key.begin(), key.end(), ::isspace), key.end());

      size_t v0 = eq + 1;
      std::string value;
      if (v0 < params.size() && params[v0] == '"') {
        ++v0;
        size_t v1 = params.find('"', v0);
        if (v1 == std::string::npos) break;
        value = params.substr(v0, v1 - v0);
        p = v1 + 1;
      } else {
        size_t v1 = params.find(',', v0);
        if (v1 == std::string::npos) v1 = params.size();
        value = params.substr(v0, v1 - v0);
        value.erase(std::remove_if(value.begin(), value.end(), ::isspace), value.end());
        p = v1;
      }

      if (key == "realm") setRealm(value);
      // add more keys as needed
    }
  };

  switch (_Authtype) {
    case BASICAUTH: {
      std::string clearstr;
      base64str >> clearstr;                     // "user:pass"
      size_t colon = clearstr.find(':');
      if (colon == std::string::npos) {
        HTTPException e; e[HTTPException::Error] << "Basic Auth payload missing separator ':'";
        throw e;
      }
      setUsername(clearstr.substr(0, colon));
      setPassword(clearstr.substr(colon + 1));
    } break;

    case DIGESTAUTH: {
      if (token_data.empty()) {
        HTTPException e; e[HTTPException::Error] << "Digest Auth header is empty"; throw e;
      }
      parseDigestParameters(token_data);
    } break;

    default: {
      HTTPException e; e[HTTPException::Error] << "No valid authorization method found"; throw e;
    }
  }
}



void libhttppp::HttpAuth::setAuth(libhttppp::HttpResponse &curresp){
  HttpHeader::HeaderData *dat = curresp.setHeaderData("WWW-Authenticate");
  HttpHeader::HeaderData::Values cval;

  auto random_hex = [] (size_t n) {
    static thread_local std::mt19937_64 rng{std::random_device{}()};
    static const char* hex = "0123456789abcdef";
    std::string s; s.reserve(n);
    for (size_t i = 0; i < n; ++i) s.push_back(hex[rng() & 0xF]);
    return s;
  };

  switch (_Authtype) {
    case BASICAUTH: {
      cval << "Basic";
      if (!_Realm.empty()) cval << " realm=\"" + _Realm + "\"";
    } break;
    case DIGESTAUTH: {
      cval << "Digest";
      if (!_Realm.empty()) cval << " realm=\"" + _Realm + "\"";
      std::string nonce = random_hex(32);
      cval << " nonce=\"" + nonce + "\"";
    } break;
    default: {
      HTTPException e; e[HTTPException::Error] << "no auth method choose !"; throw e;
    }
  }
  dat->push_back(cval);
}


void libhttppp::HttpAuth::setAuthType(int authtype){
  _Authtype=authtype;
}

void libhttppp::HttpAuth::setRealm(const std::string &realm){
  _Realm.clear();
  _Realm=realm;
}

void libhttppp::HttpAuth::setUsername(const std::string &username){
  _Username.clear();
  _Username=username;
}

void libhttppp::HttpAuth::setPassword(const std::string & password){
  _Password.clear();
  _Password=password;
}


const std::string &libhttppp::HttpAuth::getUsername(){
  return _Username;
}

const std::string &libhttppp::HttpAuth::getPassword(){
  return _Password;
}

int libhttppp::HttpAuth::getAuthType(){
  return _Authtype;
}
