Skip to content

Fix #1416 #2169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 5, 2025
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,49 @@ cli.enable_server_hostname_verification(false);
> [!NOTE]
> When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.

### SSL Error Handling

When SSL operations fail, cpp-httplib provides detailed error information through two separate error fields:

```c++
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "path/to/httplib.h"

httplib::Client cli("https://example.com");

auto res = cli.Get("/");
if (!res) {
// Check the error type
auto err = res.error();

switch (err) {
case httplib::Error::SSLConnection:
std::cout << "SSL connection failed, SSL error: "
<< res->ssl_error() << std::endl;
break;

case httplib::Error::SSLLoadingCerts:
std::cout << "SSL cert loading failed, OpenSSL error: "
<< std::hex << res->ssl_openssl_error() << std::endl;
break;

case httplib::Error::SSLServerVerification:
std::cout << "SSL verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
break;

case httplib::Error::SSLServerHostnameVerification:
std::cout << "SSL hostname verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
break;

default:
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
}
}
}
```

Server
------

Expand Down
65 changes: 59 additions & 6 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,17 @@ class Result {
Headers &&request_headers = Headers{})
: res_(std::move(res)), err_(err),
request_headers_(std::move(request_headers)) {}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
int ssl_error)
: res_(std::move(res)), err_(err),
request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {}
Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
int ssl_error, unsigned long ssl_openssl_error)
: res_(std::move(res)), err_(err),
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
ssl_openssl_error_(ssl_openssl_error) {}
#endif
// Response
operator bool() const { return res_ != nullptr; }
bool operator==(std::nullptr_t) const { return res_ == nullptr; }
Expand All @@ -1260,6 +1271,13 @@ class Result {
// Error
Error error() const { return err_; }

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
// SSL Error
int ssl_error() const { return ssl_error_; }
// OpenSSL Error
unsigned long ssl_openssl_error() const { return ssl_openssl_error_; }
#endif

// Request Headers
bool has_request_header(const std::string &key) const;
std::string get_request_header_value(const std::string &key,
Expand All @@ -1273,6 +1291,10 @@ class Result {
std::unique_ptr<Response> res_;
Error err_ = Error::Unknown;
Headers request_headers_;
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int ssl_error_ = 0;
unsigned long ssl_openssl_error_ = 0;
#endif
};

class ClientImpl {
Expand Down Expand Up @@ -1570,6 +1592,11 @@ class ClientImpl {

Logger logger_;

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int last_ssl_error_ = 0;
unsigned long last_openssl_error_ = 0;
#endif

private:
bool send_(Request &req, Response &res, Error &error);
Result send_(Request &&req);
Expand Down Expand Up @@ -1840,6 +1867,9 @@ class SSLServer : public Server {

SSL_CTX *ctx_;
std::mutex ctx_mutex_;
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int last_ssl_error_ = 0;
#endif
};

class SSLClient final : public ClientImpl {
Expand Down Expand Up @@ -8173,7 +8203,12 @@ inline Result ClientImpl::send_(Request &&req) {
auto res = detail::make_unique<Response>();
auto error = Error::Success;
auto ret = send(req, *res, error);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
last_ssl_error_, last_openssl_error_};
#else
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
#endif
}

inline bool ClientImpl::handle_request(Stream &strm, Request &req,
Expand Down Expand Up @@ -8723,7 +8758,12 @@ inline Result ClientImpl::send_with_content_provider(
req, body, content_length, std::move(content_provider),
std::move(content_provider_without_length), content_type, error);

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
last_openssl_error_};
#else
return Result{std::move(res), error, std::move(req.headers)};
#endif
}

inline std::string
Expand Down Expand Up @@ -9790,8 +9830,8 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
template <typename U>
bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl,
U ssl_connect_or_accept,
time_t timeout_sec,
time_t timeout_usec) {
time_t timeout_sec, time_t timeout_usec,
int *ssl_error) {
auto res = 0;
while ((res = ssl_connect_or_accept(ssl)) != 1) {
auto err = SSL_get_error(ssl, res);
Expand All @@ -9804,6 +9844,7 @@ bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl,
break;
default: break;
}
if (ssl_error) { *ssl_error = err; }
return false;
}
return true;
Expand Down Expand Up @@ -9897,9 +9938,10 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
if (ret >= 0) { return ret; }
err = SSL_get_error(ssl_, ret);
} else {
return -1;
break;
}
}
assert(ret < 0);
}
return ret;
} else {
Expand Down Expand Up @@ -9929,9 +9971,10 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
if (ret >= 0) { return ret; }
err = SSL_get_error(ssl_, ret);
} else {
return -1;
break;
}
}
assert(ret < 0);
}
return ret;
}
Expand Down Expand Up @@ -9982,6 +10025,7 @@ inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) !=
1 ||
SSL_CTX_check_private_key(ctx_) != 1) {
last_ssl_error_ = static_cast<int>(ERR_get_error());
SSL_CTX_free(ctx_);
ctx_ = nullptr;
} else if (client_ca_cert_file_path || client_ca_cert_dir_path) {
Expand Down Expand Up @@ -10055,7 +10099,8 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
sock, ctx_, ctx_mutex_,
[&](SSL *ssl2) {
return detail::ssl_connect_or_accept_nonblocking(
sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_);
sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_,
&last_ssl_error_);
},
[](SSL * /*ssl2*/) { return true; });

Expand Down Expand Up @@ -10123,6 +10168,7 @@ inline SSLClient::SSLClient(const std::string &host, int port,
SSL_FILETYPE_PEM) != 1 ||
SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(),
SSL_FILETYPE_PEM) != 1) {
last_openssl_error_ = ERR_get_error();
SSL_CTX_free(ctx_);
ctx_ = nullptr;
}
Expand All @@ -10149,6 +10195,7 @@ inline SSLClient::SSLClient(const std::string &host, int port,

if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 ||
SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) {
last_openssl_error_ = ERR_get_error();
SSL_CTX_free(ctx_);
ctx_ = nullptr;
}
Expand Down Expand Up @@ -10292,11 +10339,13 @@ inline bool SSLClient::load_certs() {
if (!ca_cert_file_path_.empty()) {
if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(),
nullptr)) {
last_openssl_error_ = ERR_get_error();
ret = false;
}
} else if (!ca_cert_dir_path_.empty()) {
if (!SSL_CTX_load_verify_locations(ctx_, nullptr,
ca_cert_dir_path_.c_str())) {
last_openssl_error_ = ERR_get_error();
ret = false;
}
} else {
Expand Down Expand Up @@ -10329,7 +10378,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {

if (!detail::ssl_connect_or_accept_nonblocking(
socket.sock, ssl2, SSL_connect, connection_timeout_sec_,
connection_timeout_usec_)) {
connection_timeout_usec_, &last_ssl_error_)) {
error = Error::SSLConnection;
return false;
}
Expand All @@ -10342,6 +10391,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
}

if (verification_status == SSLVerifierResponse::CertificateRejected) {
last_openssl_error_ = ERR_get_error();
error = Error::SSLServerVerification;
return false;
}
Expand All @@ -10350,6 +10400,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
verify_result_ = SSL_get_verify_result(ssl2);

if (verify_result_ != X509_V_OK) {
last_openssl_error_ = static_cast<unsigned long>(verify_result_);
error = Error::SSLServerVerification;
return false;
}
Expand All @@ -10358,12 +10409,14 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
auto se = detail::scope_exit([&] { X509_free(server_cert); });

if (server_cert == nullptr) {
last_openssl_error_ = ERR_get_error();
error = Error::SSLServerVerification;
return false;
}

if (server_hostname_verification_) {
if (!verify_host(server_cert)) {
last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH;
error = Error::SSLServerHostnameVerification;
return false;
}
Expand Down
82 changes: 74 additions & 8 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7492,6 +7492,47 @@ TEST(SSLClientTest, ServerNameIndication_Online) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}

TEST(SSLClientTest, ServerCertificateVerificationError_Online) {
// Use a site that will cause SSL verification failure due to self-signed cert
SSLClient cli("self-signed.badssl.com", 443);
cli.enable_server_certificate_verification(true);
auto res = cli.Get("/");

ASSERT_TRUE(!res);
EXPECT_EQ(Error::SSLServerVerification, res.error());

// For SSL server verification errors, ssl_error should be 0, only
// ssl_openssl_error should be set
EXPECT_EQ(0, res.ssl_error());

// Verify OpenSSL error is captured for SSLServerVerification
// This occurs when SSL_get_verify_result() returns a verification failure
EXPECT_EQ(static_cast<unsigned long>(X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT),
res.ssl_openssl_error());
}

TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
// Use a site where hostname doesn't match the certificate
// badssl.com provides wrong.host.badssl.com which has cert for *.badssl.com
SSLClient cli("wrong.host.badssl.com", 443);
cli.enable_server_certificate_verification(true);
cli.enable_server_hostname_verification(true);

auto res = cli.Get("/");
ASSERT_TRUE(!res);

EXPECT_EQ(Error::SSLServerHostnameVerification, res.error());

// For SSL hostname verification errors, ssl_error should be 0, only
// ssl_openssl_error should be set
EXPECT_EQ(0, res.ssl_error());

// Verify OpenSSL error is captured for SSLServerHostnameVerification
// This occurs when verify_host() fails due to hostname mismatch
EXPECT_EQ(static_cast<unsigned long>(X509_V_ERR_HOSTNAME_MISMATCH),
res.ssl_openssl_error());
}

TEST(SSLClientTest, ServerCertificateVerification1_Online) {
Client cli("https://google.com");
auto res = cli.Get("/");
Expand All @@ -7501,19 +7542,33 @@ TEST(SSLClientTest, ServerCertificateVerification1_Online) {

TEST(SSLClientTest, ServerCertificateVerification2_Online) {
SSLClient cli("google.com");
cli.enable_server_certificate_verification(true);
cli.set_ca_cert_path("hello");
cli.set_ca_cert_path(CA_CERT_FILE);
auto res = cli.Get("/");
ASSERT_TRUE(!res);
EXPECT_EQ(Error::SSLLoadingCerts, res.error());
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
}

TEST(SSLClientTest, ServerCertificateVerification3_Online) {
SSLClient cli("google.com");
cli.set_ca_cert_path(CA_CERT_FILE);
cli.enable_server_certificate_verification(true);
cli.set_ca_cert_path("hello");

auto res = cli.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
ASSERT_TRUE(!res);
EXPECT_EQ(Error::SSLLoadingCerts, res.error());

// For SSL_CTX operations, ssl_error should be 0, only ssl_openssl_error
// should be set
EXPECT_EQ(0, res.ssl_error());

// Verify OpenSSL error is captured for SSLLoadingCerts
// This error occurs when SSL_CTX_load_verify_locations() fails
// > openssl errstr 0x80000002
// error:80000002:system library::No such file or directory
// > openssl errstr 0xA000126
// error:0A000126:SSL routines::unexpected eof while reading
EXPECT_TRUE(res.ssl_openssl_error() == 0x80000002 ||
res.ssl_openssl_error() == 0xA000126);
}

TEST(SSLClientTest, ServerCertificateVerification4) {
Expand Down Expand Up @@ -7790,10 +7845,20 @@ TEST(SSLClientServerTest, ClientCertMissing) {
svr.wait_until_ready();

SSLClient cli(HOST, PORT);
auto res = cli.Get("/test");
cli.set_connection_timeout(30);

auto res = cli.Get("/test");
ASSERT_TRUE(!res);
EXPECT_EQ(Error::SSLServerVerification, res.error());

// For SSL server verification errors, ssl_error should be 0, only
// ssl_openssl_error should be set
EXPECT_EQ(0, res.ssl_error());

// Verify OpenSSL error is captured for SSLServerVerification
// Note: This test may have different error codes depending on the exact
// verification failure
EXPECT_NE(0UL, res.ssl_openssl_error());
}

TEST(SSLClientServerTest, TrustDirOptional) {
Expand Down Expand Up @@ -7868,6 +7933,7 @@ TEST(SSLClientServerTest, SSLConnectTimeout) {
auto res = cli.Get("/test");
ASSERT_TRUE(!res);
EXPECT_EQ(Error::SSLConnection, res.error());
EXPECT_EQ(SSL_ERROR_WANT_READ, res.ssl_error());
}

TEST(SSLClientServerTest, CustomizeServerSSLCtx) {
Expand Down
Loading