aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/httpserver.cpp
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2026-04-10 19:09:10 +0200
committerGitHub Enterprise <[email protected]>2026-04-10 19:09:10 +0200
commit1783a5c414c6ae2088bacf6183580432fddbfbe7 (patch)
treea4d3e40bbb7f1a111e50b41c1e85fc1e5bd565b7 /src/zenhttp/httpserver.cpp
parentreduce test runtime (#933) (diff)
downloadzen-1783a5c414c6ae2088bacf6183580432fddbfbe7.tar.xz
zen-1783a5c414c6ae2088bacf6183580432fddbfbe7.zip
HTTP range responses (RFC 7233) - httpobjectstore (#928)
- Improvement: HTTP range responses (RFC 7233) are now fully compliant across the object store and build store - 206 Partial Content responses now include a `Content-Range` header; previously absent for single-range requests, which broke `HttpClient::GetRanges()` - 416 Range Not Satisfiable responses now include `Content-Range: bytes */N` as required by RFC 7233 - Out-of-bounds range requests return 416 Range Not Satisfiable (was 400 Bad Request) - Single-byte ranges (`bytes=N-N`) are now correctly accepted (were previously rejected) - Range byte positions widened from 32-bit to 64-bit; RFC 7233 imposes no size limit on byte range values - Build store binary GET requests with a Range header now return 206 Partial Content with `Content-Range` (previously returned 200 OK without it)
Diffstat (limited to 'src/zenhttp/httpserver.cpp')
-rw-r--r--src/zenhttp/httpserver.cpp95
1 files changed, 92 insertions, 3 deletions
diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp
index 38021be16..3668e1e8f 100644
--- a/src/zenhttp/httpserver.cpp
+++ b/src/zenhttp/httpserver.cpp
@@ -266,10 +266,10 @@ TryParseHttpRangeHeader(std::string_view RangeHeader, HttpRanges& Ranges)
return false;
}
- const auto Start = ParseInt<uint32_t>(Token.substr(0, Delim));
- const auto End = ParseInt<uint32_t>(Token.substr(Delim + 1));
+ const auto Start = ParseInt<uint64_t>(Token.substr(0, Delim));
+ const auto End = ParseInt<uint64_t>(Token.substr(Delim + 1));
- if (Start.has_value() && End.has_value() && End.value() > Start.value())
+ if (Start.has_value() && End.has_value() && End.value() >= Start.value())
{
Ranges.push_back({.Start = Start.value(), .End = End.value()});
}
@@ -286,6 +286,45 @@ TryParseHttpRangeHeader(std::string_view RangeHeader, HttpRanges& Ranges)
return Count != Ranges.size();
}
+MultipartByteRangesResult
+BuildMultipartByteRanges(const IoBuffer& Data, const HttpRanges& Ranges)
+{
+ Oid::String_t BoundaryStr;
+ Oid::NewOid().ToString(BoundaryStr);
+ std::string_view Boundary(BoundaryStr, Oid::StringLength);
+
+ const uint64_t TotalSize = Data.GetSize();
+
+ std::vector<IoBuffer> Parts;
+ Parts.reserve(Ranges.size() * 2 + 1);
+
+ for (const HttpRange& Range : Ranges)
+ {
+ uint64_t RangeEnd = (Range.End != ~uint64_t(0)) ? Range.End : TotalSize - 1;
+ if (RangeEnd >= TotalSize || Range.Start > RangeEnd)
+ {
+ return {};
+ }
+
+ uint64_t RangeSize = 1 + (RangeEnd - Range.Start);
+
+ std::string PartHeader = fmt::format("\r\n--{}\r\nContent-Type: application/octet-stream\r\nContent-Range: bytes {}-{}/{}\r\n\r\n",
+ Boundary,
+ Range.Start,
+ RangeEnd,
+ TotalSize);
+ Parts.push_back(IoBufferBuilder::MakeCloneFromMemory(PartHeader.data(), PartHeader.size()));
+
+ IoBuffer RangeData(Data, Range.Start, RangeSize);
+ Parts.push_back(RangeData);
+ }
+
+ std::string ClosingBoundary = fmt::format("\r\n--{}--", Boundary);
+ Parts.push_back(IoBufferBuilder::MakeCloneFromMemory(ClosingBoundary.data(), ClosingBoundary.size()));
+
+ return {.Parts = std::move(Parts), .ContentType = fmt::format("multipart/byteranges; boundary={}", Boundary)};
+}
+
//////////////////////////////////////////////////////////////////////////
const std::string_view
@@ -564,6 +603,56 @@ HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, HttpContentType
}
void
+HttpServerRequest::WriteResponse(HttpContentType ContentType, const IoBuffer& Data, const HttpRanges& Ranges)
+{
+ if (Ranges.empty())
+ {
+ WriteResponse(HttpResponseCode::OK, ContentType, IoBuffer(Data));
+ return;
+ }
+
+ if (Ranges.size() == 1)
+ {
+ const HttpRange& Range = Ranges[0];
+ const uint64_t TotalSize = Data.GetSize();
+ // ~uint64_t(0) is the sentinel meaning "end of file" (suffix range).
+ const uint64_t RangeEnd = (Range.End != ~uint64_t(0)) ? Range.End : TotalSize - 1;
+
+ if (RangeEnd >= TotalSize || Range.Start > RangeEnd)
+ {
+ m_ContentRangeHeader = fmt::format("bytes */{}", TotalSize);
+ WriteResponse(HttpResponseCode::RangeNotSatisfiable);
+ return;
+ }
+
+ const uint64_t RangeSize = 1 + (RangeEnd - Range.Start);
+ IoBuffer RangeBuf(Data, Range.Start, RangeSize);
+
+ m_ContentRangeHeader = fmt::format("bytes {}-{}/{}", Range.Start, RangeEnd, TotalSize);
+ WriteResponse(HttpResponseCode::PartialContent, ContentType, std::move(RangeBuf));
+ return;
+ }
+
+ // Multi-range
+ MultipartByteRangesResult MultipartResult = BuildMultipartByteRanges(Data, Ranges);
+ if (MultipartResult.Parts.empty())
+ {
+ m_ContentRangeHeader = fmt::format("bytes */{}", Data.GetSize());
+ WriteResponse(HttpResponseCode::RangeNotSatisfiable);
+ return;
+ }
+ WriteResponse(HttpResponseCode::PartialContent, std::move(MultipartResult.ContentType), std::span<IoBuffer>(MultipartResult.Parts));
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, const std::string& CustomContentType, std::span<IoBuffer> Blobs)
+{
+ ZEN_ASSERT(ParseContentType(CustomContentType) == HttpContentType::kUnknownContentType);
+ m_ContentTypeOverride = CustomContentType;
+ WriteResponse(ResponseCode, HttpContentType::kBinary, Blobs);
+}
+
+void
HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, CompositeBuffer& Payload)
{
std::span<const SharedBuffer> Segments = Payload.GetSegments();