NetServices: Add support for an input body in a request and handle this with redirects.
Change-Id: Id2399d49aa673469c8c04ebd13884cdbcb24112d
This commit is contained in:
@@ -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
|
||||
\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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -341,6 +386,12 @@ namespace Network {
|
||||
<td> How many redirections should be followed. Set to 0 to disable. </td>
|
||||
<td> Defaults to 8 redirections per request </td>
|
||||
</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>
|
||||
<td> \ref StopOnError() </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
|
||||
\brief Is the request set to parse the full response on error.
|
||||
@@ -593,6 +655,8 @@ namespace Network {
|
||||
* \c Accept
|
||||
* \c Accept-Encoding
|
||||
* \c Connection
|
||||
* \c Content-Type
|
||||
* \c Content-Length
|
||||
|
||||
\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)
|
||||
\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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
\ingroup netservices
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#define _B_HTTP_REQUEST_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
@@ -82,6 +83,14 @@ struct BHttpAuthentication {
|
||||
|
||||
class BHttpRequest {
|
||||
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
|
||||
BHttpRequest();
|
||||
BHttpRequest(const BUrl& url);
|
||||
@@ -99,6 +108,7 @@ public:
|
||||
const BHttpFields& Fields() const noexcept;
|
||||
uint8 MaxRedirections() const noexcept;
|
||||
const BHttpMethod& Method() const noexcept;
|
||||
const Body* RequestBody() const noexcept;
|
||||
bool StopOnError() const noexcept;
|
||||
bigtime_t Timeout() const noexcept;
|
||||
const BUrl& Url() const noexcept;
|
||||
@@ -108,10 +118,16 @@ public:
|
||||
void SetFields(const BHttpFields& fields);
|
||||
void SetMaxRedirections(uint8 maxRedirections);
|
||||
void SetMethod(const BHttpMethod& method);
|
||||
void SetRequestBody(std::unique_ptr<BDataIO> input,
|
||||
BString mimeType, std::optional<off_t> size);
|
||||
void SetStopOnError(bool stopOnError);
|
||||
void SetTimeout(bigtime_t timeout);
|
||||
void SetUrl(const BUrl& url);
|
||||
|
||||
// Clearing Options
|
||||
void ClearAuthentication() noexcept;
|
||||
std::unique_ptr<BDataIO> ClearRequestBody() noexcept;
|
||||
|
||||
// Serialization
|
||||
ssize_t SerializeHeaderTo(BDataIO* target) const;
|
||||
BString HeaderToString() const;
|
||||
@@ -119,6 +135,9 @@ public:
|
||||
private:
|
||||
friend class BHttpSession;
|
||||
struct Data;
|
||||
|
||||
bool RewindBody() noexcept;
|
||||
|
||||
std::unique_ptr<Data> fData;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#define _HTTP_STREAM_H_
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class BDataIO;
|
||||
class BMallocIO;
|
||||
@@ -29,7 +30,12 @@ public:
|
||||
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;
|
||||
|
||||
private:
|
||||
std::unique_ptr<BMallocIO> fHeader;
|
||||
BDataIO* fBody;
|
||||
off_t fTotalSize = 0;
|
||||
off_t fBodyOffset = 0;
|
||||
off_t fCurrentPos = 0;
|
||||
off_t fRemainingHeaderSize = 0;
|
||||
BDataIO* fBody = nullptr;
|
||||
off_t fTotalBodySize = 0;
|
||||
off_t fBufferedBodySize = 0;
|
||||
off_t fTransferredBodySize = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <DataIO.h>
|
||||
#include <HttpFields.h>
|
||||
#include <MimeType.h>
|
||||
#include <NetServicesDefs.h>
|
||||
#include <Url.h>
|
||||
|
||||
@@ -166,6 +167,7 @@ struct BHttpRequest::Data {
|
||||
std::optional<BHttpAuthentication> authentication;
|
||||
bool stopOnError = false;
|
||||
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
|
||||
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,
|
||||
"Accept"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
|
||||
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
|
||||
_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()));
|
||||
}
|
||||
|
||||
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) {
|
||||
bytesWritten += _write_to_dataio(target, field.RawField());
|
||||
bytesWritten += _write_to_dataio(target, "\r\n"sv);
|
||||
@@ -460,3 +528,21 @@ BHttpRequest::HeaderToString() const
|
||||
|
||||
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;
|
||||
|
||||
// Redirection
|
||||
BHttpStatus fRedirectStatus;
|
||||
std::optional<BHttpStatus> fRedirectStatus;
|
||||
int8 fRemainingRedirects;
|
||||
|
||||
// Optional decompression
|
||||
@@ -652,7 +652,7 @@ BHttpSession::Request::Request(Request& original, const BHttpSession::Redirect&
|
||||
if (redirect.redirectToGet
|
||||
&& (fRequest.Method() != BHttpMethod::Head && fRequest.Method() != BHttpMethod::Get)) {
|
||||
fRequest.SetMethod(BHttpMethod::Get);
|
||||
// TODO: clear Post fields/Update Data when that is supported.
|
||||
fRequest.ClearRequestBody();
|
||||
}
|
||||
|
||||
fRemainingRedirects = original.fRemainingRedirects--;
|
||||
@@ -809,18 +809,33 @@ BHttpSession::Request::ReceiveResult()
|
||||
if (status.code != 0) {
|
||||
// the status headers are now received, decide what to do next
|
||||
|
||||
// Handle redirects
|
||||
if (status.StatusClass() == BHttpStatusClass::Redirection && fRemainingRedirects > 0) {
|
||||
fRedirectStatus = std::move(status);
|
||||
} else {
|
||||
// Register NoContent before moving the status to the result
|
||||
if (status.StatusCode() == BHttpStatusCode::NoContent)
|
||||
fNoContent = true;
|
||||
|
||||
fResult->SetStatus(std::move(status));
|
||||
// TODO: inform listeners of receiving the status code
|
||||
// Determine if we can handle redirects; else notify of receiving status
|
||||
if (fRemainingRedirects > 0) {
|
||||
switch (status.StatusCode()) {
|
||||
case BHttpStatusCode::MovedPermanently:
|
||||
case BHttpStatusCode::TemporaryRedirect:
|
||||
case BHttpStatusCode::PermanentRedirect:
|
||||
// These redirects require the request body to be sent again. It this is
|
||||
// possible, BHttpRequest::RewindBody() will return true in which case we can
|
||||
// handle the redirect.
|
||||
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
|
||||
|| status.StatusClass() == BHttpStatusClass::ServerError)
|
||||
&& fRequest.StopOnError())
|
||||
@@ -832,7 +847,10 @@ BHttpSession::Request::ReceiveResult()
|
||||
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;
|
||||
} else {
|
||||
@@ -863,9 +881,9 @@ BHttpSession::Request::ReceiveResult()
|
||||
// The headers have been received, now set up the rest of the response handling
|
||||
|
||||
// Handle redirects
|
||||
if (fRedirectStatus.StatusClass() == BHttpStatusClass::Redirection) {
|
||||
if (fRedirectStatus) {
|
||||
auto redirectToGet = false;
|
||||
switch (fRedirectStatus.StatusCode()) {
|
||||
switch (fRedirectStatus->StatusCode()) {
|
||||
case BHttpStatusCode::Found:
|
||||
case BHttpStatusCode::SeeOther:
|
||||
// 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::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");
|
||||
if (locationField == fFields.end()) {
|
||||
throw BNetworkRequestError(__PRETTY_FUNCTION__,
|
||||
|
||||
@@ -8,19 +8,122 @@
|
||||
|
||||
#include <HttpStream.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <DataIO.h>
|
||||
#include <HttpRequest.h>
|
||||
|
||||
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)
|
||||
: fHeader(std::make_unique<BMallocIO>()), fBody(nullptr)
|
||||
: fBody(nullptr)
|
||||
{
|
||||
// Serialize the header of the request to text
|
||||
fTotalSize = request.SerializeHeaderTo(fHeader.get());
|
||||
fBodyOffset = fTotalSize;
|
||||
// TODO: add size of request body to total size
|
||||
ByteIOHelper helper(fBuffer);
|
||||
fRemainingHeaderSize = request.SerializeHeaderTo(&helper);
|
||||
|
||||
// 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::Transfer(BDataIO* target)
|
||||
{
|
||||
if (fCurrentPos == fTotalSize)
|
||||
return TransferInfo{0, fTotalSize, fTotalSize, true};
|
||||
if (fBuffer.size() == 0 && fTotalBodySize == fBufferedBodySize) {
|
||||
// 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) {
|
||||
// Writing the header
|
||||
auto remainingSize = fBodyOffset - fCurrentPos;
|
||||
bytesWritten = target->Write(
|
||||
static_cast<const char*>(fHeader->Buffer()) + fCurrentPos, remainingSize);
|
||||
if (bytesWritten == B_WOULD_BLOCK)
|
||||
return TransferInfo{0, 0, fTotalSize, false};
|
||||
else if (bytesWritten < 0)
|
||||
throw BSystemError("BDataIO::Write()", bytesWritten);
|
||||
|
||||
fCurrentPos += bytesWritten;
|
||||
|
||||
if (bytesWritten < remainingSize) {
|
||||
return TransferInfo{bytesWritten, fCurrentPos, fTotalSize, false};
|
||||
if (bufferedSize == B_WOULD_BLOCK) {
|
||||
// do nothing; try again next round
|
||||
} else if (bufferedSize == 0) {
|
||||
// no remaining data; throw error
|
||||
throw BRuntimeError(__PRETTY_FUNCTION__,
|
||||
"No more data in request input body while more data is expected");
|
||||
} else if (bufferedSize < 0) {
|
||||
throw BSystemError(__PRETTY_FUNCTION__, bufferedSize);
|
||||
} else {
|
||||
// update counters
|
||||
fBufferedBodySize += bufferedSize;
|
||||
if (fBufferedBodySize == fTotalBodySize) {
|
||||
// no more body to load
|
||||
fBody = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the body
|
||||
if (fBody) {
|
||||
// TODO
|
||||
throw BRuntimeError(__PRETTY_FUNCTION__, "Not implemented");
|
||||
if (fBuffer.size() == 0) {
|
||||
// nothing this round
|
||||
return TransferInfo{0, fTransferredBodySize, fTotalBodySize, false};
|
||||
}
|
||||
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);
|
||||
RequestStreamTestIO testIO(kExpectedStreamText.data());
|
||||
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) {
|
||||
auto [currentBytesWritten, totalBytesWritten, totalSize, complete]
|
||||
= requestStream.Transfer(&testIO);
|
||||
CPPUNIT_ASSERT_EQUAL(expectedBytesWritten, currentBytesWritten);
|
||||
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);
|
||||
}
|
||||
finished = complete;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +476,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent)
|
||||
testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest);
|
||||
testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest);
|
||||
testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest);
|
||||
testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest);
|
||||
|
||||
suite.addTest(testCaller);
|
||||
parent.addTest("HttpIntegrationTest", &suite);
|
||||
@@ -520,6 +500,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent)
|
||||
testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest);
|
||||
testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest);
|
||||
testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest);
|
||||
testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest);
|
||||
|
||||
suite.addTest(testCaller);
|
||||
parent.addTest("HttpsIntegrationTest", &suite);
|
||||
@@ -735,3 +716,63 @@ HttpIntegrationTest::RequestCancelTest()
|
||||
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 StopOnErrorTest();
|
||||
void RequestCancelTest();
|
||||
void PostTest();
|
||||
|
||||
static void AddTests(BTestSuite& suite);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user