Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions include/cpp_request/client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#pragma once

#include <chrono>
#include <string_view>

#include <cpp_request/request.hpp>
#include <cpp_request/response.hpp>
#include <cpp_request/result.hpp>

namespace cpp_request {

class Client final {
public:
Client() noexcept = default;

[[nodiscard]] Result<Response> request(const Request& request);

[[nodiscard]] Result<Response> get(std::string_view url);
[[nodiscard]] Result<Response> head(std::string_view url);
[[nodiscard]] Result<Response> post(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> put(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> patch(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> del(std::string_view url);

void set_connect_timeout(std::chrono::milliseconds timeout) noexcept {
connect_timeout_ = timeout;
}

void set_read_timeout(std::chrono::milliseconds timeout) noexcept {
read_timeout_ = timeout;
}

void set_write_timeout(std::chrono::milliseconds timeout) noexcept {
write_timeout_ = timeout;
}

private:
std::chrono::milliseconds connect_timeout_{5000};
std::chrono::milliseconds read_timeout_{30000};
std::chrono::milliseconds write_timeout_{30000};
};

[[nodiscard]] Result<Response> get(std::string_view url);
[[nodiscard]] Result<Response> head(std::string_view url);
[[nodiscard]] Result<Response> post(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> put(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> patch(
std::string_view url,
std::string_view body = {});
[[nodiscard]] Result<Response> del(std::string_view url);

} // namespace cpp_request
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ add_library(internal
${CMAKE_CURRENT_SOURCE_DIR}/dummy.cpp
${CMAKE_CURRENT_SOURCE_DIR}/url.cpp
${CMAKE_CURRENT_SOURCE_DIR}/headers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/client.cpp
${CMAKE_CURRENT_SOURCE_DIR}/http/request_serializer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/http/chunked_decoder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/http/response_parser.cpp
Expand Down
170 changes: 170 additions & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#include <cpp_request/client.hpp>

#include "http/request_serializer.hpp"
#include "http/response_parser.hpp"
#include "net/resolver.hpp"
#include "net/tcp_connection.hpp"

#include <array>
#include <chrono>
#include <cstddef>
#include <string_view>
#include <utility>

namespace cpp_request {
namespace {

[[nodiscard]] std::chrono::milliseconds remaining_timeout(
std::chrono::steady_clock::time_point deadline) noexcept {
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return std::chrono::milliseconds{0};
}

auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - now);
if (remaining.count() <= 0) {
remaining = std::chrono::milliseconds{1};
}
return remaining;
}

[[nodiscard]] Request make_request(
Method method,
std::string_view url,
std::string_view body = {}) noexcept {
Request request{method, url};
request.set_body(body);
return request;
}

} // namespace

Result<Response> Client::request(const Request& request_value) {
auto serialized_result = detail::http::serialize_request(request_value);
if (!serialized_result) {
return serialized_result.error();
}
auto serialized = std::move(serialized_result).value();

auto endpoints_result = detail::net::Resolver::resolve(
serialized.url.host(),
serialized.url.port());
if (!endpoints_result) {
return endpoints_result.error();
}

auto connection_result = detail::net::TcpConnection::connect(
endpoints_result.value(),
connect_timeout_);
if (!connection_result) {
return connection_result.error();
}
auto connection = std::move(connection_result).value();

const auto write_deadline = std::chrono::steady_clock::now() + write_timeout_;

auto head_write = connection.write_all(
serialized.head,
remaining_timeout(write_deadline));
if (!head_write) {
return head_write.error();
}

if (!serialized.body.empty()) {
auto body_write = connection.write_all(
serialized.body,
remaining_timeout(write_deadline));
if (!body_write) {
return body_write.error();
}
}

detail::http::ResponseParser parser{request_value.method()};
std::array<char, 16 * 1024> read_buffer{};

while (!parser.complete()) {
auto read_result = connection.read_some(
read_buffer.data(),
read_buffer.size(),
read_timeout_);

if (!read_result) {
if (read_result.error().code == ErrorCode::ConnectionClosed) {
auto eof_result = parser.finish_eof();
Comment on lines +93 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate connection resets instead of treating them as EOF

When a close-delimited response is interrupted by ECONNRESET/WSAECONNRESET, TcpConnection::read_some() reports ConnectionClosed with a nonzero native error, so this branch calls finish_eof() and returns the partially received body as a successful response. Only an orderly recv() == 0 closure is valid EOF; reset-style closures must remain errors to avoid silently accepting truncated response data.

Useful? React with 👍 / 👎.

if (!eof_result) {
return eof_result.error();
}
if (parser.complete()) {
break;
}
}
return read_result.error();
}

auto parse_result = parser.feed(std::string_view{
read_buffer.data(),
read_result.value()});
if (!parse_result) {
return parse_result.error();
}
}

return parser.take_response();
}

Result<Response> Client::get(std::string_view url) {
return request(make_request(Method::Get, url));
}

Result<Response> Client::head(std::string_view url) {
return request(make_request(Method::Head, url));
}

Result<Response> Client::post(std::string_view url, std::string_view body) {
return request(make_request(Method::Post, url, body));
}

Result<Response> Client::put(std::string_view url, std::string_view body) {
return request(make_request(Method::Put, url, body));
}

Result<Response> Client::patch(std::string_view url, std::string_view body) {
return request(make_request(Method::Patch, url, body));
}

Result<Response> Client::del(std::string_view url) {
return request(make_request(Method::Delete, url));
}

Result<Response> get(std::string_view url) {
Client client;
return client.get(url);
}

Result<Response> head(std::string_view url) {
Client client;
return client.head(url);
}

Result<Response> post(std::string_view url, std::string_view body) {
Client client;
return client.post(url, body);
}

Result<Response> put(std::string_view url, std::string_view body) {
Client client;
return client.put(url, body);
}

Result<Response> patch(std::string_view url, std::string_view body) {
Client client;
return client.patch(url, body);
}

Result<Response> del(std::string_view url) {
Client client;
return client.del(url);
}

} // namespace cpp_request
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ add_executable(cpp_request_tests
${CMAKE_CURRENT_SOURCE_DIR}/chunked_decoder_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/response_parser_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/response_upgrade_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/client_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/native_socket_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/resolver_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tcp_connection_test.cpp
Expand Down
Loading
Loading