NetServices: Add support for an input body in a request and handle this with redirects.

Change-Id: Id2399d49aa673469c8c04ebd13884cdbcb24112d
This commit is contained in:
Niels Sascha Reedijk
2022-06-23 21:13:35 +01:00
parent 6cbbd9bf4e
commit 92357c131e
9 changed files with 533 additions and 73 deletions
+122 -1
View File
@@ -302,6 +302,52 @@ namespace Network {
*/ */
/*!
\struct BHttpRequest::Body
\ingroup netservices
\brief Describe the body for a network request
\since Haiku R1
*/
/*!
\var std::unique_ptr<BDataIO> BHttpRequest::Body::input
\brief The \ref BDataIO object that holds the contents of the body.
\since Haiku R1
*/
/*!
\var BString BHttpRequest::Body::mimeType
\brief The mimetype of the body.
The \c Content-Type header field of the request is set to this value.
\since Haiku R1
*/
/*!
\var std::optional<off_t> BHttpRequest::Body::size
\brief The size of the content, if known.
\since Haiku R1
*/
/*!
\var std::optional<off_t> BHttpRequest::Body::startPosition
\brief If the input is a \ref BPositionIO, this is the current position when the body was set.
This value is used to rewind the input when it needs to be resubmitted, for example in the case
of a redirection.
\since Haiku R1
*/
/*! /*!
\class BHttpRequest \class BHttpRequest
\ingroup netservices \ingroup netservices
@@ -310,7 +356,6 @@ namespace Network {
This class can be used to construct HTTP requests that can be executed by the Network Services This class can be used to construct HTTP requests that can be executed by the Network Services
Kit. A request has two states, either it is is a valid request, or it is an empty request. The Kit. A request has two states, either it is is a valid request, or it is an empty request. The
criterium is whether or not the request has a URL. criterium is whether or not the request has a URL.
This class has all kinds of convenience methods set and retrieve particular options. Most This class has all kinds of convenience methods set and retrieve particular options. Most
options are wrapped in specialized container classes that do some form of validation. options are wrapped in specialized container classes that do some form of validation.
@@ -341,6 +386,12 @@ namespace Network {
<td> How many redirections should be followed. Set to 0 to disable. </td> <td> How many redirections should be followed. Set to 0 to disable. </td>
<td> Defaults to 8 redirections per request </td> <td> Defaults to 8 redirections per request </td>
</tr> </tr>
<tr>
<td> \ref RequestBody() </td>
<td> \ref SetRequestBody() </td>
<td> Body contents that is sent with the request. </td>
<td> Defaults to an empty body </td>
</tr>
<tr> <tr>
<td> \ref StopOnError() </td> <td> \ref StopOnError() </td>
<td> \ref SetStopOnError() </td> <td> \ref SetStopOnError() </td>
@@ -527,6 +578,17 @@ namespace Network {
*/ */
/*!
\fn const BHttpRequest::Body* BHttpRequest::RequestBody() const noexcept
\brief Get the details of the custom body set for the request.
\return When no body is set for this request, the method returns a \c nullptr.
Otherwise, it will return a pointer to a struct that describes the current body.
\since Haiku R1
*/
/*! /*!
\fn bool BHttpRequest::StopOnError() const noexcept \fn bool BHttpRequest::StopOnError() const noexcept
\brief Is the request set to parse the full response on error. \brief Is the request set to parse the full response on error.
@@ -593,6 +655,8 @@ namespace Network {
* \c Accept * \c Accept
* \c Accept-Encoding * \c Accept-Encoding
* \c Connection * \c Connection
* \c Content-Type
* \c Content-Length
\param fields Additional fields for the header of the request. \param fields Additional fields for the header of the request.
@@ -641,6 +705,31 @@ namespace Network {
*/ */
/*!
\fn void BHttpRequest::SetRequestBody(std::unique_ptr<BDataIO> input, BString mimeType,
std::optional<off_t> size)
\brief Set a body for this request.
When the requests needs a body, this method can be used to set the contents of that body.
\param input The input is an owned pointer to an input. The lifetime of the input is guaranteed
up to the point that the request is sent for execution.
\param mimeType A valid mimetype, with a class and a subtype. For example \c text/plain is a
valid mime type.
\param size When the content size is set, the request will have a \c Content-Length header
field. If the \a input has less data in the buffer, this will cause the request to
error out. However, if the input has more data, it is only read up to size. If the actual
size of the data is unknown, this can be made optional. The request body will
then be sent as a so-called chunked transfer, sending data until the input is at the end.
\exception std::bad_alloc This exception may be raised if it is impossible to allocate memory.
\exception std::invalid_argument This exception is raised when the \a mimeType is invalid or
when \a input is a \c nullptr.
\since Haiku R1
*/
/*! /*!
\fn void BHttpRequest::SetStopOnError(bool stopOnError) \fn void BHttpRequest::SetStopOnError(bool stopOnError)
\brief Set whether the entire response will be parsed on a client or server error. \brief Set whether the entire response will be parsed on a client or server error.
@@ -697,6 +786,38 @@ namespace Network {
//! @} //! @}
/*!
\name Clearing options
*/
//! @{
/*!
\fn void BHttpRequest::ClearAuthentication() noexcept
\brief Clear any authentication details previously set with \ref SetAuthentication().
If there is no authentication data set, this method does nothing.
\since Haiku R1
*/
/*!
\fn std::unique_ptr<BDataIO> BHttpRequest::ClearRequestBody() noexcept
\brief Clear any request body previously set with \ref SetRequestBody().
\return Returns the previously set input \ref BDataIO object. If there is no request body set,
this method returns \c nullptr.
\since Haiku R1
*/
//! @}
/*! /*!
\name Serialization \name Serialization
*/ */
+33
View File
@@ -105,6 +105,39 @@ namespace Network {
*/ */
/*!
\fn ssize_t BAbstractDataStream::BufferData(BDataIO* source, size_t maxSize)
\brief Internal method to append data to the internal buffer.
This is a helper method that reads data from a \a source into the internal \ref fBuffer.
The number of bytes depends loaded depends on the following properties:
- The maximum number of bytes for the fBuffer is set to 65kB. If there already is data in
the buffer, only additional bytes up to the maximum buffer size are read.
- If \a maxSize is larger than 65kB, or if \a maxSize plus the current size of the input
buffer is larger than 65kB, only a maximum of 65kB will be loaded.
- If the \a source has fewer than \a maxSize bytes, then fewer bytes will be loaded.
\param source The data source to read from.
\param maxSize The maximum size to read from the source.
\return The output of the \ref BDataIO::Read() call that is executed on the source. When actual
data is read, this will be the number of bytes that are read. If it is an error, all errors
will be returned, except for \c B_INTERRUPTED, as interrupted \ref BDataIO::Read() calls
are retried. It is up to the calling implementation to do further error handling.
\exception std::bad_alloc This exception may be raised if it is impossible to allocate memory.
\since Haiku R1
*/
/*!
\var std::vector<std::byte> BAbstractDataStream::fBuffer
\brief Internal buffer that can be used by implementations to buffer data.
*/
/*! /*!
\class BHttpRequestStream \class BHttpRequestStream
\ingroup netservices \ingroup netservices
@@ -7,6 +7,7 @@
#define _B_HTTP_REQUEST_H_ #define _B_HTTP_REQUEST_H_
#include <memory> #include <memory>
#include <optional>
#include <string_view> #include <string_view>
#include <variant> #include <variant>
@@ -82,6 +83,14 @@ struct BHttpAuthentication {
class BHttpRequest { class BHttpRequest {
public: public:
// Aggregate parameter types
struct Body {
std::unique_ptr<BDataIO> input;
BString mimeType;
std::optional<off_t> size;
std::optional<off_t> startPosition;
};
// Constructors and Destructor // Constructors and Destructor
BHttpRequest(); BHttpRequest();
BHttpRequest(const BUrl& url); BHttpRequest(const BUrl& url);
@@ -99,6 +108,7 @@ public:
const BHttpFields& Fields() const noexcept; const BHttpFields& Fields() const noexcept;
uint8 MaxRedirections() const noexcept; uint8 MaxRedirections() const noexcept;
const BHttpMethod& Method() const noexcept; const BHttpMethod& Method() const noexcept;
const Body* RequestBody() const noexcept;
bool StopOnError() const noexcept; bool StopOnError() const noexcept;
bigtime_t Timeout() const noexcept; bigtime_t Timeout() const noexcept;
const BUrl& Url() const noexcept; const BUrl& Url() const noexcept;
@@ -108,10 +118,16 @@ public:
void SetFields(const BHttpFields& fields); void SetFields(const BHttpFields& fields);
void SetMaxRedirections(uint8 maxRedirections); void SetMaxRedirections(uint8 maxRedirections);
void SetMethod(const BHttpMethod& method); void SetMethod(const BHttpMethod& method);
void SetRequestBody(std::unique_ptr<BDataIO> input,
BString mimeType, std::optional<off_t> size);
void SetStopOnError(bool stopOnError); void SetStopOnError(bool stopOnError);
void SetTimeout(bigtime_t timeout); void SetTimeout(bigtime_t timeout);
void SetUrl(const BUrl& url); void SetUrl(const BUrl& url);
// Clearing Options
void ClearAuthentication() noexcept;
std::unique_ptr<BDataIO> ClearRequestBody() noexcept;
// Serialization // Serialization
ssize_t SerializeHeaderTo(BDataIO* target) const; ssize_t SerializeHeaderTo(BDataIO* target) const;
BString HeaderToString() const; BString HeaderToString() const;
@@ -119,6 +135,9 @@ public:
private: private:
friend class BHttpSession; friend class BHttpSession;
struct Data; struct Data;
bool RewindBody() noexcept;
std::unique_ptr<Data> fData; std::unique_ptr<Data> fData;
}; };
+12 -6
View File
@@ -7,6 +7,7 @@
#define _HTTP_STREAM_H_ #define _HTTP_STREAM_H_
#include <memory> #include <memory>
#include <vector>
class BDataIO; class BDataIO;
class BMallocIO; class BMallocIO;
@@ -29,7 +30,12 @@ public:
bool complete; bool complete;
}; };
virtual TransferInfo Transfer(BDataIO*) = 0; virtual TransferInfo Transfer(BDataIO*) = 0;
protected:
ssize_t BufferData(BDataIO* source, size_t maxSize);
std::vector<std::byte> fBuffer;
}; };
@@ -41,11 +47,11 @@ public:
virtual TransferInfo Transfer(BDataIO* target) override; virtual TransferInfo Transfer(BDataIO* target) override;
private: private:
std::unique_ptr<BMallocIO> fHeader; off_t fRemainingHeaderSize = 0;
BDataIO* fBody; BDataIO* fBody = nullptr;
off_t fTotalSize = 0; off_t fTotalBodySize = 0;
off_t fBodyOffset = 0; off_t fBufferedBodySize = 0;
off_t fCurrentPos = 0; off_t fTransferredBodySize = 0;
}; };
@@ -15,6 +15,7 @@
#include <DataIO.h> #include <DataIO.h>
#include <HttpFields.h> #include <HttpFields.h>
#include <MimeType.h>
#include <NetServicesDefs.h> #include <NetServicesDefs.h>
#include <Url.h> #include <Url.h>
@@ -166,6 +167,7 @@ struct BHttpRequest::Data {
std::optional<BHttpAuthentication> authentication; std::optional<BHttpAuthentication> authentication;
bool stopOnError = false; bool stopOnError = false;
bigtime_t timeout = B_INFINITE_TIMEOUT; bigtime_t timeout = B_INFINITE_TIMEOUT;
std::optional<Body> requestBody;
}; };
@@ -255,6 +257,15 @@ BHttpRequest::Method() const noexcept
} }
const BHttpRequest::Body*
BHttpRequest::RequestBody() const noexcept
{
if (fData && fData->requestBody)
return std::addressof(*fData->requestBody);
return nullptr;
}
bool bool
BHttpRequest::StopOnError() const noexcept BHttpRequest::StopOnError() const noexcept
{ {
@@ -292,11 +303,13 @@ BHttpRequest::SetAuthentication(const BHttpAuthentication& authentication)
} }
static constexpr std::array<std::string_view, 4> fReservedOptionalFieldNames = { static constexpr std::array<std::string_view, 6> fReservedOptionalFieldNames = {
"Host"sv, "Host"sv,
"Accept"sv, "Accept"sv,
"Accept-Encoding"sv, "Accept-Encoding"sv,
"Connection"sv "Connection"sv,
"Content-Type"sv,
"Content-Length"sv
}; };
@@ -336,6 +349,33 @@ BHttpRequest::SetMethod(const BHttpMethod& method)
} }
void
BHttpRequest::SetRequestBody(std::unique_ptr<BDataIO> input, BString mimeType,
std::optional<off_t> size)
{
if (input == nullptr)
throw std::invalid_argument("input cannot be null");
// TODO: support optional mimetype arguments like type/subtype;parameter=value
if (!BMimeType::IsValid(mimeType.String()))
throw std::invalid_argument("mimeType must be a valid mimetype");
// TODO: review if there should be complex validation between the method and whether or not
// there is a request body. The current implementation does the validation at the request
// generation stage, where GET, HEAD, OPTIONS, CONNECT and TRACE will not submit a body.
if (!fData)
fData = std::make_unique<Data>();
fData->requestBody = {std::move(input), std::move(mimeType), size};
// Check if the input is a BPositionIO, and if so, store the current position, so that it can
// be rewinded in case of a redirect.
auto inputPositionIO = dynamic_cast<BPositionIO*>(fData->requestBody->input.get());
if (inputPositionIO != nullptr)
fData->requestBody->startPosition = inputPositionIO->Position();
}
void void
BHttpRequest::SetStopOnError(bool stopOnError) BHttpRequest::SetStopOnError(bool stopOnError)
{ {
@@ -373,6 +413,26 @@ BHttpRequest::SetUrl(const BUrl& url)
} }
void
BHttpRequest::ClearAuthentication() noexcept
{
if (fData)
fData->authentication = std::nullopt;
}
std::unique_ptr<BDataIO>
BHttpRequest::ClearRequestBody() noexcept
{
if (fData && fData->requestBody) {
auto body = std::move(fData->requestBody->input);
fData->requestBody = std::nullopt;
return body;
}
return nullptr;
}
[[nodiscard]] static inline ssize_t [[nodiscard]] static inline ssize_t
_write_to_dataio(BDataIO* target, const std::string_view& data) _write_to_dataio(BDataIO* target, const std::string_view& data)
{ {
@@ -437,6 +497,14 @@ BHttpRequest::SerializeHeaderTo(BDataIO* target) const
outputFields.AddField("Authorization"sv, std::string_view(authorization.String())); outputFields.AddField("Authorization"sv, std::string_view(authorization.String()));
} }
if (fData->requestBody) {
outputFields.AddField("Content-Type"sv, std::string_view(fData->requestBody->mimeType.String()));
if (fData->requestBody->size)
outputFields.AddField("Content-Length"sv, std::to_string(*fData->requestBody->size));
else
throw BRuntimeError(__PRETTY_FUNCTION__, "Transfer body with unknown content length; chunked transfer not supported");
}
for (const auto& field: outputFields) { for (const auto& field: outputFields) {
bytesWritten += _write_to_dataio(target, field.RawField()); bytesWritten += _write_to_dataio(target, field.RawField());
bytesWritten += _write_to_dataio(target, "\r\n"sv); bytesWritten += _write_to_dataio(target, "\r\n"sv);
@@ -460,3 +528,21 @@ BHttpRequest::HeaderToString() const
return BString(static_cast<const char*>(buffer.Buffer()), size); return BString(static_cast<const char*>(buffer.Buffer()), size);
} }
/*!
\brief Private method used by BHttpSession::Request to rewind the content in case of redirect
\retval true Content was rewinded successfully. Also the case if there is no content
\retval false Cannot/could not rewind content.
*/
bool
BHttpRequest::RewindBody() noexcept
{
if (fData && fData->requestBody && fData->requestBody->startPosition) {
auto inputData = dynamic_cast<BPositionIO*>(fData->requestBody->input.get());
return *fData->requestBody->startPosition
== inputData->Seek(*fData->requestBody->startPosition, SEEK_SET);
}
return true;
}
@@ -134,7 +134,7 @@ private:
bool fNoContent = false; bool fNoContent = false;
// Redirection // Redirection
BHttpStatus fRedirectStatus; std::optional<BHttpStatus> fRedirectStatus;
int8 fRemainingRedirects; int8 fRemainingRedirects;
// Optional decompression // Optional decompression
@@ -652,7 +652,7 @@ BHttpSession::Request::Request(Request& original, const BHttpSession::Redirect&
if (redirect.redirectToGet if (redirect.redirectToGet
&& (fRequest.Method() != BHttpMethod::Head && fRequest.Method() != BHttpMethod::Get)) { && (fRequest.Method() != BHttpMethod::Head && fRequest.Method() != BHttpMethod::Get)) {
fRequest.SetMethod(BHttpMethod::Get); fRequest.SetMethod(BHttpMethod::Get);
// TODO: clear Post fields/Update Data when that is supported. fRequest.ClearRequestBody();
} }
fRemainingRedirects = original.fRemainingRedirects--; fRemainingRedirects = original.fRemainingRedirects--;
@@ -809,18 +809,33 @@ BHttpSession::Request::ReceiveResult()
if (status.code != 0) { if (status.code != 0) {
// the status headers are now received, decide what to do next // the status headers are now received, decide what to do next
// Handle redirects // Determine if we can handle redirects; else notify of receiving status
if (status.StatusClass() == BHttpStatusClass::Redirection && fRemainingRedirects > 0) { if (fRemainingRedirects > 0) {
fRedirectStatus = std::move(status); switch (status.StatusCode()) {
} else { case BHttpStatusCode::MovedPermanently:
// Register NoContent before moving the status to the result case BHttpStatusCode::TemporaryRedirect:
if (status.StatusCode() == BHttpStatusCode::NoContent) case BHttpStatusCode::PermanentRedirect:
fNoContent = true; // These redirects require the request body to be sent again. It this is
// possible, BHttpRequest::RewindBody() will return true in which case we can
fResult->SetStatus(std::move(status)); // handle the redirect.
// TODO: inform listeners of receiving the status code if (!fRequest.RewindBody())
break;
[[fallthrough]];
case BHttpStatusCode::Found:
case BHttpStatusCode::SeeOther:
// These redirects redirect to GET, so we don't care if we can rewind the
// body; in this case redirect
fRedirectStatus = std::move(status);
break;
default:
break;
}
} }
// Register NoContent before moving the status to the result
if (status.StatusCode() == BHttpStatusCode::NoContent)
fNoContent = true;
if ((status.StatusClass() == BHttpStatusClass::ClientError if ((status.StatusClass() == BHttpStatusClass::ClientError
|| status.StatusClass() == BHttpStatusClass::ServerError) || status.StatusClass() == BHttpStatusClass::ServerError)
&& fRequest.StopOnError()) && fRequest.StopOnError())
@@ -832,7 +847,10 @@ BHttpSession::Request::ReceiveResult()
return true; return true;
} }
// TODO: handle the case where we have an error code and we want to stop on error if (!fRedirectStatus) {
// we are not redirecting and there is no error, so inform listeners
fResult->SetStatus(std::move(status));
}
fRequestStatus = StatusReceived; fRequestStatus = StatusReceived;
} else { } else {
@@ -863,9 +881,9 @@ BHttpSession::Request::ReceiveResult()
// The headers have been received, now set up the rest of the response handling // The headers have been received, now set up the rest of the response handling
// Handle redirects // Handle redirects
if (fRedirectStatus.StatusClass() == BHttpStatusClass::Redirection) { if (fRedirectStatus) {
auto redirectToGet = false; auto redirectToGet = false;
switch (fRedirectStatus.StatusCode()) { switch (fRedirectStatus->StatusCode()) {
case BHttpStatusCode::Found: case BHttpStatusCode::Found:
case BHttpStatusCode::SeeOther: case BHttpStatusCode::SeeOther:
// 302 and 303 redirections convert all requests to GET request, except for HEAD // 302 and 303 redirections convert all requests to GET request, except for HEAD
@@ -875,7 +893,7 @@ BHttpSession::Request::ReceiveResult()
case BHttpStatusCode::TemporaryRedirect: case BHttpStatusCode::TemporaryRedirect:
case BHttpStatusCode::PermanentRedirect: case BHttpStatusCode::PermanentRedirect:
{ {
std::cout << "ReceiveResult() [" << Id() << "] Handle redirect with status: " << fRedirectStatus.code << std::endl; std::cout << "ReceiveResult() [" << Id() << "] Handle redirect with status: " << fRedirectStatus->code << std::endl;
auto locationField = fFields.FindField("Location"); auto locationField = fFields.FindField("Location");
if (locationField == fFields.end()) { if (locationField == fFields.end()) {
throw BNetworkRequestError(__PRETTY_FUNCTION__, throw BNetworkRequestError(__PRETTY_FUNCTION__,
+161 -26
View File
@@ -8,19 +8,122 @@
#include <HttpStream.h> #include <HttpStream.h>
#include <optional>
#include <DataIO.h> #include <DataIO.h>
#include <HttpRequest.h> #include <HttpRequest.h>
using namespace BPrivate::Network; using namespace BPrivate::Network;
// Buffer size constant
constexpr std::size_t kBufferSize = 64 * 1024;
// #pragma mark -- ByteIOHelper base class
class ByteIOHelper : public BDataIO {
public:
ByteIOHelper(std::vector<std::byte>& buffer);
virtual ssize_t Read(void* buffer, size_t size) override;
virtual ssize_t Write(const void* buffer, size_t size) override;
private:
std::vector<std::byte>& fBuffer;
};
ByteIOHelper::ByteIOHelper(std::vector<std::byte>& buffer)
: fBuffer(buffer)
{
if (buffer.size() != 0)
throw BRuntimeError(__PRETTY_FUNCTION__, "Target buffer with size > 0");
}
ssize_t
ByteIOHelper::Read(void* buffer, size_t size)
{
throw BRuntimeError(__PRETTY_FUNCTION__, "Unexpected Read() call");
}
ssize_t
ByteIOHelper::Write(const void* buffer, size_t size)
{
auto remainingSize = kBufferSize - fBuffer.size();
if (remainingSize < 0)
return 0;
if (size > remainingSize)
size = remainingSize;
auto bufferCast = static_cast<const std::byte*>(buffer);
fBuffer.insert(fBuffer.end(), bufferCast, bufferCast + size);
return size;
}
// #pragma mark -- BAbstractDataStream (helper methods)
/*!
\brief Load data from \a source into the internal buffer.
The buffer will be filled up to the maximum size (64kB). Partial reads are supported; it will
not do a retry.
\return The return value of the underlying BDataIO::Read() call.
*/
ssize_t
BAbstractDataStream::BufferData(BDataIO* source, size_t maxSize)
{
auto currentSize = fBuffer.size();
auto remainingSize = kBufferSize - currentSize;
if (remainingSize < 0)
return B_OK;
if (remainingSize > maxSize)
remainingSize = maxSize;
fBuffer.resize(currentSize + remainingSize);
ssize_t readSize = B_INTERRUPTED;
while (readSize == B_INTERRUPTED)
readSize = source->Read(fBuffer.data() + currentSize, remainingSize);
if (readSize <= 0) {
fBuffer.resize(currentSize); // resize back to the original size
return readSize;
}
if (readSize > 0)
fBuffer.resize(currentSize + readSize);
return readSize;
}
// #pragma mark -- BHttpRequestStream
BHttpRequestStream::BHttpRequestStream(const BHttpRequest& request) BHttpRequestStream::BHttpRequestStream(const BHttpRequest& request)
: fHeader(std::make_unique<BMallocIO>()), fBody(nullptr) : fBody(nullptr)
{ {
// Serialize the header of the request to text // Serialize the header of the request to text
fTotalSize = request.SerializeHeaderTo(fHeader.get()); ByteIOHelper helper(fBuffer);
fBodyOffset = fTotalSize; fRemainingHeaderSize = request.SerializeHeaderTo(&helper);
// TODO: add size of request body to total size
// Check if there is a body
if (auto requestBody = request.RequestBody()) {
fBody = requestBody->input.get();
if (!requestBody->size) {
throw BRuntimeError(__PRETTY_FUNCTION__,
"BHttpRequestStream: chunked transfer not implemented");
}
fTotalBodySize += *requestBody->size;
}
} }
@@ -30,32 +133,64 @@ BHttpRequestStream::~BHttpRequestStream() = default;
BHttpRequestStream::TransferInfo BHttpRequestStream::TransferInfo
BHttpRequestStream::Transfer(BDataIO* target) BHttpRequestStream::Transfer(BDataIO* target)
{ {
if (fCurrentPos == fTotalSize) if (fBuffer.size() == 0 && fTotalBodySize == fBufferedBodySize) {
return TransferInfo{0, fTotalSize, fTotalSize, true}; // all done; header was written and no more body left
return TransferInfo{0, fTotalBodySize, fTotalBodySize, true};
}
off_t bytesWritten = 0; if (fBody != nullptr && fBuffer.size() < kBufferSize) {
// buffer additional data from the body in the buffer
auto remainingBodySize = fTotalBodySize - fBufferedBodySize;
auto bufferedSize = BufferData(fBody, remainingBodySize);
if (fCurrentPos < fBodyOffset) { if (bufferedSize == B_WOULD_BLOCK) {
// Writing the header // do nothing; try again next round
auto remainingSize = fBodyOffset - fCurrentPos; } else if (bufferedSize == 0) {
bytesWritten = target->Write( // no remaining data; throw error
static_cast<const char*>(fHeader->Buffer()) + fCurrentPos, remainingSize); throw BRuntimeError(__PRETTY_FUNCTION__,
if (bytesWritten == B_WOULD_BLOCK) "No more data in request input body while more data is expected");
return TransferInfo{0, 0, fTotalSize, false}; } else if (bufferedSize < 0) {
else if (bytesWritten < 0) throw BSystemError(__PRETTY_FUNCTION__, bufferedSize);
throw BSystemError("BDataIO::Write()", bytesWritten); } else {
// update counters
fCurrentPos += bytesWritten; fBufferedBodySize += bufferedSize;
if (fBufferedBodySize == fTotalBodySize) {
if (bytesWritten < remainingSize) { // no more body to load
return TransferInfo{bytesWritten, fCurrentPos, fTotalSize, false}; fBody = nullptr;
}
} }
} }
// Write the body if (fBuffer.size() == 0) {
if (fBody) { // nothing this round
// TODO return TransferInfo{0, fTransferredBodySize, fTotalBodySize, false};
throw BRuntimeError(__PRETTY_FUNCTION__, "Not implemented");
} }
return TransferInfo{bytesWritten, fTotalSize, fTotalSize, true};
auto bytesWritten = target->Write(fBuffer.data(), fBuffer.size());
if (bytesWritten == B_WOULD_BLOCK || bytesWritten == 0)
return TransferInfo{0, fTransferredBodySize, fTotalBodySize, false};
else if (bytesWritten < 0)
throw BSystemError("BDataIO::Write()", bytesWritten);
// Adjust the buffer
if (static_cast<size_t>(bytesWritten) == fBuffer.size())
fBuffer.clear();
else
fBuffer.erase(fBuffer.begin(), fBuffer.begin() + bytesWritten);
// Update the stats and return
if (fRemainingHeaderSize > 0){
if (bytesWritten >= fRemainingHeaderSize) {
bytesWritten -= fRemainingHeaderSize;
fRemainingHeaderSize = 0;
} else {
fRemainingHeaderSize -= bytesWritten;
bytesWritten = 0;
}
}
fTransferredBodySize += bytesWritten;
auto complete = fRemainingHeaderSize == 0 && fTransferredBodySize == fTotalBodySize;
return TransferInfo{bytesWritten, fTransferredBodySize, fTotalBodySize, complete};
} }
@@ -360,31 +360,10 @@ HttpProtocolTest::HttpRequestStreamTest()
BHttpRequestStream requestStream(request); BHttpRequestStream requestStream(request);
RequestStreamTestIO testIO(kExpectedStreamText.data()); RequestStreamTestIO testIO(kExpectedStreamText.data());
bool finished = false; bool finished = false;
ssize_t expectedBytesWritten = 8;
ssize_t expectedTotalBytesWritten = 8;
const ssize_t expectedTotalSize = kExpectedStreamText.size();
if (expectedTotalSize < 8) {
expectedBytesWritten = expectedTotalSize;
expectedTotalBytesWritten = expectedTotalSize;
}
while (!finished) { while (!finished) {
auto [currentBytesWritten, totalBytesWritten, totalSize, complete] auto [currentBytesWritten, totalBytesWritten, totalSize, complete]
= requestStream.Transfer(&testIO); = requestStream.Transfer(&testIO);
CPPUNIT_ASSERT_EQUAL(expectedBytesWritten, currentBytesWritten); finished = complete;
CPPUNIT_ASSERT_EQUAL(expectedTotalBytesWritten, totalBytesWritten);
CPPUNIT_ASSERT_EQUAL(expectedTotalSize, totalSize);
if (expectedTotalBytesWritten == expectedTotalSize) {
// loop should be finished
CPPUNIT_ASSERT_EQUAL(true, complete);
finished = true;
} else {
// prepare for next loop
if (totalSize - totalBytesWritten < 8) {
expectedBytesWritten = totalSize - totalBytesWritten;
}
expectedTotalBytesWritten += expectedBytesWritten;
CPPUNIT_ASSERT_EQUAL(false, complete);
}
} }
} }
@@ -497,6 +476,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent)
testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest);
testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest); testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest);
testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest); testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest);
testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest);
suite.addTest(testCaller); suite.addTest(testCaller);
parent.addTest("HttpIntegrationTest", &suite); parent.addTest("HttpIntegrationTest", &suite);
@@ -520,6 +500,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent)
testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest);
testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest); testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest);
testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest); testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest);
testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest);
suite.addTest(testCaller); suite.addTest(testCaller);
parent.addTest("HttpsIntegrationTest", &suite); parent.addTest("HttpsIntegrationTest", &suite);
@@ -735,3 +716,63 @@ HttpIntegrationTest::RequestCancelTest()
CPPUNIT_ASSERT(e.Type() == BNetworkRequestError::Canceled); CPPUNIT_ASSERT(e.Type() == BNetworkRequestError::Canceled);
} }
} }
static const BString kPostText =
"The MIT License\n"
"\n"
"Copyright (c) <year> <copyright holders>\n"
"\n"
"Permission is hereby granted, free of charge, to any person obtaining a copy\n"
"of this software and associated documentation files (the \"Software\"), to deal\n"
"in the Software without restriction, including without limitation the rights\n"
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
"copies of the Software, and to permit persons to whom the Software is\n"
"furnished to do so, subject to the following conditions:\n"
"\n"
"The above copyright notice and this permission notice shall be included in\n"
"all copies or substantial portions of the Software.\n"
"\n"
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n"
"THE SOFTWARE.\n"
"\n";
static BString kExpectedPostBody
= BString().SetToFormat(
"Path: /post\r\n"
"\r\n"
"Headers:\r\n"
"--------\r\n"
"Host: 127.0.0.1:PORT\r\n"
"Accept: *\r\n"
"Accept-Encoding: gzip\r\n"
"Connection: close\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 1083\r\n"
"\r\n"
"Request body:\r\n"
"-------------\r\n"
"%s\r\n", kPostText.String());
void
HttpIntegrationTest::PostTest()
{
auto postBody = std::make_unique<BMallocIO>();
postBody->Write(kPostText.String(), kPostText.Length());
postBody->Seek(0, SEEK_SET);
auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/post"));
request.SetMethod(BHttpMethod::Post);
request.SetRequestBody(std::move(postBody), "text/plain", kPostText.Length());
auto result = fSession.Execute(std::move(request));
CPPUNIT_ASSERT_EQUAL(kExpectedPostBody.Length(), result.Body().text.Length());
CPPUNIT_ASSERT(result.Body().text == kExpectedPostBody);
}
@@ -46,6 +46,7 @@ public:
void BasicAuthTest(); void BasicAuthTest();
void StopOnErrorTest(); void StopOnErrorTest();
void RequestCancelTest(); void RequestCancelTest();
void PostTest();
static void AddTests(BTestSuite& suite); static void AddTests(BTestSuite& suite);