Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
Frameworks/WebServer/Response.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <algorithm>
7#include <cstdlib>
8#include <sstream>
9
15#include "Stroika/Foundation/Containers/Common.h"
16#include "Stroika/Foundation/Containers/Support/ReserveTweaks.h"
17#include "Stroika/Foundation/DataExchange/BadFormatException.h"
24#include "Stroika/Foundation/IO/Network/HTTP/ClientErrorException.h"
25#include "Stroika/Foundation/IO/Network/HTTP/Exception.h"
26#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
27
28#include "Response.h"
29
30using std::byte;
31
32using namespace Stroika::Foundation;
36using namespace Stroika::Foundation::Memory;
37
38using namespace Stroika::Frameworks;
39using namespace Stroika::Frameworks::WebServer;
40
42using PropertyChangedEventResultType = Common::PropertyCommon::PropertyChangedEventResultType;
43
45using Memory::BLOB;
46
47// Comment this in to turn on aggressive noisy DbgTrace in this module
48// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
49
50/*
51 ********************************************************************************
52 **************************** WebServer::Response *******************************
53 ********************************************************************************
54 */
55namespace {
56 /**
57 * Very confused about what is going on here. Only tested with HTTP 1.1, but both chrome and msft edge dont
58 * handle transfer-encoding the way I would have expected.
59 *
60 * This works FINE with curl - decoding the deflate just fine.
61 */
62 constexpr bool kTransferEncodingCompressionDoesntAppearToWorkWithBrowsers_ = true;
63}
64
65namespace {
66 constexpr char kCRLF_[] = "\r\n";
67}
68
69static_assert (not copyable<Response>); // enforce Satisfies Concepts
70static_assert (movable<Response>);
71
72Response::Response (Response&& src)
73 // Would be nice to use inherited src move, but PITA, because then would need to duplicate creating the properties below.
74 : Response{src.fSocket_, src.fProtocolOutputStream_, src.headers ()}
75{
76 fState_ = src.fState_;
77 fHeadMode_ = src.fHeadMode_;
78 fAborted_ = src.fAborted_;
79 fAutoTransferChunkSize_ = src.fAutoTransferChunkSize_;
80 fBodyEncoding_ = move (src.fBodyEncoding_);
81 fBodyRawStream_ = move (src.fBodyRawStream_);
82 fBodyRawStreamLength_ = move (src.fBodyRawStreamLength_);
83 fBodyRowStreamLengthWhenLastChunkGenerated_ = move (src.fBodyRowStreamLengthWhenLastChunkGenerated_);
84 fBodyCompressedStream_ = move (src.fBodyCompressedStream_);
85 fUseOutStream_ = move (src.fUseOutStream_);
86 fCodePage_ = move (src.fCodePage_);
87 fCodeCvt_ = move (src.fCodeCvt_);
88 fETagDigester_ = move (src.fETagDigester_);
89}
90
91Response::Response (const IO::Network::Socket::Ptr& s, const Streams::OutputStream::Ptr<byte>& outStream, const optional<HTTP::Headers>& initialHeaders)
92 : inherited{initialHeaders}
93 , autoComputeETag{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
94 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::autoComputeETag);
95 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
96 return thisObj->fETagDigester_.has_value ();
97 },
98 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const bool newAutoComputeETag) {
99 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::autoComputeETag);
100 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
101 Require (thisObj->state () == State::ePreparingHeaders);
102 Require (thisObj->fBodyRawStreamLength_ == 0);
103 if (newAutoComputeETag) {
104 thisObj->fETagDigester_ = ETagDigester_{};
105 }
106 else {
107 thisObj->fETagDigester_ = nullopt;
108 }
109 }}
110 , automaticTransferChunkSize{
111 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
112 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::automaticTransferChunkSize);
113 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
114 return thisObj->fAutoTransferChunkSize_;
115 },
116 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const optional<size_t>& newAutoComputeValue) {
117 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::automaticTransferChunkSize);
118 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
119 Require (thisObj->state () == State::ePreparingHeaders);
120 thisObj->fAutoTransferChunkSize_ = newAutoComputeValue;
121 }}
122 , bodyEncoding{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
123 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::bodyEncoding);
124 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
125 return thisObj->fBodyEncoding_;
126 },
127 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const auto& newCT) {
128 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::bodyEncoding);
129 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
130 thisObj->fBodyEncoding_ = newCT;
131 }}
132 , chunkedTransferMode{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
133 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::chunkedTransferMode);
134 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
135 auto te = thisObj->headers ().transferEncoding ();
136 return te and te->Contains (HTTP::TransferEncoding::kChunked);
137 }}
138 , codeCvt{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
139 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::codeCvt);
140 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
141 if (thisObj->fCodeCvt_ == nullopt) {
142 AssertExternallySynchronizedMutex::WriteContext declareContext2{const_cast<Response*> (thisObj)->_fThisAssertExternallySynchronized};
143 // http://stroika-bugs.sophists.com/browse/STK-983
144 thisObj->fCodeCvt_ = Characters::CodeCvt<>{thisObj->fCodePage_};
145 }
146 return *thisObj->fCodeCvt_;
147 }}
148 , codePage{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
149 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::codePage);
150 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
151 return thisObj->fCodePage_;
152 },
153 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const auto& newCodePage) {
154 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::codePage);
155 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
156 Require (thisObj->headersCanBeSet ());
157 Require (thisObj->fBodyRawStreamLength_ == 0);
158 bool diff = thisObj->fCodePage_ != newCodePage;
159 thisObj->fCodePage_ = newCodePage;
160 if (diff) {
161 if (auto ct = thisObj->headers ().contentType ()) {
162 thisObj->rwHeaders ().contentType = thisObj->AdjustContentTypeForCodePageIfNeeded_ (*ct);
163 }
164 thisObj->fCodeCvt_ = nullopt;
165 }
166 }}
167 , contentType{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
168 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::contentType);
169 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
170 return thisObj->headers ().contentType ();
171 },
172 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const auto& newCT) {
173 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::contentType);
174 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
175 thisObj->rwHeaders ().contentType = newCT;
176 }}
177 , hasEntityBody{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
178 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::hasEntityBody);
179 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
180 if (thisObj->fHeadMode_) {
181 return false;
182 }
183 if (thisObj->status () == HTTP::StatusCodes::kNotModified) {
184 return false;
185 }
186 return thisObj->fBodyRawStreamLength_ > 0; //??? - better way to tell???
187 }}
188 , headersCanBeSet{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
189 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::headersCanBeSet);
190 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
191 return thisObj->fState_ == State::ePreparingHeaders;
192 }}
193 , headMode{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
194 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::headMode);
195 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
196 return thisObj->fHeadMode_;
197 },
198 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const auto& newHeadMode) {
199 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::headMode);
200 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
201 Require (thisObj->fState_ == State::ePreparingHeaders);
202 if (newHeadMode) {
203 thisObj->fHeadMode_ = true;
204 }
205 else {
206 Require (thisObj->fHeadMode_ == newHeadMode);
207 }
208 }}
209 , location{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
210 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::location);
211 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
212 return thisObj->headers ().location ();
213 },
214 [qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] auto* property, const auto& newLoc) {
215 Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::location);
216 AssertExternallySynchronizedMutex::WriteContext declareContext{thisObj->_fThisAssertExternallySynchronized};
217 thisObj->rwHeaders ().location = newLoc;
218 }}
219 , responseAborted{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
220 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::responseAborted);
221 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
222 return thisObj->fAborted_;
223 }}
224 , responseCompleted{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
225 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::responseCompleted);
226 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
227 return thisObj->fState_ == State::eCompleted;
228 }}
229 , responseStatusSent{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
230 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::responseStatusSent);
231 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
232 return thisObj->fState_ != State::ePreparingHeaders;
233 }}
234 , state{[qStroika_Foundation_Common_Property_ExtraCaptureStuff] ([[maybe_unused]] const auto* property) {
235 const Response* thisObj = qStroika_Foundation_Common_Property_OuterObjPtr (property, &Response::state);
236 AssertExternallySynchronizedMutex::ReadContext declareContext{thisObj->_fThisAssertExternallySynchronized};
237 return thisObj->fState_;
238 }}
239 , fSocket_{s}
240 , fProtocolOutputStream_{outStream}
241 , fBodyRawStream_{Streams::SharedMemoryStream::New<byte> (Streams::SharedMemoryStream::Options{
242 .fInternallySynchronized = Execution::InternallySynchronized::eNotKnownInternallySynchronized, .fSeekable = false})}
243 , fUseOutStream_{Streams::BufferedOutputStream::New<byte> (outStream)}
244{
246 DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wunused-lambda-capture\""); // sadly no way to [[maybe_unused]] on captures
247 this->status.rwPropertyChangedHandlers ().push_front ([this] ([[maybe_unused]] const auto& propertyChangedEvent) {
248 Require (not this->responseStatusSent ());
249 return PropertyChangedEventResultType::eContinueProcessing;
250 });
251 this->statusAndOverrideReason.rwPropertyChangedHandlers ().push_front ([this] ([[maybe_unused]] const auto& propertyChangedEvent) {
252 Require (not this->responseStatusSent ());
253 return PropertyChangedEventResultType::eContinueProcessing;
254 });
255 this->rwHeaders.rwPropertyReadHandlers ().push_front ([this] (HTTP::Headers* h) {
256 Require (this->headersCanBeSet ());
257 return h;
258 });
259 this->rwHeaders.rwPropertyChangedHandlers ().push_front ([this] ([[maybe_unused]] const auto& propertyChangedEvent) {
260 Require (this->headersCanBeSet ());
261 return PropertyChangedEventResultType::eContinueProcessing;
262 });
263 DISABLE_COMPILER_CLANG_WARNING_END ("clang diagnostic ignored \"-Wunused-lambda-capture\"");
264 }
265 this->rwHeaders ().transferEncoding.rwPropertyChangedHandlers ().push_front ([this] ([[maybe_unused]] const auto& propertyChangedEvent) {
266 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
267 Require (this->headersCanBeSet ());
268 return PropertyChangedEventResultType::eContinueProcessing;
269 });
270 // auto-compute the content-length unless chunked transfer
271 this->rwHeaders ().contentLength.rwPropertyReadHandlers ().push_front ([this] ([[maybe_unused]] const auto& baseValue) -> optional<uint64_t> {
272 if (this->chunkedTransferMode ()) {
273 return nullopt;
274 }
275 if (this->fHeadMode_) {
276 // https://stackoverflow.com/questions/27868314/avoiding-content-length-in-head-response
277 // // https://www.rfc-editor.org/rfc/rfc7231#section-4.3.2
278 // The server SHOULD send the same header fields in response to a HEAD request as it would
279 // have sent if the request had been a GET,
280 // except that the payload header fields (Section 3.3) MAY be omitted.
281 return nullopt;
282 }
283 // in non-chunked mode, we count on all the 'writes' having been completed
284 if (fBodyCompressedStream_ != nullptr) {
285 return fBodyCompressedStream_.GetOffset ();
286 }
287 if (this->fBodyRawStream_ != nullptr) {
288 return fBodyRawStreamLength_;
289 }
291 return nullopt;
292 });
293 this->rwHeaders ().contentLength.rwPropertyChangedHandlers ().push_front ([] ([[maybe_unused]] const auto& propertyChangedEvent) {
294 RequireNotReached (); // since v3.0d7 - disallow
295 return PropertyChangedEventResultType::eSilentlyCutOffProcessing;
296 });
297 // auto-compute the etag if autoComputeETag (fETagDigester_.has_value()) is true
298 this->rwHeaders ().ETag.rwPropertyReadHandlers ().push_front ([this] (const auto& baseETagValue) -> optional<HTTP::ETag> {
299 // return the current tag 'so far' (if we are auto-computing)
300 if (fETagDigester_) {
301 auto copy = *fETagDigester_; // copy cuz this could get called multiple times and Complete only allowed to be called once
302 return HTTP::ETag{copy.Complete ()};
303 }
304 return baseETagValue; // if we are not auto-computing
305 });
306 this->rwHeaders ().ETag.rwPropertyChangedHandlers ().push_front ([this] ([[maybe_unused]] const auto& propertyChangedEvent) {
307 Require (this->headersCanBeSet ());
308 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
309 // if someone explicitly sets the etag, then stop auto-computing it
310 this->autoComputeETag = false;
311 return PropertyChangedEventResultType::eContinueProcessing;
312 });
313}
314
315Response& Response::operator= (Response&& rhs) noexcept
316{
317 inherited::operator= (move (rhs));
318 fSocket_ = rhs.fSocket_;
319 fProtocolOutputStream_ = rhs.fProtocolOutputStream_;
320 fState_ = rhs.fState_;
321 fHeadMode_ = rhs.fHeadMode_;
322 fAborted_ = rhs.fAborted_;
323 fAutoTransferChunkSize_ = rhs.fAutoTransferChunkSize_;
324 fBodyEncoding_ = move (rhs.fBodyEncoding_);
325 fBodyRawStream_ = move (rhs.fBodyRawStream_);
326 fBodyRawStreamLength_ = move (rhs.fBodyRawStreamLength_);
327 fBodyRowStreamLengthWhenLastChunkGenerated_ = move (rhs.fBodyRowStreamLengthWhenLastChunkGenerated_);
328 fBodyCompressedStream_ = move (rhs.fBodyCompressedStream_);
329 fUseOutStream_ = move (rhs.fUseOutStream_);
330 fCodePage_ = move (rhs.fCodePage_);
331 fCodeCvt_ = move (rhs.fCodeCvt_);
332 fETagDigester_ = move (rhs.fETagDigester_);
333 return *this;
334}
335
336void Response::StateTransition_ (State to)
337{
338#if USE_NOISY_TRACE_IN_THIS_MODULE_
339 Debug::TraceContextBumper ctx{"Response::StateTransition_", "from={}, to={}"_f, static_cast<State> (fState_), static_cast<State> (to)};
340#endif
341 Require (fState_ <= to);
342 if (to != fState_) {
343 if (fState_ == State::ePreparingHeaders and to == State::eHeadersSent) {
344 ApplyBodyEncodingIfNeeded_ ();
345 {
346 auto curStatusInfo = this->statusAndOverrideReason ();
347 Status curStatus = get<0> (curStatusInfo);
348 String statusMsg =
349 Memory::NullCoalesce (get<1> (curStatusInfo), IO::Network::HTTP::Exception::GetStandardTextForStatus (curStatus, true));
350 wstring version = L"1.1";
351 String tmp = "HTTP/{} {} {}\r\n"_f(version, curStatus, statusMsg);
352 u8string utf8 = tmp.AsUTF8 ();
353 fUseOutStream_.Write (as_bytes (span{utf8.data (), utf8.length ()}));
354 }
355 {
356 for (const auto& i : this->headers ().As<> ()) {
357 u8string utf8 = "{}: {}\r\n"_f(i.fKey, i.fValue).AsUTF8 ();
358 fUseOutStream_.Write (as_bytes (span{utf8.data (), utf8.length ()}));
359 }
360#if USE_NOISY_TRACE_IN_THIS_MODULE_
361 DbgTrace ("headers: {}"_f, headers ());
362#endif
363 }
364 fUseOutStream_.Write (as_bytes (span{kCRLF_, ::strlen (kCRLF_)}));
365 }
366 fState_ = to;
368 if (to >= State::eHeadersSent and hasEntityBody ()) {
369 Assert (chunkedTransferMode () or this->headers ().contentLength ().has_value ()); // I think is is always required, but double check...
370 }
371 }
372 }
373}
374
375void Response::ApplyBodyEncodingIfNeeded_ ()
376{
377#if USE_NOISY_TRACE_IN_THIS_MODULE_
378 Debug::TraceContextBumper ctx{"Response::ApplyBodyEncodingIfNeeded_", "this->bodyEncoding={}"_f, fBodyEncoding_};
379#endif
380 if (fState_ == State::ePreparingHeaders and fBodyEncoding_ and fBodyCompressedStream_ == nullptr) {
381 auto applyBodyEncoding = [this] (HTTP::ContentEncoding ce) {
382 if (this->chunkedTransferMode ()) {
383 if (kTransferEncodingCompressionDoesntAppearToWorkWithBrowsers_) {
384 rwHeaders ().contentEncoding = ce;
385 }
386 else {
387 HTTP::TransferEncodings htc = *headers ().transferEncoding ();
389 rwHeaders ().transferEncoding = htc;
390 }
391 }
392 else {
393 rwHeaders ().contentEncoding = ce;
394 }
395 };
396 Compression::Ptr currentCompression; // @todo could support multiple compression schemes, but I don't see the point - so don't
397 if constexpr (Compression::Deflate::kSupported) {
398 if (fBodyEncoding_->Contains (HTTP::ContentEncoding::kDeflate)) {
399 constexpr auto compressOpts = Compression::Deflate::Compress::Options{.fCompressionLevel = 1.0f}; // @todo config option - passed in - didn't seem to help here
400 currentCompression = Compression::Deflate::Compress::New (compressOpts);
401 applyBodyEncoding (HTTP::ContentEncoding::kDeflate);
402 }
403 }
404 if constexpr (Compression::GZip::kSupported) {
405 if (currentCompression == nullptr and fBodyEncoding_->Contains (HTTP::ContentEncoding::kGZip)) {
406 static const auto compressOpts = Compression::GZip::Compress::Options{{.fCompressionLevel = 1.0f}}; // @todo config option - passed in - didn't seem to help here
407 currentCompression = Compression::GZip::Compress::New (compressOpts);
408 applyBodyEncoding (HTTP::ContentEncoding::kGZip);
409 }
410 }
411 if (currentCompression) {
412 // compressed stream reads from the raw body stream
413 fBodyCompressedStream_ = currentCompression.Transform (fBodyRawStream_);
414 }
415 }
416}
417
418void Response::WriteChunk_ (span<const byte> rawBytes)
419{
420#if USE_NOISY_TRACE_IN_THIS_MODULE_
421 Debug::TraceContextBumper ctx{"Response::WriteChunk_", "rawBytes=byte[{}]{}"_f, rawBytes.size (), rawBytes};
422#endif
423 // note rawBytes maybe empty - in fact the final chunk is always empty
424 string n = CString::Format ("%x\r\n", static_cast<unsigned int> (rawBytes.size ()));
425 fUseOutStream_.Write (as_bytes (span{n.data (), n.size ()}));
426 fUseOutStream_.Write (rawBytes);
427 fUseOutStream_.Write (as_bytes (span{kCRLF_, strlen (kCRLF_)}));
428}
429
430InternetMediaType Response::AdjustContentTypeForCodePageIfNeeded_ (const InternetMediaType& ct) const
431{
432 if (InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::Wildcards::kText, ct)) {
433 using AtomType = InternetMediaType::AtomType;
434 // Don't override already specifed characterset
435 Containers::Mapping<String, String> params = ct.GetParameters ();
436 params.Add ("charset"sv, Characters::GetCharsetString (fCodePage_), AddReplaceMode::eAddIfMissing);
437 return InternetMediaType{ct.GetType<AtomType> (), ct.GetSubType<AtomType> (), ct.GetSuffix (), params};
438 }
439 return ct;
440}
441
442void Response::Flush ()
443{
444#if USE_NOISY_TRACE_IN_THIS_MODULE_
445 Debug::TraceContextBumper ctx{"Response::Flush", "fState_ = {}"_f, static_cast<State> (fState_)};
446#endif
447 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
448
449 if (fState_ == State::ePreparingHeaders) {
450 StateTransition_ (State::eHeadersSent); // this flushes the headers
451 }
452 Assert (fState_ >= State::eHeadersSent);
453 fUseOutStream_.Flush ();
454}
455
456bool Response::End ()
457{
458#if USE_NOISY_TRACE_IN_THIS_MODULE_
459 Debug::TraceContextBumper ctx{"Response::End()"};
460 DbgTrace ("*this={}"_f, ToString ());
461#endif
462 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
463 if (fState_ != State::eCompleted) {
464 try {
465 fBodyRawStream_.CloseWrite (); // if any avail to read, must write that chunk...
466
467 ApplyBodyEncodingIfNeeded_ (); // normally done in flush, but that would be too late
468
469 // If NOT in chunked mode, Accumulate the body we will write before our initial flush, so that we have the right contentLength header
470 // Note this is needed even for HEAD Method, since we may need to emit the right Content-Length header even if there is
471 // no body
472 optional<BLOB> body2Write;
473 if (not chunkedTransferMode ()) {
474 if (fBodyCompressedStream_ != nullptr) {
475 body2Write = fBodyCompressedStream_.ReadAll ();
476 }
477 else {
478 body2Write = fBodyRawStream_.ReadAll ();
479 }
480 }
481 if (fState_ == State::ePreparingHeaders) {
482 StateTransition_ (State::eHeadersSent); // this flushes the headers
483 }
484 Assert (fState_ >= State::eHeadersSent);
485
486 if (chunkedTransferMode () and not fHeadMode_) {
487 if (fBodyCompressedStream_ != nullptr) {
488 auto b = fBodyCompressedStream_.ReadAll ();
489 if (not b.empty ()) {
490 WriteChunk_ (b);
491 }
492 }
493 else {
494 auto b = fBodyRawStream_.ReadAll ();
495 if (not b.empty ()) {
496 WriteChunk_ (b);
497 }
498 }
499 WriteChunk_ (span<const byte>{}); // an empty chunk marks the end of transfer encoding
500 }
501
502 if (fState_ == State::ePreparingHeaders) {
503 StateTransition_ (State::eHeadersSent); // this flushes the headers
504 }
505 if (body2Write) {
506 Assert (not chunkedTransferMode ());
507 Assert (fState_ != State::eCompleted); // We PREVENT any writes when completed
508 // See https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - body must not be sent for not-modified, HEAD, etc
509 if (this->hasEntityBody ()) {
510 fUseOutStream_.Write (*body2Write);
511 }
512 }
513 Assert (fState_ >= State::eHeadersSent);
514 StateTransition_ (State::eCompleted);
515 fUseOutStream_.Flush ();
516 }
517 catch (...) {
518 DbgTrace ("Exception during Response::End () automaticaly triggers Response::Abort()"_f);
519 Abort ();
520 WeakAssert (this->responseAborted ()); // NOT ALWAYS TRUE, cuz could have been completed before we got to abort
521 Ensure (this->responseCompleted ());
522 Execution::ReThrow (); // but still rethrow so caller can see failure
523 }
524 }
525 Ensure (fState_ == State::eCompleted);
526 return not fAborted_;
527}
528
529void Response::Abort ()
530{
531#if USE_NOISY_TRACE_IN_THIS_MODULE_
532 Debug::TraceContextBumper ctx{"Response::Abort()"};
533#endif
534 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
535 if (fState_ != State::eCompleted) {
536 fState_ = State::eCompleted;
537 fAborted_ = true;
538 fUseOutStream_.Abort ();
539 fSocket_.Close ();
540 if (fBodyRawStream_ != nullptr) {
541 fBodyRawStream_.Close ();
542 }
543 }
544 Ensure (fState_ == State::eCompleted);
545}
546
547void Response::Redirect (const URI& url)
548{
549 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
550 Require (this->headersCanBeSet ());
551 if (fBodyRawStream_ != nullptr) {
552 fBodyRawStream_.Close ();
553 }
554
555 // PERHAPS should clear some header values???
556 auto& updatableHeaders = this->rwHeaders ();
557 updatableHeaders.connection = IO::Network::HTTP::Headers::eClose;
558 updatableHeaders.location = url;
559 this->status = HTTP::StatusCodes::kMovedPermanently;
560 if (fState_ == State::ePreparingHeaders) {
561 StateTransition_ (State::eHeadersSent); // this flushes the headers
562 }
563 Assert (fState_ >= State::eHeadersSent);
564 StateTransition_ (State::eCompleted);
565}
566
567void Response::write (const span<const byte>& bytes)
568{
569#if USE_NOISY_TRACE_IN_THIS_MODULE_
570 Debug::TraceContextBumper ctx{"Response::write()", "bytes=byte[{}]{}"_f, bytes.size (), bytes};
571#endif
572 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
573 Require (not this->responseCompleted ());
574 Require (not this->responseStatusSent () or chunkedTransferMode ());
575 if (fETagDigester_) {
576 fETagDigester_->Write (bytes); // @todo - FIX - To send ETAG - we must use a trailer!!! - maybe other things as well!
577 }
578 fBodyRawStream_.Write (bytes);
579 fBodyRawStreamLength_ += bytes.size ();
580 Assert (fBodyRawStreamLength_ == fBodyRawStream_.GetWriteOffset ());
581
582 size_t thisChunkSize = fBodyRawStreamLength_ - fBodyRowStreamLengthWhenLastChunkGenerated_; // compute when to chunk based on raw write size not compressed size cuz easier - for now --LGP 2024-06-26
583 if (thisChunkSize > fAutoTransferChunkSize_.value_or (kAutomaticTransferChunkSize_Default) and not chunkedTransferMode ()) {
584 rwHeaders ().transferEncoding = HTTP::TransferEncoding::kChunked;
585 }
586
587 if (fState_ == State::ePreparingHeaders and chunkedTransferMode ()) {
588 // must send first draft of headers before we can begin chunked transfer emitting
589 // but cannot flush if not chunked transfer mode, cuz then we would be freezing content-length (unless we were handed it
590 // externally, but we don't currently have an API for that -- LGP 2024-06-25 - could add promisedContentLength property(but that would disallow our compression)
591 // maybe if/when we support trailers we wont need to worry about this.
592 StateTransition_ (State::eHeadersSent); // this sends the headers (not necessarily all the way out over wire but through our pipeline)
593 Assert (fState_ >= State::eHeadersSent);
594 }
595
596 FlushNextChunkIfNeeded_ ();
597}
598
599template <>
600void Response::write (const String& s)
601{
602 Memory::StackBuffer<Character> maybeIgnoreBuf1;
603 span<const Character> thisData = s.GetData (&maybeIgnoreBuf1);
604 string cpStr = codeCvt ().String2Bytes<string> (thisData);
605 if (not cpStr.empty ()) {
606 write (as_bytes (span{cpStr.c_str (), cpStr.length ()}));
607 }
608}
609
610void Response::printf (const wchar_t* format, ...)
611{
612 ////DEPRECATED
613 AssertExternallySynchronizedMutex::WriteContext declareContext{_fThisAssertExternallySynchronized};
614 va_list argsList;
615 va_start (argsList, format);
616 String tmp = Characters::FormatV (format, argsList);
617 va_end (argsList);
618 write (tmp);
619}
620
621void Response::FlushNextChunkIfNeeded_ ()
622{
623 if (not fHeadMode_ and chunkedTransferMode ()) {
624 size_t thisChunkSize = fBodyRawStreamLength_ - fBodyRowStreamLengthWhenLastChunkGenerated_; // compute when to chunk based on raw write size not compressed size cuz easier - for now --LGP 2024-06-26
625 if (thisChunkSize > fAutoTransferChunkSize_.value_or (kAutomaticTransferChunkSize_Default)) {
626 BLOB data2Write;
627 if (this->fBodyCompressedStream_ != nullptr) {
628 if (auto o = fBodyCompressedStream_.ReadAllAvailable ()) {
629 data2Write = *o;
630 }
631 }
632 else {
633 optional<Memory::InlineBuffer<byte>> aa = fBodyRawStream_.ReadAllAvailable ();
634 if (aa.has_value ()) {
635 data2Write = BLOB{*aa};
636 }
637 }
638 // Don't write empty chunks as they would mark the end of the response
639 if (not data2Write.empty ()) {
640 WriteChunk_ (data2Write);
641 fBodyRowStreamLengthWhenLastChunkGenerated_ = fBodyRawStreamLength_;
642 }
643 }
644 }
645}
646
647String Response::ToString () const
648{
649 AssertExternallySynchronizedMutex::ReadContext declareContext{_fThisAssertExternallySynchronized};
650 StringBuilder sb = inherited::ToString ().SubString (0, -1); // strip trailing '}'
651 sb << "Socket: "sv << fSocket_;
652 sb << ", chunkedTransferMode: "sv << this->chunkedTransferMode ();
653 sb << ", hasEntityBody: "sv << this->hasEntityBody ();
654 sb << ", State: "sv << fState_;
655 sb << ", CodePage: "sv << fCodePage_;
656 //sb << ", BodyRawStream_: "sv << fBodyRawStream_ ; // @todo write seek pos, non-null etc
657 //sb << ", ProtocolOutputStream: "sv << fProtocolOutputStream_ ; // @todo write seek pos, non-null etc
658 //sb << ", BodyCompressedStream_: "sv << fBodyCompressedStream_ ; // @todo write seek pos, non-null etc
659 sb << ", HeadMode: "sv << fHeadMode_;
660 sb << ", ETagDigester: "sv << fETagDigester_.has_value ();
661 sb << "}"sv;
662 return sb;
663}
#define RequireNotReached()
Definition Assertions.h:385
#define qStroika_Foundation_Debug_AssertionsChecked
The qStroika_Foundation_Debug_AssertionsChecked flag determines if assertions are checked and validat...
Definition Assertions.h:48
#define WeakAssert(c)
A WeakAssert() is for things that aren't guaranteed to be true, but are overwhelmingly likely to be t...
Definition Assertions.h:438
#define AssertNotReached()
Definition Assertions.h:355
#define DbgTrace
Definition Trace.h:309
CodeCvt unifies byte <-> unicode conversions, vaguely inspired by (and wraps) std::codecvt,...
Definition CodeCvt.h:118
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
static span< const CHAR_TYPE > GetData(const PeekSpanData &pds, Memory::StackBuffer< CHAR_TYPE, STACK_BUFFER_SZ > *possiblyUsedBuffer)
return the constant character data inside the string (rep) in the form of a span, possibly quickly an...
Definition String.inl:961
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:190
nonvirtual RETURN_TYPE GetType() const
Gets the primary (major) type of the full internet media type (as a string or atom)
nonvirtual optional< RETURN_TYPE > GetSuffix() const
this is the +XXX part of the internet media type (e.g. +xml) and is often omitted (but note this omit...
static Execution::Synchronized< InternetMediaTypeRegistry > sThe
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
shared_lock< const AssertExternallySynchronizedMutex > ReadContext
Instantiate AssertExternallySynchronizedMutex::ReadContext to designate an area of code where protect...
unique_lock< AssertExternallySynchronizedMutex > WriteContext
Instantiate AssertExternallySynchronizedMutex::WriteContext to designate an area of code where protec...
ClientErrorException is to capture exceptions caused by a bad (e.g ill-formed) request.
roughly equivalent to Association<String,String>, except that the class is smart about certain keys a...
Definition Headers.h:129
a smart pointer wrapper (like shared_ptr <_IRep>).
Definition Socket.h:178
nonvirtual bool empty() const
Definition BLOB.inl:246
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
OutputStream<>::Ptr is Smart pointer to a stream-based sink of data.
String ToString(T &&t, ARGS... args)
Return a debug-friendly, display version of the argument: not guaranteed parsable or usable except fo...
Definition ToString.inl:465
void Abort(const Traversal::Iterable< Ptr > &threads)
foreach Thread t: t.Abort ()
Definition Thread.cpp:987
Content coding values indicate an encoding transformation that has been or can be applied to an entit...
Transfer-Encoding is a hop-by-hop header, that is applied to a message between two nodes,...
Options to configure a new SharedMemoryStream; defaults should always work fine, but options can allo...
Execution::InternallySynchronized fInternallySynchronized
controls if the shared stream is automatically synchronized internally so that it can be used by two ...