diff --git a/docs/user/netservices/HttpRequest.dox b/docs/user/netservices/HttpRequest.dox index 049145c9d1..d506e7cd14 100644 --- a/docs/user/netservices/HttpRequest.dox +++ b/docs/user/netservices/HttpRequest.dox @@ -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 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 BHttpRequest::Body::size + \brief The size of the content, if known. + + \since Haiku R1 +*/ + + +/*! + \var std::optional 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 { How many redirections should be followed. Set to 0 to disable. Defaults to 8 redirections per request + + \ref RequestBody() + \ref SetRequestBody() + Body contents that is sent with the request. + Defaults to an empty body + \ref StopOnError() \ref SetStopOnError() @@ -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 input, BString mimeType, + std::optional 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 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 */ diff --git a/docs/user/netservices/HttpStream.dox b/docs/user/netservices/HttpStream.dox index 4b5da9c643..438267d8ce 100644 --- a/docs/user/netservices/HttpStream.dox +++ b/docs/user/netservices/HttpStream.dox @@ -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 BAbstractDataStream::fBuffer + \brief Internal buffer that can be used by implementations to buffer data. +*/ + + /*! \class BHttpRequestStream \ingroup netservices diff --git a/headers/private/netservices2/HttpRequest.h b/headers/private/netservices2/HttpRequest.h index 15b0517ad6..978848147f 100644 --- a/headers/private/netservices2/HttpRequest.h +++ b/headers/private/netservices2/HttpRequest.h @@ -7,6 +7,7 @@ #define _B_HTTP_REQUEST_H_ #include +#include #include #include @@ -82,6 +83,14 @@ struct BHttpAuthentication { class BHttpRequest { public: + // Aggregate parameter types + struct Body { + std::unique_ptr input; + BString mimeType; + std::optional size; + std::optional 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 input, + BString mimeType, std::optional size); void SetStopOnError(bool stopOnError); void SetTimeout(bigtime_t timeout); void SetUrl(const BUrl& url); + // Clearing Options + void ClearAuthentication() noexcept; + std::unique_ptr 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 fData; }; diff --git a/headers/private/netservices2/HttpStream.h b/headers/private/netservices2/HttpStream.h index 59d6632c2a..1732572950 100644 --- a/headers/private/netservices2/HttpStream.h +++ b/headers/private/netservices2/HttpStream.h @@ -7,6 +7,7 @@ #define _HTTP_STREAM_H_ #include +#include 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 fBuffer; }; @@ -41,11 +47,11 @@ public: virtual TransferInfo Transfer(BDataIO* target) override; private: - std::unique_ptr 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; }; diff --git a/src/kits/network/libnetservices2/HttpRequest.cpp b/src/kits/network/libnetservices2/HttpRequest.cpp index 016851b8f8..131330113e 100644 --- a/src/kits/network/libnetservices2/HttpRequest.cpp +++ b/src/kits/network/libnetservices2/HttpRequest.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -166,6 +167,7 @@ struct BHttpRequest::Data { std::optional authentication; bool stopOnError = false; bigtime_t timeout = B_INFINITE_TIMEOUT; + std::optional 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 fReservedOptionalFieldNames = { +static constexpr std::array 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 input, BString mimeType, + std::optional 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(); + 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(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 +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(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(fData->requestBody->input.get()); + return *fData->requestBody->startPosition + == inputData->Seek(*fData->requestBody->startPosition, SEEK_SET); + } + return true; +} \ No newline at end of file diff --git a/src/kits/network/libnetservices2/HttpSession.cpp b/src/kits/network/libnetservices2/HttpSession.cpp index fd2df64b05..cb69e907f0 100644 --- a/src/kits/network/libnetservices2/HttpSession.cpp +++ b/src/kits/network/libnetservices2/HttpSession.cpp @@ -134,7 +134,7 @@ private: bool fNoContent = false; // Redirection - BHttpStatus fRedirectStatus; + std::optional 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__, diff --git a/src/kits/network/libnetservices2/HttpStream.cpp b/src/kits/network/libnetservices2/HttpStream.cpp index b4e10d08c5..d7f306800d 100644 --- a/src/kits/network/libnetservices2/HttpStream.cpp +++ b/src/kits/network/libnetservices2/HttpStream.cpp @@ -8,19 +8,122 @@ #include +#include + #include #include 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& 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& fBuffer; +}; + + +ByteIOHelper::ByteIOHelper(std::vector& 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(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()), 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(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(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}; } diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp index 62af4e0758..c16711d184 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp @@ -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) \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(); + 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); +} diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.h b/src/tests/kits/net/netservices2/HttpProtocolTest.h index d49c53dd72..71ec8e0842 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.h +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.h @@ -46,6 +46,7 @@ public: void BasicAuthTest(); void StopOnErrorTest(); void RequestCancelTest(); + void PostTest(); static void AddTests(BTestSuite& suite);