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
|
\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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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__,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user