Loading src/http/HttpProxyServer.cpp +111 −52 Changes for src/http/HttpProxyServer.cpp: 111 added lines, 52 removed lines. Original line number Diff line number Diff line Loading @@ -181,6 +181,52 @@ namespace { // Below this, gzip/brotli framing overhead eats whatever the algorithm saves -- matches // nginx's gzip_min_length default. constexpr size_t kMinCompressibleBytes = 256; // A GET response whose declared Content-Length exceeds this is relayed chunk by chunk (the // same path Range responses take) instead of being buffered whole for compression. Buffering // a multi-GB download (an ISO image, say) used to mean holding all of it in memory before the // client saw a single byte -- slow at best, std::bad_alloc at worst. constexpr size_t kMaxBufferedBody = 8 * 1024 * 1024; // Hard ceiling for a buffered body whose size wasn't known up front (chunked / read-until-EOF // responses have no Content-Length to compare against kMaxBufferedBody): fail the request // with a 502 instead of letting an unbounded upstream body exhaust memory. constexpr size_t kMaxUnknownLengthBody = 256 * 1024 * 1024; bool isChunkedResponse(const libhttppp::HttpResponse &resp) { try { for (auto *v = resp.getTransferEncoding(); v; v = v->nextvalue()) { std::string val = v->getvalue(); std::transform(val.begin(), val.end(), val.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); if (val == "chunked") return true; } } catch (...) { } return false; } // True when GetStream()'s response carries a trustworthy Content-Length (present, not // overridden by chunked framing -- mirrors libhttppp's own GetStream mode selection) that is // too large to buffer. bool exceedsBufferLimit(const libhttppp::HttpResponse &resp) { if (!resp.getHeaderData("content-length") || isChunkedResponse(resp)) return false; return resp.getContentLength() > kMaxBufferedBody; } // Drains whatever body GetStream() left pending into one buffer. std::vector<char> drainStreamedBody(libhttppp::HttpClient &client) { std::vector<char> body; char chunk[65536]; for (;;) { size_t n = client.readBodyChunk(chunk, sizeof(chunk)); // blocking; 0 = body complete if (n == 0) break; if (body.size() + n > kMaxUnknownLengthBody) throw std::runtime_error("upstream response body too large to buffer"); body.insert(body.end(), chunk, chunk + n); } return body; } } // namespace namespace { Loading Loading @@ -596,6 +642,58 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int outreq.setHeaderData("x-forwarded-proto")->push_back(_tlsActive ? "https" : "http"); std::vector<char> body; // Relays a fully buffered upstream response (status, headers, compression). // `last` is HttpClient::lastResponse() for the buffering methods, or GetStream()'s // own response for a small GET (GetStream() never populates lastResponse()). auto relayBuffered = [&](const libhttppp::HttpResponse *last) { libhttppp::HttpResponse curres; int status = 200; if (last) { status = last->getStatusCode(); // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3 // responses only carry a numeric ":status" pseudo-header) -- setState() // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone // (e.g. "OK", see HttpResponse::parse) would drop the code from our own // status line, so always lead with the numeric code ourselves. std::string reason = last->getState(); curres.setState(forwardedResponseState(status, reason)); curres.setContentType(last->getContentType()); // Forward every upstream response header to the client except hop-by-hop // ones and the handful applyResponseCompression() owns exclusively below -- // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and // any application-specific header were silently dropped (only status/ // content-type/content-encoding ever reached the client), breaking e.g. any // upstream that sets a session cookie. Mirrors kHopByHop on the // request-forwarding side above. static const std::set<std::string> kResponseSkip = { "content-type", "content-length", "content-encoding", "vary", "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer", }; for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) { const std::string &key = hd->getkey(); if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue; auto *out = curres.setHeaderData(key); for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) { out->push_back(v->getvalue()); } } std::string upstreamEncoding; if (auto *hd = last->getHeaderData("content-encoding")) { if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue(); } applyResponseCompression(curreq, upstreamEncoding, body, curres); } else { curres.setState(HTTP200); } curres.send(curreq, body, static_cast<int>(body.size())); // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the // application itself) counts as "up" -- only a transport-level failure (caught // below) or a 5xx from the backend counts against it for passive health. upstreamOk = status < 500; }; if (method == "POST" || method == "PUT" || method == "PATCH") { outreq.setRequestType(method == "POST" ? POSTREQUEST : method == "PUT" ? PUTREQUEST Loading Loading @@ -646,7 +744,14 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // byte-range response is inherently uncompressible-as-a-whole (the range // refers to the uncompressed representation), and neither app's media routes // send a Content-Encoding on these responses anyway. if (curreq.getHeaderData("range")) { // // The same applies to any response too large to buffer (exceedsBufferLimit): // every GET goes through GetStream() so the size is known before any body is // read, and only small (or unknown-length) bodies get drained and buffered for // the compression path below. A large one keeps whatever Content-Encoding the // upstream chose, forwarded unchanged. libhttppp::HttpResponse streamResp = client.GetStream(outreq); if (curreq.getHeaderData("range") || exceedsBufferLimit(streamResp)) { // H1 vs H2/H3 need genuinely different delivery mechanisms below, not // just different plumbing -- see the two branches' own comments for why. // getRequestVersion() is set from real parsed values for every protocol Loading @@ -655,7 +760,6 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // so this is a reliable, already-public way to tell them apart here. bool isH1 = curreq.getRequestVersion().rfind("HTTP/1", 0) == 0; libhttppp::HttpResponse streamResp = client.GetStream(outreq); int rangeStatus = streamResp.getStatusCode(); libhttppp::HttpResponse curres; Loading Loading @@ -729,7 +833,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int try { n = client.readBodyChunk(chunk, toRead); // blocking; 0 = body complete } catch (const std::exception &e) { std::cerr << "[http] streaming range response failed after " std::cerr << "[http] streaming response failed after " << (rangeContentLength - remaining) << "/" << rangeContentLength << " bytes: " << e.what() << std::endl; deliveryOk = false; Loading Loading @@ -787,55 +891,10 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int return; } body = client.Get(outreq, /*maxTries=*/1); body = drainStreamedBody(client); relayBuffered(&streamResp); } libhttppp::HttpResponse curres; int status = 200; if (auto *last = client.lastResponse()) { status = last->getStatusCode(); // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3 // responses only carry a numeric ":status" pseudo-header) -- setState() // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone // (e.g. "OK", see HttpResponse::parse) would drop the code from our own // status line, so always lead with the numeric code ourselves. std::string reason = last->getState(); curres.setState(forwardedResponseState(status, reason)); curres.setContentType(relayedContentType(*last)); // Forward every upstream response header to the client except hop-by-hop // ones and the handful applyResponseCompression() owns exclusively below -- // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and // any application-specific header were silently dropped (only status/ // content-type/content-encoding ever reached the client), breaking e.g. any // upstream that sets a session cookie. Mirrors kHopByHop on the // request-forwarding side above. static const std::set<std::string> kResponseSkip = { "content-type", "content-length", "content-encoding", "vary", "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer", }; for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) { const std::string &key = hd->getkey(); if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue; auto *out = curres.setHeaderData(key); for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) { out->push_back(v->getvalue()); } } std::string upstreamEncoding; if (auto *hd = last->getHeaderData("content-encoding")) { if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue(); } applyResponseCompression(curreq, upstreamEncoding, body, curres); } else { curres.setState(HTTP200); } curres.send(curreq, body, static_cast<int>(body.size())); // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the // application itself) counts as "up" -- only a transport-level failure (caught // below) or a 5xx from the backend counts against it for passive health. upstreamOk = status < 500; if (method != "GET") relayBuffered(client.lastResponse()); } catch (const std::exception &e) { sendError(curreq, HTTP502, std::string("upstream error: ") + e.what()); } Loading Loading @@ -868,7 +927,7 @@ void HttpProxyController::ResponseEvent(libhttppp::HttpRequest &curreq, const in try { n = streaming.client->readBodyChunkNonBlocking(chunk, toRead); } catch (const std::exception &e) { std::cerr << "[http] streaming range response failed after " std::cerr << "[http] streaming response failed after " << streaming.produced << "/" << streaming.send_size << " bytes: " << e.what() << std::endl; n = static_cast<size_t>(-1); Loading Loading
src/http/HttpProxyServer.cpp +111 −52 Changes for src/http/HttpProxyServer.cpp: 111 added lines, 52 removed lines. Original line number Diff line number Diff line Loading @@ -181,6 +181,52 @@ namespace { // Below this, gzip/brotli framing overhead eats whatever the algorithm saves -- matches // nginx's gzip_min_length default. constexpr size_t kMinCompressibleBytes = 256; // A GET response whose declared Content-Length exceeds this is relayed chunk by chunk (the // same path Range responses take) instead of being buffered whole for compression. Buffering // a multi-GB download (an ISO image, say) used to mean holding all of it in memory before the // client saw a single byte -- slow at best, std::bad_alloc at worst. constexpr size_t kMaxBufferedBody = 8 * 1024 * 1024; // Hard ceiling for a buffered body whose size wasn't known up front (chunked / read-until-EOF // responses have no Content-Length to compare against kMaxBufferedBody): fail the request // with a 502 instead of letting an unbounded upstream body exhaust memory. constexpr size_t kMaxUnknownLengthBody = 256 * 1024 * 1024; bool isChunkedResponse(const libhttppp::HttpResponse &resp) { try { for (auto *v = resp.getTransferEncoding(); v; v = v->nextvalue()) { std::string val = v->getvalue(); std::transform(val.begin(), val.end(), val.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); if (val == "chunked") return true; } } catch (...) { } return false; } // True when GetStream()'s response carries a trustworthy Content-Length (present, not // overridden by chunked framing -- mirrors libhttppp's own GetStream mode selection) that is // too large to buffer. bool exceedsBufferLimit(const libhttppp::HttpResponse &resp) { if (!resp.getHeaderData("content-length") || isChunkedResponse(resp)) return false; return resp.getContentLength() > kMaxBufferedBody; } // Drains whatever body GetStream() left pending into one buffer. std::vector<char> drainStreamedBody(libhttppp::HttpClient &client) { std::vector<char> body; char chunk[65536]; for (;;) { size_t n = client.readBodyChunk(chunk, sizeof(chunk)); // blocking; 0 = body complete if (n == 0) break; if (body.size() + n > kMaxUnknownLengthBody) throw std::runtime_error("upstream response body too large to buffer"); body.insert(body.end(), chunk, chunk + n); } return body; } } // namespace namespace { Loading Loading @@ -596,6 +642,58 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int outreq.setHeaderData("x-forwarded-proto")->push_back(_tlsActive ? "https" : "http"); std::vector<char> body; // Relays a fully buffered upstream response (status, headers, compression). // `last` is HttpClient::lastResponse() for the buffering methods, or GetStream()'s // own response for a small GET (GetStream() never populates lastResponse()). auto relayBuffered = [&](const libhttppp::HttpResponse *last) { libhttppp::HttpResponse curres; int status = 200; if (last) { status = last->getStatusCode(); // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3 // responses only carry a numeric ":status" pseudo-header) -- setState() // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone // (e.g. "OK", see HttpResponse::parse) would drop the code from our own // status line, so always lead with the numeric code ourselves. std::string reason = last->getState(); curres.setState(forwardedResponseState(status, reason)); curres.setContentType(last->getContentType()); // Forward every upstream response header to the client except hop-by-hop // ones and the handful applyResponseCompression() owns exclusively below -- // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and // any application-specific header were silently dropped (only status/ // content-type/content-encoding ever reached the client), breaking e.g. any // upstream that sets a session cookie. Mirrors kHopByHop on the // request-forwarding side above. static const std::set<std::string> kResponseSkip = { "content-type", "content-length", "content-encoding", "vary", "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer", }; for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) { const std::string &key = hd->getkey(); if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue; auto *out = curres.setHeaderData(key); for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) { out->push_back(v->getvalue()); } } std::string upstreamEncoding; if (auto *hd = last->getHeaderData("content-encoding")) { if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue(); } applyResponseCompression(curreq, upstreamEncoding, body, curres); } else { curres.setState(HTTP200); } curres.send(curreq, body, static_cast<int>(body.size())); // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the // application itself) counts as "up" -- only a transport-level failure (caught // below) or a 5xx from the backend counts against it for passive health. upstreamOk = status < 500; }; if (method == "POST" || method == "PUT" || method == "PATCH") { outreq.setRequestType(method == "POST" ? POSTREQUEST : method == "PUT" ? PUTREQUEST Loading Loading @@ -646,7 +744,14 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // byte-range response is inherently uncompressible-as-a-whole (the range // refers to the uncompressed representation), and neither app's media routes // send a Content-Encoding on these responses anyway. if (curreq.getHeaderData("range")) { // // The same applies to any response too large to buffer (exceedsBufferLimit): // every GET goes through GetStream() so the size is known before any body is // read, and only small (or unknown-length) bodies get drained and buffered for // the compression path below. A large one keeps whatever Content-Encoding the // upstream chose, forwarded unchanged. libhttppp::HttpResponse streamResp = client.GetStream(outreq); if (curreq.getHeaderData("range") || exceedsBufferLimit(streamResp)) { // H1 vs H2/H3 need genuinely different delivery mechanisms below, not // just different plumbing -- see the two branches' own comments for why. // getRequestVersion() is set from real parsed values for every protocol Loading @@ -655,7 +760,6 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // so this is a reliable, already-public way to tell them apart here. bool isH1 = curreq.getRequestVersion().rfind("HTTP/1", 0) == 0; libhttppp::HttpResponse streamResp = client.GetStream(outreq); int rangeStatus = streamResp.getStatusCode(); libhttppp::HttpResponse curres; Loading Loading @@ -729,7 +833,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int try { n = client.readBodyChunk(chunk, toRead); // blocking; 0 = body complete } catch (const std::exception &e) { std::cerr << "[http] streaming range response failed after " std::cerr << "[http] streaming response failed after " << (rangeContentLength - remaining) << "/" << rangeContentLength << " bytes: " << e.what() << std::endl; deliveryOk = false; Loading Loading @@ -787,55 +891,10 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int return; } body = client.Get(outreq, /*maxTries=*/1); body = drainStreamedBody(client); relayBuffered(&streamResp); } libhttppp::HttpResponse curres; int status = 200; if (auto *last = client.lastResponse()) { status = last->getStatusCode(); // HttpResponse::parseH2/parseH3 clear the reason phrase entirely (H2/H3 // responses only carry a numeric ":status" pseudo-header) -- setState() // throws on an empty string, and an HTTP/1.1 upstream's reason phrase alone // (e.g. "OK", see HttpResponse::parse) would drop the code from our own // status line, so always lead with the numeric code ourselves. std::string reason = last->getState(); curres.setState(forwardedResponseState(status, reason)); curres.setContentType(relayedContentType(*last)); // Forward every upstream response header to the client except hop-by-hop // ones and the handful applyResponseCompression() owns exclusively below -- // without this, Set-Cookie, Location, Cache-Control, WWW-Authenticate, and // any application-specific header were silently dropped (only status/ // content-type/content-encoding ever reached the client), breaking e.g. any // upstream that sets a session cookie. Mirrors kHopByHop on the // request-forwarding side above. static const std::set<std::string> kResponseSkip = { "content-type", "content-length", "content-encoding", "vary", "connection", "transfer-encoding", "keep-alive", "upgrade", "trailer", }; for (auto *hd = last->getfirstHeaderData(); hd; hd = hd->nextHeaderData()) { const std::string &key = hd->getkey(); if (key.empty() || key[0] == ':' || kResponseSkip.count(key)) continue; auto *out = curres.setHeaderData(key); for (auto *v = hd->getfirstValue(); v; v = v->nextvalue()) { out->push_back(v->getvalue()); } } std::string upstreamEncoding; if (auto *hd = last->getHeaderData("content-encoding")) { if (auto *v = hd->getfirstValue()) upstreamEncoding = v->getvalue(); } applyResponseCompression(curreq, upstreamEncoding, body, curres); } else { curres.setState(HTTP200); } curres.send(curreq, body, static_cast<int>(body.size())); // A pooled backend answering with a real HTTP response (even a 4xx/5xx from the // application itself) counts as "up" -- only a transport-level failure (caught // below) or a 5xx from the backend counts against it for passive health. upstreamOk = status < 500; if (method != "GET") relayBuffered(client.lastResponse()); } catch (const std::exception &e) { sendError(curreq, HTTP502, std::string("upstream error: ") + e.what()); } Loading Loading @@ -868,7 +927,7 @@ void HttpProxyController::ResponseEvent(libhttppp::HttpRequest &curreq, const in try { n = streaming.client->readBodyChunkNonBlocking(chunk, toRead); } catch (const std::exception &e) { std::cerr << "[http] streaming range response failed after " std::cerr << "[http] streaming response failed after " << streaming.produced << "/" << streaming.send_size << " bytes: " << e.what() << std::endl; n = static_cast<size_t>(-1); Loading