Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-awscrt for openSUSE:Factory 
checked in at 2026-05-27 16:15:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-awscrt (Old)
 and      /work/SRC/openSUSE:Factory/.python-awscrt.new.1937 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-awscrt"

Wed May 27 16:15:00 2026 rev:7 rq:1355228 version:0.33.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-awscrt/python-awscrt.changes      
2026-04-29 19:19:44.609750636 +0200
+++ /work/SRC/openSUSE:Factory/.python-awscrt.new.1937/python-awscrt.changes    
2026-05-27 16:15:49.786137050 +0200
@@ -1,0 +2,10 @@
+Tue May 26 13:28:48 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to version 0.33.0
+  * builder -> v0.9.92 and clang-latest by @sbSteveK in (#733)
+  * Fix free threading http connection race by @TingDaoK in (#738)
+  * Error safely if header values are invalid by @azkrishpy in (#739)
+  * Enforce the depth limit for cbor decoder by @TingDaoK in (#736)
+  * Latest submodules by @TingDaoK in (#740)
+
+-------------------------------------------------------------------

Old:
----
  awscrt-0.32.2.tar.gz

New:
----
  awscrt-0.33.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-awscrt.spec ++++++
--- /var/tmp/diff_new_pack.RSrT2i/_old  2026-05-27 16:15:52.186235632 +0200
+++ /var/tmp/diff_new_pack.RSrT2i/_new  2026-05-27 16:15:52.202236290 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-awscrt
-Version:        0.32.2
+Version:        0.33.0
 Release:        0
 Summary:        A common runtime for AWS Python projects
 License:        Apache-2.0

++++++ awscrt-0.32.2.tar.gz -> awscrt-0.33.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/.github/workflows/ci.yml 
new/aws-crt-python-0.33.0/.github/workflows/ci.yml
--- old/aws-crt-python-0.32.2/.github/workflows/ci.yml  2026-04-24 
22:49:53.000000000 +0200
+++ new/aws-crt-python-0.33.0/.github/workflows/ci.yml  2026-05-22 
19:12:54.000000000 +0200
@@ -7,7 +7,7 @@
       - 'docs'
 
 env:
-  BUILDER_VERSION: v0.9.87
+  BUILDER_VERSION: v0.9.92
   BUILDER_SOURCE: releases
   BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net
   PACKAGE_NAME: aws-crt-python
@@ -209,6 +209,7 @@
           - clang-9
           - clang-10
           - clang-11
+          - clang-latest
           - gcc-5
           - gcc-6
           - gcc-7
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/source/cbor.c 
new/aws-crt-python-0.33.0/source/cbor.c
--- old/aws-crt-python-0.32.2/source/cbor.c     2026-04-24 22:49:53.000000000 
+0200
+++ new/aws-crt-python-0.33.0/source/cbor.c     2026-05-22 19:12:54.000000000 
+0200
@@ -6,6 +6,9 @@
 
 #include <aws/common/cbor.h>
 
+/* Maximum nesting depth for recursive CBOR decoding to prevent stack 
exhaustion */
+static const size_t s_cbor_max_decode_depth = 128;
+
 
/*******************************************************************************
  * ENCODE
  
******************************************************************************/
@@ -185,30 +188,49 @@
     /**
      * TODO: timestamp <-> datetime?? Decimal fraction <-> decimal??
      */
-    if (PyLong_CheckExact(py_object)) {
-        return s_cbor_encoder_write_pylong(encoder, py_object);
-    } else if (PyFloat_CheckExact(py_object)) {
-        return s_cbor_encoder_write_pyobject_as_float(encoder, py_object);
-    } else if (PyBool_Check(py_object)) {
-        return s_cbor_encoder_write_pyobject_as_bool(encoder, py_object);
-    } else if (PyBytes_CheckExact(py_object)) {
-        return s_cbor_encoder_write_pyobject_as_bytes(encoder, py_object);
-    } else if (PyUnicode_Check(py_object)) {
+
+    /* Handle None first as it's a singleton, not a type */
+    if (py_object == Py_None) {
+        aws_cbor_encoder_write_null(encoder);
+        Py_RETURN_NONE;
+    }
+
+    /* Get type once for efficiency - PyObject_Type returns a new reference */
+    /* https://docs.python.org/3/c-api/structures.html#c.Py_TYPE is not a 
stable API until 3.14, so that we cannot use
+     * it. */
+    PyObject *type = PyObject_Type(py_object);
+    if (!type) {
+        return NULL;
+    }
+
+    PyObject *result = NULL;
+
+    /* Exact type matches first (no subclasses) */
+    if (type == (PyObject *)&PyLong_Type) {
+        result = s_cbor_encoder_write_pylong(encoder, py_object);
+    } else if (type == (PyObject *)&PyFloat_Type) {
+        result = s_cbor_encoder_write_pyobject_as_float(encoder, py_object);
+    } else if (type == (PyObject *)&PyBool_Type) {
+        result = s_cbor_encoder_write_pyobject_as_bool(encoder, py_object);
+    } else if (type == (PyObject *)&PyBytes_Type) {
+        result = s_cbor_encoder_write_pyobject_as_bytes(encoder, py_object);
+    } else if (PyType_IsSubtype((PyTypeObject *)type, &PyUnicode_Type)) {
         /* Allow subclasses of `str` */
-        return s_cbor_encoder_write_pyobject_as_text(encoder, py_object);
-    } else if (PyList_Check(py_object)) {
+        result = s_cbor_encoder_write_pyobject_as_text(encoder, py_object);
+    } else if (PyType_IsSubtype((PyTypeObject *)type, &PyList_Type)) {
         /* Write py_list, allow subclasses of `list` */
-        return s_cbor_encoder_write_pylist(encoder, py_object);
-    } else if (PyDict_Check(py_object)) {
+        result = s_cbor_encoder_write_pylist(encoder, py_object);
+    } else if (PyType_IsSubtype((PyTypeObject *)type, &PyDict_Type)) {
         /* Write py_dict, allow subclasses of `dict` */
-        return s_cbor_encoder_write_pydict(encoder, py_object);
-    } else if (py_object == Py_None) {
-        aws_cbor_encoder_write_null(encoder);
+        result = s_cbor_encoder_write_pydict(encoder, py_object);
     } else {
-        PyErr_Format(PyExc_ValueError, "Not supported type %R", (PyObject 
*)Py_TYPE(py_object));
+        /* Unsupported type */
+        PyErr_Format(PyExc_ValueError, "Not supported type %R", type);
     }
 
-    Py_RETURN_NONE;
+    /* Release the type reference */
+    Py_DECREF(type);
+    return result;
 }
 
 /*********************************** BINDINGS 
***********************************************/
@@ -290,6 +312,9 @@
 
     /* Encoder has simple lifetime, no async/multi-thread allowed. */
     PyObject *self_py;
+
+    /* Current recursion depth for pop_next_data_item */
+    size_t current_depth;
 };
 
 static const char *s_capsule_name_cbor_decoder = "aws_cbor_decoder";
@@ -646,62 +671,85 @@
  * Generic helper to convert a cbor encoded data to PyObject
  */
 static PyObject *s_cbor_decoder_pop_next_data_item_to_pyobject(struct 
decoder_binding *binding) {
+    if (binding->current_depth >= s_cbor_max_decode_depth) {
+        PyErr_SetString(PyExc_RecursionError, "CBOR nesting depth exceeds 
maximum allowed (128)");
+        return NULL;
+    }
+    ++binding->current_depth;
+
+    PyObject *result = NULL;
     struct aws_cbor_decoder *decoder = binding->native;
     enum aws_cbor_type out_type = AWS_CBOR_TYPE_UNKNOWN;
     if (aws_cbor_decoder_peek_type(decoder, &out_type)) {
-        return PyErr_AwsLastError();
+        result = PyErr_AwsLastError();
+        goto done;
     }
     switch (out_type) {
         case AWS_CBOR_TYPE_UINT:
-            return 
s_cbor_decoder_pop_next_unsigned_int_val_to_pyobject(decoder);
+            result = 
s_cbor_decoder_pop_next_unsigned_int_val_to_pyobject(decoder);
+            break;
         case AWS_CBOR_TYPE_NEGINT: {
             /* The value from native code is -1 - val. */
             PyObject *minus_one = PyLong_FromLong(-1);
             if (!minus_one) {
-                return NULL;
+                break;
             }
             PyObject *val = 
s_cbor_decoder_pop_next_negative_int_val_to_pyobject(decoder);
             if (!val) {
                 Py_DECREF(minus_one);
-                return NULL;
+                break;
             }
-            PyObject *ret_val = PyNumber_Subtract(minus_one, val);
+            result = PyNumber_Subtract(minus_one, val);
             Py_DECREF(minus_one);
             Py_DECREF(val);
-            return ret_val;
+            break;
         }
         case AWS_CBOR_TYPE_FLOAT:
-            return s_cbor_decoder_pop_next_float_val_to_pyobject(decoder);
+            result = s_cbor_decoder_pop_next_float_val_to_pyobject(decoder);
+            break;
         case AWS_CBOR_TYPE_BYTES:
-            return s_cbor_decoder_pop_next_bytes_val_to_pyobject(decoder);
+            result = s_cbor_decoder_pop_next_bytes_val_to_pyobject(decoder);
+            break;
         case AWS_CBOR_TYPE_TEXT:
-            return s_cbor_decoder_pop_next_text_val_to_pyobject(decoder);
+            result = s_cbor_decoder_pop_next_text_val_to_pyobject(decoder);
+            break;
         case AWS_CBOR_TYPE_BOOL:
-            return s_cbor_decoder_pop_next_boolean_val_to_pyobject(decoder);
+            result = s_cbor_decoder_pop_next_boolean_val_to_pyobject(decoder);
+            break;
         case AWS_CBOR_TYPE_NULL:
             /* fall through */
         case AWS_CBOR_TYPE_UNDEFINED:
             aws_cbor_decoder_consume_next_single_element(decoder);
-            Py_RETURN_NONE;
+            Py_INCREF(Py_None);
+            result = Py_None;
+            break;
         case AWS_CBOR_TYPE_MAP_START:
             /* fall through */
         case AWS_CBOR_TYPE_INDEF_MAP_START:
-            return s_cbor_decoder_pop_next_data_item_to_py_dict(binding);
+            result = s_cbor_decoder_pop_next_data_item_to_py_dict(binding);
+            break;
         case AWS_CBOR_TYPE_ARRAY_START:
             /* fall through */
         case AWS_CBOR_TYPE_INDEF_ARRAY_START:
-            return s_cbor_decoder_pop_next_data_item_to_py_list(binding);
+            result = s_cbor_decoder_pop_next_data_item_to_py_list(binding);
+            break;
         case AWS_CBOR_TYPE_INDEF_BYTES_START:
-            return s_cbor_decoder_pop_next_inf_bytes_to_py_bytes(decoder);
+            result = s_cbor_decoder_pop_next_inf_bytes_to_py_bytes(decoder);
+            break;
         case AWS_CBOR_TYPE_INDEF_TEXT_START:
-            return s_cbor_decoder_pop_next_inf_string_to_py_str(decoder);
+            result = s_cbor_decoder_pop_next_inf_string_to_py_str(decoder);
+            break;
         case AWS_CBOR_TYPE_TAG:
-            return s_cbor_decoder_pop_next_tag_to_pyobject(binding);
+            result = s_cbor_decoder_pop_next_tag_to_pyobject(binding);
+            break;
         default:
             aws_raise_error(AWS_ERROR_CBOR_UNEXPECTED_TYPE);
-            return PyErr_AwsLastError();
+            result = PyErr_AwsLastError();
+            break;
     }
-    return NULL;
+done:
+    --binding->current_depth;
+    return result;
 }
 
 /*********************************** BINDINGS 
***********************************************/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/source/http_connection.c 
new/aws-crt-python-0.33.0/source/http_connection.c
--- old/aws-crt-python-0.32.2/source/http_connection.c  2026-04-24 
22:49:53.000000000 +0200
+++ new/aws-crt-python-0.33.0/source/http_connection.c  2026-05-22 
19:12:54.000000000 +0200
@@ -7,6 +7,7 @@
 #include "io.h"
 
 #include <aws/common/array_list.h>
+#include <aws/common/ref_count.h>
 #include <aws/http/connection.h>
 #include <aws/http/proxy.h>
 #include <aws/http/request_response.h>
@@ -16,20 +17,22 @@
 
 /**
  * Lifetime notes:
- * - If connect() reports immediate failure, binding can be destroyed.
- * - If on_connection_setup reports failure, binding can be destroyed.
- * - Otherwise, binding cannot be destroyed until BOTH release() has been 
called AND on_connection_shutdown has fired.
+ * - Binding starts with ref_count=1 (owned by whoever will destroy on 
failure, or by the capsule on success).
+ * - If on_connection_setup succeeds, acquire another refcount. The additional 
ref is for the connection
+ *   thread to invoke callbacks (e.g. s_on_connection_shutdown) with a valid 
binding. The shutdown callback
+ *   is the last callback invoked by the connection thread, so it releases 
this ref.
+ * - The last release (ref_count 1→0) calls s_connection_destroy().
  */
 struct http_connection_binding {
     struct aws_http_connection *native;
     /* Reference to python object that reference to other related python 
object to keep it alive */
     PyObject *py_core;
 
-    bool release_called;
-    bool shutdown_called;
+    struct aws_ref_count ref_count;
 };
 
-static void s_connection_destroy(struct http_connection_binding *connection) {
+static void s_connection_destroy(void *user_data) {
+    struct http_connection_binding *connection = user_data;
     Py_XDECREF(connection->py_core);
 
     aws_mem_release(aws_py_get_allocator(), connection);
@@ -40,38 +43,23 @@
         connection, s_capsule_name_http_connection, "HttpConnectionBase", 
http_connection_binding);
 }
 
-static void s_connection_release(struct http_connection_binding *connection) {
-    AWS_FATAL_ASSERT(!connection->release_called);
-    connection->release_called = true;
-
-    bool destroy_after_release = connection->shutdown_called;
+static void s_connection_capsule_destructor(PyObject *capsule) {
+    struct http_connection_binding *connection = PyCapsule_GetPointer(capsule, 
s_capsule_name_http_connection);
 
     aws_http_connection_release(connection->native);
 
-    if (destroy_after_release) {
-        s_connection_destroy(connection);
-    }
-}
-
-static void s_connection_capsule_destructor(PyObject *capsule) {
-    struct http_connection_binding *connection = PyCapsule_GetPointer(capsule, 
s_capsule_name_http_connection);
-    s_connection_release(connection);
+    aws_ref_count_release(&connection->ref_count);
 }
 
 static void s_on_connection_shutdown(struct aws_http_connection 
*native_connection, int error_code, void *user_data) {
     (void)native_connection;
     struct http_connection_binding *connection = user_data;
-    AWS_FATAL_ASSERT(!connection->shutdown_called);
 
     PyGILState_STATE state;
     if (aws_py_gilstate_ensure(&state)) {
         return; /* Python has shut down. Nothing matters anymore, but don't 
crash */
     }
 
-    connection->shutdown_called = true;
-
-    bool destroy_after_shutdown = connection->release_called;
-
     /* Invoke on_shutdown, then clear our reference to it */
     PyObject *result = PyObject_CallMethod(connection->py_core, 
"_on_shutdown", "(i)", error_code);
 
@@ -82,9 +70,8 @@
         PyErr_WriteUnraisable(PyErr_Occurred());
     }
 
-    if (destroy_after_shutdown) {
-        s_connection_destroy(connection);
-    }
+    /* This is the last callback invoked by the connection thread. Release the 
connection-thread ref. */
+    aws_ref_count_release(&connection->ref_count);
 
     PyGILState_Release(state);
 }
@@ -107,6 +94,10 @@
     /* If setup was successful, encapsulate binding so we can pass it to 
python */
     PyObject *capsule = NULL;
     if (!error_code) {
+        /* Acquire ref for the connection thread to invoke callbacks with a 
valid binding.
+         * The shutdown callback is the last one invoked, and will release 
this ref. */
+        aws_ref_count_acquire(&connection->ref_count);
+
         capsule = PyCapsule_New(connection, s_capsule_name_http_connection, 
s_connection_capsule_destructor);
         if (!capsule) {
             error_code = AWS_ERROR_UNKNOWN;
@@ -125,13 +116,18 @@
     }
 
     if (native_connection) {
-        /* Connection exists, but failed to create capsule. Release 
connection, which eventually destroys binding */
         if (!capsule) {
-            s_connection_release(connection);
+            /* Native connection exists but capsule creation failed. We won't 
use the connection as a capsule,
+             * but the connection thread is still running. Release the initial 
ref (no capsule to own it),
+             * then release the native connection to initiate shutdown. The 
shutdown callback will release
+             * the connection-thread ref. */
+            aws_ref_count_release(&connection->ref_count);
+            aws_http_connection_release(connection->native);
         }
     } else {
-        /* Connection failed its setup, destroy binding now */
-        s_connection_destroy(connection);
+        /* Native connection failed to create. No capsule, no connection 
thread running.
+         * Release the sole ref to destroy the binding. */
+        aws_ref_count_release(&connection->ref_count);
     }
 
     Py_XDECREF(capsule);
@@ -281,6 +277,7 @@
     }
 
     struct http_connection_binding *connection = aws_mem_calloc(allocator, 1, 
sizeof(struct http_connection_binding));
+    aws_ref_count_init(&connection->ref_count, connection, 
s_connection_destroy);
     /* From hereon, we need to clean up if errors occur */
     struct aws_http2_setting *http2_settings = NULL;
     size_t http2_settings_count = 0;
@@ -388,7 +385,7 @@
         aws_mem_release(allocator, http2_settings);
     }
     if (!success) {
-        s_connection_destroy(connection);
+        aws_ref_count_release(&connection->ref_count);
         return NULL;
     }
     Py_RETURN_NONE;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/source/websocket.c 
new/aws-crt-python-0.33.0/source/websocket.c
--- old/aws-crt-python-0.32.2/source/websocket.c        2026-04-24 
22:49:53.000000000 +0200
+++ new/aws-crt-python-0.33.0/source/websocket.c        2026-05-22 
19:12:54.000000000 +0200
@@ -209,12 +209,24 @@
             PyObject *tuple_py = PyTuple_New(2);
             AWS_FATAL_ASSERT(tuple_py && "header tuple allocation failed");
 
+            /* Header names are tokens as per RFC 7230 Section 3.2 (strict 
ASCII),
+             * which means aws-c-http rejects on the wire if they contain 
non-ASCII bytes.
+             * So errors related to http header decoding will be caught at the 
protocol level.
+             * We should never fail wrangling the header name. */
             PyObject *name_py = PyUnicode_FromAwsByteCursor(&header_i->name);
             AWS_FATAL_ASSERT(name_py && "header name wrangling failed");
             PyTuple_SetItem(tuple_py, 0, name_py); /* Steals a reference */
 
+            /* Header value can contain RFC 7230 obs-text (0x80-0xFF), which is
+             * not guaranteed valid UTF-8. On decode failure, log it and drop
+             * the whole header list rather than aborting the process. */
             PyObject *value_py = PyUnicode_FromAwsByteCursor(&header_i->value);
-            AWS_FATAL_ASSERT(value_py && "header value wrangling failed");
+            if (!value_py) {
+                PyErr_WriteUnraisable(websocket_core_py);
+                Py_DECREF(tuple_py);
+                Py_CLEAR(headers_py);
+                break;
+            }
             PyTuple_SetItem(tuple_py, 1, value_py); /* Steals a reference */
 
             PyList_SetItem(headers_py, i, tuple_py); /* Steals a reference */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/aws-crt-python-0.32.2/test/test_http_connection_lifetime.py 
new/aws-crt-python-0.33.0/test/test_http_connection_lifetime.py
--- old/aws-crt-python-0.32.2/test/test_http_connection_lifetime.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/aws-crt-python-0.33.0/test/test_http_connection_lifetime.py     
2026-05-22 19:12:54.000000000 +0200
@@ -0,0 +1,118 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0.
+
+import gc
+import threading
+import unittest
+from test import NativeResourceTest
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from awscrt.io import ClientBootstrap, DefaultHostResolver, EventLoopGroup
+from awscrt.http import HttpClientConnection, HttpRequest
+import awscrt.exceptions
+
+
+class SilentHandler(SimpleHTTPRequestHandler):
+    def log_message(self, format, *args):
+        pass
+
+    def do_GET(self):
+        self.send_response(200, 'OK')
+        self.send_header('Content-Length', '5')
+        self.end_headers()
+        self.wfile.write(b'hello')
+
+
+class TestConnectionLifetime(NativeResourceTest):
+    """Tests for http_connection_binding ref-count based lifetime management.
+
+    Under free-threaded Python (Py_GIL_DISABLED), the capsule destructor
+    (application thread) and on_connection_shutdown (event-loop thread) can
+    race. These tests exercise both orderings and a stress scenario.
+    """
+    hostname = 'localhost'
+    timeout = 10
+
+    def setUp(self):
+        super().setUp()
+        self.server = HTTPServer((self.hostname, 0), SilentHandler)
+        self.port = self.server.server_address[1]
+        self.server_thread = 
threading.Thread(target=self.server.serve_forever, daemon=True)
+        self.server_thread.start()
+
+    def tearDown(self):
+        self.server.shutdown()
+        self.server.server_close()
+        self.server_thread.join()
+        super().tearDown()
+
+    def _new_connection(self):
+        event_loop_group = EventLoopGroup()
+        host_resolver = DefaultHostResolver(event_loop_group)
+        bootstrap = ClientBootstrap(event_loop_group, host_resolver)
+        future = HttpClientConnection.new(
+            host_name=self.hostname,
+            port=self.port,
+            bootstrap=bootstrap)
+        return future.result(self.timeout)
+
+    def test_release_before_shutdown(self):
+        """Capsule destructor fires first, then shutdown callback."""
+        connection = self._new_connection()
+        shutdown_future = connection.shutdown_future
+
+        del connection
+        gc.collect()
+
+        shutdown_future.result(self.timeout)
+
+    def test_shutdown_before_release(self):
+        """Shutdown callback fires first (via close), then capsule 
destructor."""
+        connection = self._new_connection()
+        shutdown_future = connection.shutdown_future
+
+        connection.close()
+        shutdown_future.result(self.timeout)
+
+        del connection
+        gc.collect()
+
+    def test_concurrent_release_and_shutdown_stress(self):
+        """Stress: race capsule destructor against shutdown from many threads.
+
+        Under Py_GIL_DISABLED, the old two-bool approach would double-free.
+        With atomic ref-counting, exactly one path destroys the binding.
+        """
+        iterations = 50
+        errors = []
+
+        def release_connection(conn):
+            try:
+                del conn
+                gc.collect()
+            except Exception as e:
+                errors.append(e)
+
+        for i in range(iterations):
+            try:
+                connection = self._new_connection()
+            except awscrt.exceptions.AwsCrtError as e:
+                if e.name == 'AWS_IO_SOCKET_CONNECTION_REFUSED':
+                    continue
+                raise
+
+            shutdown_future = connection.shutdown_future
+
+            connection.close()
+
+            t = threading.Thread(target=release_connection, args=(connection,))
+            del connection
+            t.start()
+
+            shutdown_future.result(self.timeout)
+            t.join(self.timeout)
+
+        self.assertEqual([], errors)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/test/test_websocket.py 
new/aws-crt-python-0.33.0/test/test_websocket.py
--- old/aws-crt-python-0.32.2/test/test_websocket.py    2026-04-24 
22:49:53.000000000 +0200
+++ new/aws-crt-python-0.33.0/test/test_websocket.py    2026-05-22 
19:12:54.000000000 +0200
@@ -13,6 +13,8 @@
 from queue import Empty, Queue
 import secrets
 import socket
+import subprocess
+import sys
 from test import NativeResourceTest
 import threading
 from time import sleep, time
@@ -182,6 +184,54 @@
         asyncio.run_coroutine_threadsafe(self._current_connection.send(msg), 
self._server_loop)
 
 
+class MockHandshakeServer:
+    # A raw-socket server that accepts one connection, drains the client's
+    # HTTP handshake request, and sends back a caller-provided response.
+    # Use this when tests need to send byte sequences that the 3rdparty
+    # `websockets` library can't produce (e.g. malformed headers).
+    #
+    # Usage:
+    #     with MockHandshakeServer(host, response=b"HTTP/1.1 ...") as server:
+    #         # spawn a client that connects to (host, server.port)
+    #         ...
+
+    def __init__(self, host, response):
+        self._host = host
+        self._response = response
+        self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self._listener.bind((host, 0))
+        self._listener.listen(1)
+        self.port = self._listener.getsockname()[1]
+        self._thread = threading.Thread(target=self._serve, daemon=True)
+
+    def __enter__(self):
+        self._thread.start()
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        self._listener.close()
+        self._thread.join(TIMEOUT)
+
+    def _serve(self):
+        try:
+            conn, _ = self._listener.accept()
+        except OSError:
+            return
+        with closing(conn):
+            conn.settimeout(TIMEOUT)
+            try:
+                buf = b""
+                while b"\r\n\r\n" not in buf:
+                    chunk = conn.recv(4096)
+                    if not chunk:
+                        return
+                    buf += chunk
+                conn.sendall(self._response)
+            except OSError:
+                pass
+
+
 class TestClient(NativeResourceTest):
     def setUp(self):
         super().setUp()
@@ -324,6 +374,60 @@
             # check that body is a valid string
             
self.assertGreater(len(setup_data.handshake_response_body.decode()), 0)
 
+    def test_connect_response_header_with_invalid_name_is_protocol_error(self):
+        # A response header whose name contains a non-tchar byte (e.g. 0xE9) is
+        # rejected by aws-c-http's HTTP/1.1 decoder before reaching the 
binding.
+        # The connection should fail with AWS_ERROR_HTTP_PROTOCOL_ERROR.
+        response = (
+            b"HTTP/1.1 403 Forbidden\r\n"
+            b"Content-Length: 0\r\n"
+            b"X-Bad\xe9Name: whatever\r\n"
+            b"\r\n"
+        )
+        with MockHandshakeServer(self.host, response=response) as server:
+            setup_future = Future()
+            connect(
+                host=self.host,
+                port=server.port,
+                handshake_request=create_handshake_request(host=self.host),
+                on_connection_setup=lambda x: setup_future.set_result(x))
+
+            setup_data: OnConnectionSetupData = setup_future.result(TIMEOUT)
+
+        self.assertIsNone(setup_data.websocket)
+        self.assertIsNotNone(setup_data.exception)
+        self.assertEqual("AWS_ERROR_HTTP_PROTOCOL_ERROR", 
setup_data.exception.name)
+        # bad-name response is rejected at the parser, so no headers reach 
Python
+        self.assertIsNone(setup_data.handshake_response_headers)
+
+    def test_connect_response_header_with_obs_text_does_not_abort(self):
+        # A response header value containing a non-UTF-8 obs-text byte (e.g. 
lone 0xE9)
+        # must not crash the process. Run the client in a subprocess so that 
an abort,
+        # if it happens, is observable as a non-zero exit code.
+        response = (
+            b"HTTP/1.1 403 Forbidden\r\n"
+            b"Content-Length: 0\r\n"
+            b"X-Reason: caf\xe9\r\n"
+            b"\r\n"
+        )
+        with MockHandshakeServer(self.host, response=response) as server:
+            proc = subprocess.Popen(
+                [sys.executable, '-m', 'test.ws_connect_helper', self.host, 
str(server.port)],
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE)
+
+            try:
+                stdout, stderr = proc.communicate(timeout=TIMEOUT)
+            except subprocess.TimeoutExpired:
+                proc.kill()
+                stdout, stderr = proc.communicate()
+                self.fail("client subprocess hung")
+
+        self.assertEqual(
+            0, proc.returncode,
+            f"client subprocess crashed (returncode={proc.returncode}). "
+            f"stdout={stdout!r} stderr={stderr!r}")
+
     def test_exception_in_setup_callback_closes_websocket(self):
         with WebSocketServer(self.host, self.port) as server:
             setup_future = Future()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aws-crt-python-0.32.2/test/ws_connect_helper.py 
new/aws-crt-python-0.33.0/test/ws_connect_helper.py
--- old/aws-crt-python-0.32.2/test/ws_connect_helper.py 1970-01-01 
01:00:00.000000000 +0100
+++ new/aws-crt-python-0.33.0/test/ws_connect_helper.py 2026-05-22 
19:12:54.000000000 +0200
@@ -0,0 +1,28 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0.
+
+# Helper for test_websocket subprocess scenarios.
+# Runs awscrt.websocket.connect() against a host:port given on the command
+# line and waits for on_connection_setup to fire. Used by tests that need
+# to observe whether a malformed server response crashes the client process.
+
+import sys
+from concurrent.futures import Future
+
+from awscrt.websocket import connect, create_handshake_request
+
+TIMEOUT = 10.0
+
+
+def main(host, port):
+    setup_future = Future()
+    connect(
+        host=host,
+        port=port,
+        handshake_request=create_handshake_request(host=host),
+        on_connection_setup=lambda x: setup_future.set_result(x))
+    setup_future.result(TIMEOUT)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1], int(sys.argv[2]))

Reply via email to