Author: brane
Date: Thu Jan  1 18:45:34 2026
New Revision: 1931047

Log:
Added error callback infrastructure.

This adds three levels and four kinds of callbacks for reporting errors
from Serf: global, context-specific and (incoming or outgoing) connection
-specific. Request and response code will use their their connection's
callback, but add extra flags to indicate the source of the error message.

The SSL code in ssl_buckets.c uses an error context that callers can (or
rather "will be able to") define so that error messages get sent to
the appropriate, caller-specific callback. This part is not yet implemented
because it requires revising some of our SSL APIs.

* CMakeLists.txt: Check if <unistd.h> is available, used by tests.
  (SOURCES): Add the error_callbacks.c file.
* SConstruct: Check for <unistd.h>, as above.

* serf.h: Add public error callback prototypes and constants.
   Too many of them to list here individually.

* serf_bucket_types.h
  (serf_ssl_error_cb_set, serf_ssl_error_cb_t): Removed, obsolete.

* serf_private.h: Add private helpers for sending error messages to
   callbacks and the ssl_context infrastructure for handling errors.
  (serf_context_t): Add error_callback and error_callback_baton.
  (serf_incoming_t): Likewise.
  (serf_connection_t): Here, too.

* buckets/ssl_buckets.c:
   Update all calls to the removed ssl-specific error callback to use
   the new dispatch_ssl_error.
  (serf_ssl_context_t) Remove error_callback and error_baton.
  (global_error_ctx): New fallback error context, sends to global callback.
  (dispatch_ssl_error): New, calls an error context's dispatcher.
  (log_ssl_error): Use dispatch_ssl_error().

* src/error_callbacks.c: New file. Implements all the private helpers
   declared in serf_private.h, and also:
  (serf_global_error_callback_set): Implement here.

* src/context.c
  (serf_context_error_callback_set): Implement here.

* src/incoming.c
  (serf_incoming_error_callback_set): Implement here.

* src/outgoing.c
  (serf_connection_error_callback_set): Implement here.

* test/test_util.c
  (isatty): Import from headers if available, otherwise fake it.
  (test_error_callback): New, an error callback for the test suite.
  (setup_test_context): Register test_error_callback in verbose mode.

Added:
   serf/trunk/src/error_callbacks.c
Modified:
   serf/trunk/CMakeLists.txt
   serf/trunk/SConstruct
   serf/trunk/buckets/ssl_buckets.c
   serf/trunk/serf.h
   serf/trunk/serf_bucket_types.h
   serf/trunk/serf_private.h
   serf/trunk/src/context.c
   serf/trunk/src/incoming.c
   serf/trunk/src/outgoing.c
   serf/trunk/test/test_util.c

Modified: serf/trunk/CMakeLists.txt
==============================================================================
--- serf/trunk/CMakeLists.txt   Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/CMakeLists.txt   Thu Jan  1 18:45:34 2026        (r1931047)
@@ -137,6 +137,7 @@ list(APPEND EXPORTS_BLACKLIST
 list(APPEND SOURCES
     "src/config_store.c"
     "src/context.c"
+    "src/error_callbacks.c"
     "src/deprecated.c"
     "src/incoming.c"
     "src/inet_pton.c"
@@ -393,6 +394,7 @@ CheckFunction("SSL_library_init" "" "SER
               "openssl/ssl.h" "${OPENSSL_INCLUDE_DIR}"
               ${OPENSSL_LIBRARIES} ${SERF_STANDARD_LIBRARIES})
 CheckHeader("stdbool.h" "HAVE_STDBOOL_H=1")
+CheckHeader("unistd.h" "HAVE_UNISTD_H=1")
 CheckType("OSSL_HANDSHAKE_STATE" "openssl/ssl.h" 
"SERF_HAVE_OSSL_HANDSHAKE_STATE" ${OPENSSL_INCLUDE_DIR})
 if(Brotli_FOUND)
   CheckType("BrotliDecoderResult" "brotli/decode.h" 
"SERF_HAVE_BROTLI_DECODER_RESULT" ${BROTLI_INCLUDE_DIR})

Modified: serf/trunk/SConstruct
==============================================================================
--- serf/trunk/SConstruct       Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/SConstruct       Thu Jan  1 18:45:34 2026        (r1931047)
@@ -727,6 +727,8 @@ if CALLOUT_OKAY:
   ### some configuration stuffs
   if conf.CheckCHeader('stdbool.h'):
     env.Append(CPPDEFINES=['HAVE_STDBOOL_H'])
+  if conf.CheckCHeader('unistd.h'):
+    env.Append(CPPDEFINES=['HAVE_UNISTD_H'])
 
   env = conf.Finish()
 

Modified: serf/trunk/buckets/ssl_buckets.c
==============================================================================
--- serf/trunk/buckets/ssl_buckets.c    Thu Jan  1 17:33:48 2026        
(r1931046)
+++ serf/trunk/buckets/ssl_buckets.c    Thu Jan  1 18:45:34 2026        
(r1931047)
@@ -206,10 +206,6 @@ struct serf_ssl_context_t {
     X509 *cached_cert;
     EVP_PKEY *cached_cert_pw;
 
-    /* Error callback */
-    serf_ssl_error_cb_t error_callback;
-    void *error_baton;
-
     apr_status_t pending_err;
 
     /* Status of a fatal error, returned on subsequent encrypt or decrypt
@@ -229,6 +225,31 @@ struct serf_ssl_context_t {
     serf_config_t *config;
 };
 
+/* The fallback SSL error context which logs to the global error callback. */
+static const serf__ssl_error_ctx_t global_error_ctx = {
+    serf__global_ssl_error,     /* dispatcher */
+    NULL                        /* baton */
+};
+
+static apr_status_t dispatch_ssl_error(const serf__ssl_error_ctx_t *err_ctx,
+                                       apr_status_t status,
+                                       const char *message)
+{
+    return err_ctx->dispatch(err_ctx->baton, status, message);
+}
+
+static void log_ssl_error(const serf__ssl_error_ctx_t *err_ctx,
+                          apr_status_t status)
+{
+    unsigned long err;
+
+    while ((err = ERR_get_error())) {
+        char ebuf[256];
+        ERR_error_string_n(err, ebuf, sizeof(ebuf));
+        dispatch_ssl_error(err_ctx, status, ebuf);
+    }
+}
+
 typedef struct ssl_context_t {
     /* The bucket-independent ssl context that this bucket is associated with 
*/
     serf_ssl_context_t *ssl_ctx;
@@ -355,21 +376,6 @@ detect_renegotiate(const SSL *s, int whe
     }
 }
 
-static void log_ssl_error(serf_ssl_context_t *ctx)
-{
-    unsigned long err;
-
-    while ((err = ERR_get_error())) {
-
-        if (err && ctx->error_callback) {
-            char ebuf[256];
-            ERR_error_string_n(err, ebuf, sizeof(ebuf));
-            ctx->error_callback(ctx->error_baton, ctx->fatal_err, ebuf);
-        }
-
-    }
-}
-
 static void bio_set_data(BIO *bio, void *data)
 {
 #ifndef SERF_NO_SSL_BIO_WRAPPERS
@@ -1096,7 +1102,7 @@ static apr_status_t status_from_ssl_erro
                     ctx->fatal_err = SERF_ERROR_SSL_COMM_FAILED;
 
                 status = ctx->fatal_err;
-                log_ssl_error(ctx);
+                log_ssl_error(&global_error_ctx, status);
             }
             break;
 
@@ -1110,7 +1116,7 @@ static apr_status_t status_from_ssl_erro
 
         default:
             status = ctx->fatal_err = SERF_ERROR_SSL_COMM_FAILED;
-            log_ssl_error(ctx);
+            log_ssl_error(&global_error_ctx, status);
             break;
     }
 
@@ -1217,7 +1223,7 @@ static apr_status_t ssl_decrypt(void *ba
         } else {
             /* A fatal error occurred. */
             ctx->fatal_err = status = SERF_ERROR_SSL_COMM_FAILED;
-            log_ssl_error(ctx);
+            log_ssl_error(&global_error_ctx, status);
         }
     } else {
         *len = ssl_len;
@@ -1675,14 +1681,12 @@ static int ssl_need_client_cert(SSL *ssl
 
         store = OSSL_STORE_open(cert_uri, ui_method, ctx, NULL, NULL);
         if (!store) {
+            char ebuf[1024];
+            ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
+            apr_snprintf(ebuf, sizeof(ebuf), "could not open URI: %s", 
cert_uri);
+            dispatch_ssl_error(&global_error_ctx, ctx->fatal_err, ebuf);
 
-            if (ctx->error_callback) {
-                char ebuf[1024];
-                ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
-                apr_snprintf(ebuf, sizeof(ebuf), "could not open URI: %s", 
cert_uri);
-                ctx->error_callback(ctx->error_baton, ctx->fatal_err, ebuf);
-            }
-
+            log_ssl_error(&global_error_ctx, ctx->fatal_err);
             break;
         }
 
@@ -1697,14 +1701,12 @@ static int ssl_need_client_cert(SSL *ssl
             }
 
             if (!info) {
+                char ebuf[1024];
+                ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
+                apr_snprintf(ebuf, sizeof(ebuf), "could not read URI: %s", 
cert_uri);
+                dispatch_ssl_error(&global_error_ctx, ctx->fatal_err, ebuf);
 
-                if (ctx->error_callback) {
-                    char ebuf[1024];
-                    ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
-                    apr_snprintf(ebuf, sizeof(ebuf), "could not read URI: %s", 
cert_uri);
-                    ctx->error_callback(ctx->error_baton, ctx->fatal_err, 
ebuf);
-                }
-
+                log_ssl_error(&global_error_ctx, ctx->fatal_err);
                 break;
             }
 
@@ -1824,8 +1826,7 @@ static int ssl_need_client_cert(SSL *ssl
     UI_destroy_method(ui_method);
 
     if (ERR_peek_error()) {
-        log_ssl_error(ctx);
-
+        log_ssl_error(&global_error_ctx, ctx->fatal_err);
         return -1;
     }
 
@@ -1888,13 +1889,13 @@ static int ssl_need_client_cert(SSL *ssl
                                ctx->pool);
 
         if (status) {
-            if (ctx->error_callback) {
-                char ebuf[1024];
-                apr_snprintf(ebuf, sizeof(ebuf), "could not open PKCS12: %s", 
cert_path);
-                ctx->error_callback(ctx->error_baton, ctx->fatal_err, ebuf);
-                apr_strerror(status, ebuf, sizeof(ebuf));
-                ctx->error_callback(ctx->error_baton, ctx->fatal_err, ebuf);
-            }
+            char ebuf[1024];
+            apr_snprintf(ebuf, sizeof(ebuf), "could not open PKCS12: %s", 
cert_path);
+            dispatch_ssl_error(&global_error_ctx, status, ebuf);
+            apr_strerror(status, ebuf, sizeof(ebuf));
+            dispatch_ssl_error(&global_error_ctx, status, ebuf);
+
+            ctx->fatal_err = status;
             return -1;
         }
 
@@ -1926,10 +1927,12 @@ static int ssl_need_client_cert(SSL *ssl
             return 1;
         }
         else {
+            char ebuf[1024];
             unsigned long err = ERR_get_error();
             ERR_clear_error();
             if (ERR_GET_LIB(err) == ERR_LIB_PKCS12 &&
-                ERR_GET_REASON(err) == PKCS12_R_MAC_VERIFY_FAILURE) {
+                ERR_GET_REASON(err) == PKCS12_R_MAC_VERIFY_FAILURE)
+            {
                 if (ctx->cert_pw_callback) {
                     const char *password;
 
@@ -1973,15 +1976,11 @@ static int ssl_need_client_cert(SSL *ssl
                             return 1;
                         }
                         else {
+                            ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
+                            apr_snprintf(ebuf, sizeof(ebuf), "could not parse 
PKCS12: %s", cert_path);
+                            dispatch_ssl_error(&global_error_ctx, 
ctx->fatal_err, ebuf);
 
-                            if (ctx->error_callback) {
-                                char ebuf[1024];
-                                ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
-                                apr_snprintf(ebuf, sizeof(ebuf), "could not 
parse PKCS12: %s", cert_path);
-                                ctx->error_callback(ctx->error_baton, 
ctx->fatal_err, ebuf);
-                            }
-
-                            log_ssl_error(ctx);
+                            log_ssl_error(&global_error_ctx, ctx->fatal_err);
                             return -1;
                         }
                     }
@@ -1989,28 +1988,21 @@ static int ssl_need_client_cert(SSL *ssl
                 PKCS12_free(p12);
                 bio_meth_free(biom);
 
-                if (ctx->error_callback) {
-                    char ebuf[1024];
-                    ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
-                    apr_snprintf(ebuf, sizeof(ebuf), "PKCS12 needs a password: 
%s", cert_path);
-                    ctx->error_callback(ctx->error_baton, ctx->fatal_err, 
ebuf);
-                }
+                ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
+                apr_snprintf(ebuf, sizeof(ebuf), "PKCS12 needs a password: 
%s", cert_path);
+                dispatch_ssl_error(&global_error_ctx, ctx->fatal_err, ebuf);
 
-                log_ssl_error(ctx);
+                log_ssl_error(&global_error_ctx, ctx->fatal_err);
                 return -1;
             }
             else {
                 PKCS12_free(p12);
                 bio_meth_free(biom);
 
-                if (ctx->error_callback) {
-                    char ebuf[1024];
-                    ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
-                    apr_snprintf(ebuf, sizeof(ebuf), "could not parse PKCS12: 
%s", cert_path);
-                    ctx->error_callback(ctx->error_baton, ctx->fatal_err, 
ebuf);
-                }
+                ctx->fatal_err = SERF_ERROR_SSL_CERT_FAILED;
+                dispatch_ssl_error(&global_error_ctx, ctx->fatal_err, ebuf);
 
-                log_ssl_error(ctx);
+                log_ssl_error(&global_error_ctx, ctx->fatal_err);
                 return -1;
             }
         }
@@ -2088,15 +2080,6 @@ void serf_ssl_server_cert_chain_callback
     context->server_cert_userdata = data;
 }
 
-void serf_ssl_error_cb_set(
-    serf_ssl_context_t *context,
-    serf_ssl_error_cb_t callback,
-    void *baton)
-{
-    context->error_callback = callback;
-    context->error_baton = baton;
-}
-
 static int ssl_new_session(SSL *ssl, SSL_SESSION *session)
 {
     serf_ssl_context_t *ctx = SSL_get_app_data(ssl);
@@ -2162,9 +2145,6 @@ static serf_ssl_context_t *ssl_init_cont
     ssl_ctx->protocol_callback = NULL;
     ssl_ctx->protocol_userdata = NULL;
 
-    ssl_ctx->error_callback = NULL;
-    ssl_ctx->error_baton = NULL;
-
     SSL_CTX_set_verify(ssl_ctx->ctx, SSL_VERIFY_PEER,
                        validate_server_certificate);
     SSL_CTX_set_options(ssl_ctx->ctx, SSL_OP_ALL);
@@ -2410,14 +2390,10 @@ apr_status_t serf_ssl_load_cert_file(
 
         return APR_SUCCESS;
     }
-#if 0
-    else {
-        /* If we'd have had a serf context *, we could have used serf logging 
*/
-        ERR_print_errors_fp(stderr);
-    }
-#endif
 
-    return SERF_ERROR_SSL_CERT_FAILED;
+    status = SERF_ERROR_SSL_CERT_FAILED;
+    log_ssl_error(&global_error_ctx, status);
+    return status;
 }
 
 
@@ -2479,7 +2455,7 @@ apr_status_t serf_ssl_add_crl_from_file(
     result = X509_STORE_add_crl(store, crl);
     if (!result) {
         ssl_ctx->fatal_err = status = SERF_ERROR_SSL_CERT_FAILED;
-        log_ssl_error(ssl_ctx);
+        log_ssl_error(&global_error_ctx, status);
         return status;
     }
 

Modified: serf/trunk/serf.h
==============================================================================
--- serf/trunk/serf.h   Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/serf.h   Thu Jan  1 18:45:34 2026        (r1931047)
@@ -185,6 +185,113 @@ typedef struct serf_config_t serf_config
  */
 const char *serf_error_string(apr_status_t errcode);
 
+/**
+ * The source of an error callback invocation.
+ *
+ * @since New in 1.5.
+ */
+/* Bit masks for error sources. */
+#define SERF_ERROR_CB_MASK        0x00ff
+#define SERF_ERROR_CB_GLOBAL      0x0001
+#define SERF_ERROR_CB_CONTEXT     0x0002
+#define SERF_ERROR_CB_OUTGOING    0x0004
+#define SERF_ERROR_CB_INCOMING    0x0008
+#define SERF_ERROR_CB_REQUEST     0x0010
+#define SERF_ERROR_CB_RESPONSE    0x0020
+
+/* The following flag can be bitwise-combined with any of the above
+   values to indicate that the message originated an SSL context. */
+#define SERF_ERROR_CB_SSL_CONTEXT 0x0100
+
+/**
+ * A callback that, when set, will be called for out-of-band error reporting.
+ *
+ * A callback can be set on any of three levels: globally, for the context, or
+ * the outgoing or incoming connection. Incoming and outgoing requests and
+ * responses do not have their own error handlers, but indicate with the source
+ * flags where the message originated. The default implementations will will
+ * send messages up this hierarchy until a custom callback is found, or the
+ * default global callback drops the message to the floor. This is the error
+ * callback hierarchy:
+ * ```
+ *     Level                    Registration function
+ *
+ *     Global                   serf_error_callback_set()
+ *       Context                serf_context_error_callback_set()
+ *         Connection           serf_connection_error_callback_set()
+ *         Incoming             serf_incoming_error_callback_set()
+ * ```
+ * In addition, any of those handlers can be called from within an SSL
+ * processing context, which is indicated by the flag on the message source.
+ *
+ * The @a baton is the object provided to the callback registration, and
+ * @a source is one of the @c SERF_ERROR_CB_* values, above.
+ *
+ * The @a message lasts only as long as the callback invocation. The caller
+ * must make a copy of the message it it wants to keep it for longer.
+ *
+ * It is possible that for a given error multiple strings will be returned
+ * in multiple callbacks. The caller may choose to handle all strings, or
+ * may choose to ignore all strings but the last most detailed one.
+ *
+ * @since New in 1.5.
+ */
+typedef apr_status_t (*serf_error_cb_t)(
+    void *baton,
+    unsigned source,
+    apr_status_t status,
+    const char *message);
+
+/**
+ * Register the global error @a callback, replacing any previous version.
+ *
+ * @note This function is NOT thread-safe, and calls to the callback are not
+ *       serialized. Users are responsible for making the registration and
+ *       the callback implementation safe for their application.
+ *
+ * @since New in 1.5.
+ */
+void serf_global_error_callback_set(
+    serf_error_cb_t callback,
+    void *baton);
+
+/**
+ * Register the context-specific error callback.
+ *
+ * Like serf_error_callback_set() except that it affects the given
+ * context @a ctx and, since contexts may not be accessed from multiple
+ * threads, serialization is not a concern.
+ *
+ * @since New in 1.5.
+ */
+void serf_context_error_callback_set(
+    serf_context_t *ctx,
+    serf_error_cb_t callback,
+    void *baton);
+
+/**
+ * Register the connection-specific error callback.
+ *
+ * Like serf_context_error_callback_set() but for connections.
+ *
+ * @since New in 1.5.
+ */
+void serf_connection_error_callback_set(
+    serf_connection_t *conn,
+    serf_error_cb_t callback,
+    void *baton);
+
+/**
+ * Register the incoming-connection-specific error callback.
+ *
+ * Like serf_context_error_callback_set() but for incoming connections.
+ *
+ * @since New in 1.5.
+ */
+void serf_incoming_error_callback_set(
+    serf_incoming_t *client,
+    serf_error_cb_t callback,
+    void *baton);
 
 /**
  * Create a new context for serf operations.

Modified: serf/trunk/serf_bucket_types.h
==============================================================================
--- serf/trunk/serf_bucket_types.h      Thu Jan  1 17:33:48 2026        
(r1931046)
+++ serf/trunk/serf_bucket_types.h      Thu Jan  1 18:45:34 2026        
(r1931047)
@@ -687,33 +687,6 @@ void serf_ssl_server_cert_chain_callback
     void *data);
 
 /**
- * Callback type for detailed TLS error strings. This callback will be fired
- * every time the underlying crypto library encounters an error. The message
- * lasts only as long as the callback, if the caller wants to set aside the
- * message for later use, a copy must be made.
- *
- * It is possible that for a given error multiple strings will be returned
- * in multiple callbacks. The caller may choose to handle all strings, or
- * may choose to ignore all strings but the last most detailed one.
- */
-typedef apr_status_t (*serf_ssl_error_cb_t)(
-    void *baton,
-    apr_status_t status,
-    const char *message);
-
-/**
- * Set a callback to return any detailed certificate error from the underlying
- * cryptographic library.
- *
- * The callback is associated with the context, however the choice of baton
- * will depend on the needs of the caller.
- */
-void serf_ssl_error_cb_set(
-    serf_ssl_context_t *context,
-    serf_ssl_error_cb_t callback,
-    void *baton);
-
-/**
  * Use the default root CA certificates as included with the OpenSSL library.
  */
 apr_status_t serf_ssl_use_default_certificates(

Modified: serf/trunk/serf_private.h
==============================================================================
--- serf/trunk/serf_private.h   Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/serf_private.h   Thu Jan  1 18:45:34 2026        (r1931047)
@@ -125,6 +125,75 @@ typedef int serf__bool_t; /* Not _Bool *
         (result) = integer_;                             \
     } while(0)
 
+/*** Error callback invocation ***/
+
+/* NOTE: There is no serf__global_error() because the global handler
+         should not be called directly but only as a fallback. */
+
+apr_status_t serf__context_error(const serf_context_t* ctx,
+                                 apr_status_t status,
+                                 const char *message);
+apr_status_t serf__connection_error(const serf_connection_t *conn,
+                                    apr_status_t status,
+                                    const char *message);
+apr_status_t serf__request_error(const serf_request_t *req,
+                                 apr_status_t status,
+                                 const char *message);
+apr_status_t serf__response_error(const serf_request_t *req,
+                                  apr_status_t status,
+                                  const char *message);
+apr_status_t serf__incoming_error(const serf_incoming_t *client,
+                                  apr_status_t status,
+                                  const char *message);
+apr_status_t serf__incoming_request_error(const serf_incoming_request_t *req,
+                                          apr_status_t status,
+                                          const char *message);
+apr_status_t serf__incoming_response_error(const serf_incoming_request_t *req,
+                                           apr_status_t status,
+                                           const char *message);
+
+/* The SSL context is a special case sonce it doesn't directly
+   belong to any context or connection. The ssl context implementation
+   calls serf__ssl_context_error() with an serf__ssl_error_ctx_t provided
+   by the caller of the ssl_context function. This is a bit of a pretzel,
+   but the alternative is to only send errors from the SSL context to the
+   global error context, which is less than ideal. */
+
+typedef struct serf__ssl_error_ctx_t serf__ssl_error_ctx_t;
+struct serf__ssl_error_ctx_t
+{
+    apr_status_t (*dispatch)(const void *baton,
+                             apr_status_t status,
+                             const char *message);
+    void *baton;
+};
+
+/* Error dispatchers for the SSL error context. */
+apr_status_t serf__global_ssl_error(const void *baton,
+                                    apr_status_t status,
+                                    const char *message);
+apr_status_t serf__context_ssl_error(const void *baton,
+                                     apr_status_t status,
+                                     const char *message);
+apr_status_t serf__connection_ssl_error(const void *baton,
+                                        apr_status_t status,
+                                        const char *message);
+apr_status_t serf__request_ssl_error(const void *baton,
+                                     apr_status_t status,
+                                     const char *message);
+apr_status_t serf__response_ssl_error(const void *baton,
+                                      apr_status_t status,
+                                      const char *message);
+apr_status_t serf__incoming_ssl_error(const void *baton,
+                                      apr_status_t status,
+                                      const char *message);
+apr_status_t serf__incoming_request_ssl_error(const void *baton,
+                                              apr_status_t status,
+                                              const char *message);
+apr_status_t serf__incoming_response_ssl_error(const void *baton,
+                                               apr_status_t status,
+                                               const char *message);
+
 /*** Logging facilities ***/
 
 /* Check for the SERF_DISABLE_LOGGING define, as set by scons. */
@@ -491,6 +560,10 @@ struct serf_context_t {
     void *volatile resolve_head;
     apr_status_t resolve_init_status;
     void *resolve_context;
+
+    /* Error callback */
+    serf_error_cb_t error_callback;
+    void *error_callback_baton;
 };
 
 struct serf_listener_t {
@@ -545,6 +618,10 @@ struct serf_incoming_t {
     serf_bucket_t *proto_peek_bkt;
 
     serf_incoming_request_t *current_request; /* For HTTP/1 */
+
+    /* Error callback */
+    serf_error_cb_t error_callback;
+    void *error_callback_baton;
 };
 
 /* States for the different stages in the lifecycle of a connection. */
@@ -666,6 +743,10 @@ struct serf_connection_t {
 
     /* Configuration shared with buckets and authn plugins */
     serf_config_t *config;
+
+    /* Error callback */
+    serf_error_cb_t error_callback;
+    void *error_callback_baton;
 };
 
 /* Called by requests that still have outstanding requests to allow cleaning

Modified: serf/trunk/src/context.c
==============================================================================
--- serf/trunk/src/context.c    Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/src/context.c    Thu Jan  1 18:45:34 2026        (r1931047)
@@ -218,6 +218,16 @@ serf_context_t *serf_context_create(apr_
     return serf_context_create_ex(NULL, NULL, NULL, pool);
 }
 
+
+void serf_context_error_callback_set(serf_context_t *ctx,
+                                     serf_error_cb_t callback,
+                                     void *baton)
+{
+    ctx->error_callback_baton = baton;
+    ctx->error_callback = callback;
+}
+
+
 apr_status_t serf_context_prerun(serf_context_t *ctx)
 {
     apr_status_t status;

Added: serf/trunk/src/error_callbacks.c
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ serf/trunk/src/error_callbacks.c    Thu Jan  1 18:45:34 2026        
(r1931047)
@@ -0,0 +1,263 @@
+/* ====================================================================
+ *    Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ * ====================================================================
+ */
+
+#include "serf.h"
+#include "serf_private.h"
+
+/* Global error processing. */
+
+static apr_status_t
+default_global_error_callback(void *baton,
+                              unsigned source,
+                              apr_status_t status,
+                              const char *message)
+{
+    return APR_SUCCESS;
+}
+
+static void *global_error_callback_baton = NULL;
+static serf_error_cb_t global_error_callback = default_global_error_callback;
+
+void serf_global_error_callback_set(serf_error_cb_t callback, void *baton)
+{
+    global_error_callback_baton = baton;
+    global_error_callback = callback;
+}
+
+/* The various process_*_error functions set the error source, then either
+   call the appropriate error callback or, if that's not defined, send the
+   message up the hierarchy until it's either handled by a registered
+   callback or thrown away by default_global_error_callback().
+
+   This way, the callers of serf__*_error() don't have to bother with
+   the error source or check if their callbacks have been defined. */
+
+static apr_status_t process_global_error(unsigned source,
+                                         apr_status_t status,
+                                         const char *message)
+{
+    if (0 == (source & SERF_ERROR_CB_MASK)) {
+        source |= SERF_ERROR_CB_GLOBAL;
+    }
+    return global_error_callback(global_error_callback_baton,
+                                 source, status, message);
+}
+
+/* Context error processing. */
+
+static apr_status_t process_context_error(unsigned source,
+                                          const serf_context_t* ctx,
+                                          apr_status_t status,
+                                          const char *message)
+{
+    if (0 == (source & SERF_ERROR_CB_MASK)) {
+        source |= SERF_ERROR_CB_CONTEXT;
+    }
+    if (ctx->error_callback) {
+        return ctx->error_callback(ctx->error_callback_baton,
+                                   source, status, message);
+    }
+    return process_global_error(source, status, message);
+}
+
+apr_status_t serf__context_error(const serf_context_t* ctx,
+                                 apr_status_t status,
+                                 const char *message)
+{
+    return process_context_error(SERF_ERROR_CB_CONTEXT,
+                                 ctx, status, message);
+}
+
+/* Connection error processing. */
+
+static apr_status_t process_connection_error(unsigned source,
+                                             const serf_connection_t *conn,
+                                             apr_status_t status,
+                                             const char *message)
+{
+    if (0 == (source & SERF_ERROR_CB_MASK)) {
+        source |= SERF_ERROR_CB_OUTGOING;
+    }
+    if (conn->error_callback) {
+        return conn->error_callback(conn->error_callback_baton,
+                                    source, status, message);
+    }
+    return process_context_error(source, conn->ctx, status, message);
+}
+
+apr_status_t serf__connection_error(const serf_connection_t *conn,
+                                    apr_status_t status,
+                                    const char *message)
+{
+    return process_connection_error(SERF_ERROR_CB_OUTGOING,
+                                    conn, status, message);
+}
+
+apr_status_t serf__request_error(const serf_request_t *req,
+                                 apr_status_t status,
+                                 const char *message)
+{
+    return process_connection_error(SERF_ERROR_CB_OUTGOING
+                                    | SERF_ERROR_CB_REQUEST,
+                                    req->conn, status, message);
+}
+
+apr_status_t serf__response_error(const serf_request_t *req,
+                                  apr_status_t status,
+                                  const char *message)
+{
+    return process_connection_error(SERF_ERROR_CB_OUTGOING
+                                    | SERF_ERROR_CB_RESPONSE,
+                                    req->conn, status, message);
+}
+
+/* Incoming error processing. */
+
+static apr_status_t process_incoming_error(unsigned source,
+                                           const serf_incoming_t *client,
+                                           apr_status_t status,
+                                           const char *message)
+{
+    if (0 == (source & SERF_ERROR_CB_MASK)) {
+        source |= SERF_ERROR_CB_INCOMING;
+    }
+    if (client->error_callback) {
+        return client->error_callback(client->error_callback_baton,
+                                      source, status, message);
+    }
+    return process_context_error(source, client->ctx, status, message);
+}
+
+apr_status_t serf__incoming_error(const serf_incoming_t *client,
+                                  apr_status_t status,
+                                  const char *message)
+{
+    return process_incoming_error(SERF_ERROR_CB_INCOMING,
+                                  client, status, message);
+}
+
+apr_status_t serf__incoming_request_error(const serf_incoming_request_t *req,
+                                          apr_status_t status,
+                                          const char *message)
+{
+    return process_incoming_error(SERF_ERROR_CB_INCOMING
+                                  | SERF_ERROR_CB_REQUEST,
+                                  req->incoming, status, message);
+}
+
+apr_status_t serf__incoming_response_error(const serf_incoming_request_t *req,
+                                           apr_status_t status,
+                                           const char *message)
+{
+    return process_incoming_error(SERF_ERROR_CB_INCOMING
+                                  | SERF_ERROR_CB_RESPONSE,
+                                  req->incoming, status, message);
+}
+
+/* Errors from the SSL context.
+
+   Callers of the SSL functions can create an serf__ssl_error_ctx_t
+   on the stack and pass it to the called function, which can then
+   dispatch any errors it generates to the appropriate callback. */
+
+apr_status_t serf__global_ssl_error(const void *baton,
+                                    apr_status_t status,
+                                    const char *message)
+{
+    /* Ignores the baton, since there's only one global error callback. */
+    return process_global_error(SERF_ERROR_CB_SSL_CONTEXT
+                                | SERF_ERROR_CB_GLOBAL,
+                                status, message);
+}
+
+apr_status_t serf__context_ssl_error(const void *baton,
+                                     apr_status_t status,
+                                     const char *message)
+{
+    const serf_context_t *const ctx = baton;
+    return process_context_error(SERF_ERROR_CB_SSL_CONTEXT
+                                 | SERF_ERROR_CB_CONTEXT,
+                                 ctx, status, message);
+}
+
+
+apr_status_t serf__connection_ssl_error(const void *baton,
+                                        apr_status_t status,
+                                        const char *message)
+{
+    const serf_connection_t *const conn = baton;
+    return process_connection_error(SERF_ERROR_CB_SSL_CONTEXT
+                                    | SERF_ERROR_CB_OUTGOING,
+                                    conn, status, message);
+}
+
+apr_status_t serf__request_ssl_error(const void *baton,
+                                     apr_status_t status,
+                                     const char *message)
+{
+    const serf_request_t *const req = baton;
+    return process_connection_error(SERF_ERROR_CB_SSL_CONTEXT
+                                    | SERF_ERROR_CB_OUTGOING
+                                    | SERF_ERROR_CB_REQUEST,
+                                    req->conn, status, message);
+}
+
+apr_status_t serf__response_ssl_error(const void *baton,
+                                      apr_status_t status,
+                                      const char *message)
+{
+    const serf_request_t *const req = baton;
+    return process_connection_error(SERF_ERROR_CB_SSL_CONTEXT
+                                    | SERF_ERROR_CB_OUTGOING
+                                    | SERF_ERROR_CB_RESPONSE,
+                                    req->conn, status, message);
+}
+
+apr_status_t serf__incoming_ssl_error(const void *baton,
+                                      apr_status_t status,
+                                      const char *message)
+{
+    const serf_incoming_t *const client = baton;
+    return process_incoming_error(SERF_ERROR_CB_SSL_CONTEXT
+                                  | SERF_ERROR_CB_INCOMING,
+                                  client, status, message);
+}
+
+apr_status_t serf__incoming_request_ssl_error(const void *baton,
+                                              apr_status_t status,
+                                              const char *message)
+{
+    const serf_incoming_request_t *const req = baton;
+    return process_incoming_error(SERF_ERROR_CB_SSL_CONTEXT
+                                  | SERF_ERROR_CB_INCOMING
+                                  | SERF_ERROR_CB_REQUEST,
+                                  req->incoming, status, message);
+}
+
+apr_status_t serf__incoming_response_ssl_error(const void *baton,
+                                               apr_status_t status,
+                                               const char *message)
+{
+    const serf_incoming_request_t *const req = baton;
+    return process_incoming_error(SERF_ERROR_CB_SSL_CONTEXT
+                                  | SERF_ERROR_CB_INCOMING
+                                  | SERF_ERROR_CB_RESPONSE,
+                                  req->incoming, status, message);
+}

Modified: serf/trunk/src/incoming.c
==============================================================================
--- serf/trunk/src/incoming.c   Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/src/incoming.c   Thu Jan  1 18:45:34 2026        (r1931047)
@@ -707,6 +707,14 @@ apr_status_t serf_listener_create(
     return APR_SUCCESS;
 }
 
+void serf_incoming_error_callback_set(serf_incoming_t *client,
+                                      serf_error_cb_t callback,
+                                      void *baton)
+{
+    client->error_callback_baton = baton;
+    client->error_callback = callback;
+}
+
 apr_status_t serf__incoming_update_pollset(serf_incoming_t *client)
 {
     serf_context_t *ctx = client->ctx;

Modified: serf/trunk/src/outgoing.c
==============================================================================
--- serf/trunk/src/outgoing.c   Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/src/outgoing.c   Thu Jan  1 18:45:34 2026        (r1931047)
@@ -1452,6 +1452,15 @@ apr_status_t serf_connection_create_asyn
 }
 
 
+void serf_connection_error_callback_set(serf_connection_t *conn,
+                                        serf_error_cb_t callback,
+                                        void *baton)
+{
+    conn->error_callback_baton = baton;
+    conn->error_callback = callback;
+}
+
+
 apr_status_t serf_connection_reset(
     serf_connection_t *conn)
 {

Modified: serf/trunk/test/test_util.c
==============================================================================
--- serf/trunk/test/test_util.c Thu Jan  1 17:33:48 2026        (r1931046)
+++ serf/trunk/test/test_util.c Thu Jan  1 18:45:34 2026        (r1931047)
@@ -27,6 +27,15 @@
 
 #include <stdlib.h>
 
+#ifdef WIN32
+#include <io.h>
+#define isatty _isatty
+#elif HAVE_UNISTD_H
+#include <unistd.h>
+#else
+#define isatty(x) 0
+#endif
+
 #include "serf.h"
 
 #include "test_serf.h"
@@ -464,6 +473,28 @@ apr_status_t dummy_authn_callback(char *
 /* Test utility functions, to be used with the MockHTTPinC framework         */
 /*****************************************************************************/
 
+static apr_status_t test_error_callback(void *baton,
+                                        unsigned source,
+                                        apr_status_t status,
+                                        const char *message)
+{
+    /* We can has nice colours? Use ANSI escape codes on terminals. */
+    const char *const ERROR = (isatty(fileno(stderr))
+                                ? "\033[1;31m" "ERROR" "\033[0;m"
+                                : "ERROR");
+
+    fprintf(stderr, "%s: <%d> %c%c%c%c%c %s\n", ERROR, status,
+            ((source & SERF_ERROR_CB_SSL_CONTEXT) ? '*' : '-'),
+            ((source & SERF_ERROR_CB_GLOBAL) ? 'g' : '-'),
+            ((source & SERF_ERROR_CB_CONTEXT) ? 'c' : '-'),
+            ((source & SERF_ERROR_CB_OUTGOING) ? 'o'
+             : ((source & SERF_ERROR_CB_INCOMING) ? 'i' : '-')),
+            ((source & SERF_ERROR_CB_REQUEST) ? 'q'
+             : ((source & SERF_ERROR_CB_RESPONSE) ? 'p' : '-')),
+            message);
+    return APR_SUCCESS;
+}
+
 apr_status_t
 setup_test_context(test_baton_t *tb, apr_pool_t *pool)
 {
@@ -474,6 +505,7 @@ setup_test_context(test_baton_t *tb, apr
         tb->context = serf_context_create(pool);
 
         if (TEST_VERBOSE) {
+            serf_global_error_callback_set(test_error_callback, NULL);
             status = serf_logging_create_stream_output(&output, tb->context,
                                                        SERF_LOG_DEBUG,
                                                        SERF_LOGCOMP_ALL,


Reply via email to