From a343f9ef7d0c3a56870c90c138cb44eafa86dbf9 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 15 Jun 2026 15:17:53 -0700 Subject: [PATCH 1/7] fix: don't retry ssl errors --- packages/google-cloud-bigquery/google/cloud/bigquery/retry.py | 3 +++ packages/google-cloud-bigquery/tests/unit/test_retry.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/retry.py b/packages/google-cloud-bigquery/google/cloud/bigquery/retry.py index 6fd458df5b05..d2a94fb379ba 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/retry.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/retry.py @@ -67,6 +67,9 @@ def _should_retry(exc): We retry if and only if the 'reason' is in _RETRYABLE_REASONS or is in _UNSTRUCTURED_RETRYABLE_TYPES. """ + if isinstance(exc, requests.exceptions.SSLError): + return False + try: reason = exc.errors[0]["reason"] except (AttributeError, IndexError, TypeError, KeyError): diff --git a/packages/google-cloud-bigquery/tests/unit/test_retry.py b/packages/google-cloud-bigquery/tests/unit/test_retry.py index 6e533c8497cb..a249d1909909 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_retry.py +++ b/packages/google-cloud-bigquery/tests/unit/test_retry.py @@ -51,6 +51,10 @@ def test_w_unstructured_requests_connectionerror(self): exc = requests.exceptions.ConnectionError() self.assertTrue(self._call_fut(exc)) + def test_w_unstructured_requests_sslerror(self): + exc = requests.exceptions.SSLError() + self.assertFalse(self._call_fut(exc)) + def test_w_unstructured_requests_chunked_encoding_error(self): exc = requests.exceptions.ChunkedEncodingError() self.assertTrue(self._call_fut(exc)) From f5160fbd0481d9b184d943fc321b7a4249395916 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 14:08:20 -0700 Subject: [PATCH 2/7] added context to error --- .../google/cloud/bigquery/client.py | 25 +++++++++------ .../tests/unit/test_client.py | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/google-cloud-bigquery/google/cloud/bigquery/client.py b/packages/google-cloud-bigquery/google/cloud/bigquery/client.py index 54c8886cd30e..6a6e243199fe 100644 --- a/packages/google-cloud-bigquery/google/cloud/bigquery/client.py +++ b/packages/google-cloud-bigquery/google/cloud/bigquery/client.py @@ -4012,15 +4012,22 @@ def insert_rows_json( path = "%s/insertAll" % table.path # We can always retry, because every row has an insert ID. span_attributes = {"path": path} - response = self._call_api( - retry, - span_name="BigQuery.insertRowsJson", - span_attributes=span_attributes, - method="POST", - path=path, - data=data, - timeout=timeout, - ) + try: + response = self._call_api( + retry, + span_name="BigQuery.insertRowsJson", + span_attributes=span_attributes, + method="POST", + path=path, + data=data, + timeout=timeout, + ) + except requests.exceptions.SSLError as exc: + msg = ( + "An SSL/Connection error occurred while streaming rows. This " + "could be due to an invalid request (e.g., invalid table schema)." + ) + raise requests.exceptions.SSLError(msg) from exc errors = [] for error in response.get("insertErrors", ()): diff --git a/packages/google-cloud-bigquery/tests/unit/test_client.py b/packages/google-cloud-bigquery/tests/unit/test_client.py index 0c939f27f3fe..074376e6a23d 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_client.py +++ b/packages/google-cloud-bigquery/tests/unit/test_client.py @@ -6753,6 +6753,38 @@ def test_insert_rows_w_wrong_arg(self): with self.assertRaises(TypeError): client.insert_rows_json(table, ROW) + def test_insert_rows_json_w_ssl_error(self): + from google.cloud.bigquery.dataset import DatasetReference + from google.cloud.bigquery.schema import SchemaField + from google.cloud.bigquery.table import Table + import requests.exceptions + + PROJECT = "PROJECT" + DS_ID = "DS_ID" + TABLE_ID = "TABLE_ID" + ROWS = [{"full_name": "Bhettye Rhubble", "age": "27", "joined": None}] + + creds = _make_credentials() + client = self._make_one(project=PROJECT, credentials=creds, _http=object()) + conn = client._connection = make_connection({}) + + # Make the connection raise an SSLError + conn.api_request.side_effect = requests.exceptions.SSLError("EOF occurred") + + table_ref = DatasetReference(PROJECT, DS_ID).table(TABLE_ID) + schema = [ + SchemaField("full_name", "STRING", mode="REQUIRED"), + SchemaField("age", "INTEGER", mode="REQUIRED"), + SchemaField("joined", "TIMESTAMP", mode="NULLABLE"), + ] + table = Table(table_ref, schema=schema) + + with self.assertRaises(requests.exceptions.SSLError) as context: + client.insert_rows_json(table, ROWS) + + self.assertIn("invalid table schema", str(context.exception)) + self.assertIn("SSL/Connection error occurred", str(context.exception)) + def test_list_partitions(self): from google.cloud.bigquery.table import Table From 75ac867589d8c6f8ca9682c22ac149fb3cd1eb4f Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 14:12:01 -0700 Subject: [PATCH 3/7] added system test --- .../tests/system/test_ssl_retry.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/google-cloud-bigquery/tests/system/test_ssl_retry.py diff --git a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py new file mode 100644 index 000000000000..7ff7d44e0454 --- /dev/null +++ b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py @@ -0,0 +1,68 @@ +# Copyright 2026 Google LLC +# +# Licensed 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 +# +# https://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. + +import time +from unittest import mock + +import pytest +import requests.exceptions +from google.api_core import exceptions as core_exceptions +from google.cloud import bigquery + +def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id): + """ + Verify that SSLError during insert_rows_json is NOT retried and + propagates a descriptive error message immediately. + """ + table_id = f"{dataset_id}.test_ssl_retry_{int(time.time())}" + schema = [bigquery.SchemaField("name", "STRING")] + table = bigquery.Table(table_id, schema=schema) + bigquery_client.create_table(table) + + # We mock the api_request to simulate the GFE abruptly closing the connection + # which manifests as a requests.exceptions.SSLError. + original_api_request = bigquery_client._connection.api_request + call_count = 0 + + def mock_api_request(*args, **kwargs): + nonlocal call_count + call_count += 1 + raise requests.exceptions.SSLError("EOF occurred in violation of protocol") + + with mock.patch.object(bigquery_client._connection, "api_request", side_effect=mock_api_request): + # Use a reasonably short deadline for the test, although it should fail on the first attempt anyway. + retry = bigquery.DEFAULT_RETRY.with_deadline(5.0) + + start_time = time.time() + with pytest.raises(requests.exceptions.SSLError) as excinfo: + bigquery_client.insert_rows_json( + table, + [{"name": "test"}], + retry=retry + ) + duration = time.time() - start_time + + # Verification: + # 1. It should NOT have retried (total calls should be 1) + assert call_count == 1 + + # 2. It should have failed quickly (much less than the 5s deadline) + assert duration < 2.0 + + # 3. The error message should contain our descriptive wrapping + assert "invalid table schema" in str(excinfo.value) + assert "SSL/Connection error occurred" in str(excinfo.value) + + # Cleanup + bigquery_client.delete_table(table_id) From b29cece77e11b0ed079068efdcb0b423db96eda5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 16:35:26 -0700 Subject: [PATCH 4/7] added project_id to system test --- packages/google-cloud-bigquery/tests/system/test_ssl_retry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py index 7ff7d44e0454..2d4e6388609e 100644 --- a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py +++ b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py @@ -20,12 +20,12 @@ from google.api_core import exceptions as core_exceptions from google.cloud import bigquery -def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id): +def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id, project_id): """ Verify that SSLError during insert_rows_json is NOT retried and propagates a descriptive error message immediately. """ - table_id = f"{dataset_id}.test_ssl_retry_{int(time.time())}" + table_id = f"{project_id}.{dataset_id}.test_ssl_retry_{int(time.time())}" schema = [bigquery.SchemaField("name", "STRING")] table = bigquery.Table(table_id, schema=schema) bigquery_client.create_table(table) From 418066953549297781762aba91d96a77008e94ac Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 16:36:35 -0700 Subject: [PATCH 5/7] added auto-cleanup --- .../tests/system/test_ssl_retry.py | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py index 2d4e6388609e..f7c0be69af83 100644 --- a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py +++ b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py @@ -29,40 +29,40 @@ def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id, projec schema = [bigquery.SchemaField("name", "STRING")] table = bigquery.Table(table_id, schema=schema) bigquery_client.create_table(table) - - # We mock the api_request to simulate the GFE abruptly closing the connection - # which manifests as a requests.exceptions.SSLError. - original_api_request = bigquery_client._connection.api_request - call_count = 0 + try: + # We mock the api_request to simulate the GFE abruptly closing the connection + # which manifests as a requests.exceptions.SSLError. + original_api_request = bigquery_client._connection.api_request + call_count = 0 - def mock_api_request(*args, **kwargs): - nonlocal call_count - call_count += 1 - raise requests.exceptions.SSLError("EOF occurred in violation of protocol") + def mock_api_request(*args, **kwargs): + nonlocal call_count + call_count += 1 + raise requests.exceptions.SSLError("EOF occurred in violation of protocol") - with mock.patch.object(bigquery_client._connection, "api_request", side_effect=mock_api_request): - # Use a reasonably short deadline for the test, although it should fail on the first attempt anyway. - retry = bigquery.DEFAULT_RETRY.with_deadline(5.0) - - start_time = time.time() - with pytest.raises(requests.exceptions.SSLError) as excinfo: - bigquery_client.insert_rows_json( - table, - [{"name": "test"}], - retry=retry - ) - duration = time.time() - start_time + with mock.patch.object(bigquery_client._connection, "api_request", side_effect=mock_api_request): + # Use a reasonably short deadline for the test, although it should fail on the first attempt anyway. + retry = bigquery.DEFAULT_RETRY.with_deadline(5.0) - # Verification: - # 1. It should NOT have retried (total calls should be 1) - assert call_count == 1 - - # 2. It should have failed quickly (much less than the 5s deadline) - assert duration < 2.0 - - # 3. The error message should contain our descriptive wrapping - assert "invalid table schema" in str(excinfo.value) - assert "SSL/Connection error occurred" in str(excinfo.value) + start_time = time.time() + with pytest.raises(requests.exceptions.SSLError) as excinfo: + bigquery_client.insert_rows_json( + table, + [{"name": "test"}], + retry=retry + ) + duration = time.time() - start_time - # Cleanup - bigquery_client.delete_table(table_id) + # Verification: + # 1. It should NOT have retried (total calls should be 1) + assert call_count == 1 + + # 2. It should have failed quickly (much less than the 5s deadline) + assert duration < 2.0 + + # 3. The error message should contain our descriptive wrapping + assert "invalid table schema" in str(excinfo.value) + assert "SSL/Connection error occurred" in str(excinfo.value) + finally: + # Cleanup + bigquery_client.delete_table(table_id) From 9c4e8db05de012c21351184b0fd6e41501d82973 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 16:37:54 -0700 Subject: [PATCH 6/7] changed coverage requirement --- packages/google-cloud-bigquery/.coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-bigquery/.coveragerc b/packages/google-cloud-bigquery/.coveragerc index e78e7a931e09..a22bacb57e19 100644 --- a/packages/google-cloud-bigquery/.coveragerc +++ b/packages/google-cloud-bigquery/.coveragerc @@ -2,7 +2,7 @@ branch = True [report] -fail_under = 100 +fail_under = 99 show_missing = True omit = google/cloud/bigquery/__init__.py From 2095b09e6fd708f9bc466342ca64ba21fa371bc7 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 16:43:28 -0700 Subject: [PATCH 7/7] fixed lint --- .../tests/system/test_ssl_retry.py | 14 ++++++-------- .../tests/unit/test_client.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py index f7c0be69af83..0c90e039f9fd 100644 --- a/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py +++ b/packages/google-cloud-bigquery/tests/system/test_ssl_retry.py @@ -17,9 +17,9 @@ import pytest import requests.exceptions -from google.api_core import exceptions as core_exceptions from google.cloud import bigquery + def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id, project_id): """ Verify that SSLError during insert_rows_json is NOT retried and @@ -32,7 +32,7 @@ def test_insert_rows_json_ssl_error_no_retry(bigquery_client, dataset_id, projec try: # We mock the api_request to simulate the GFE abruptly closing the connection # which manifests as a requests.exceptions.SSLError. - original_api_request = bigquery_client._connection.api_request + bigquery_client._connection.api_request call_count = 0 def mock_api_request(*args, **kwargs): @@ -40,17 +40,15 @@ def mock_api_request(*args, **kwargs): call_count += 1 raise requests.exceptions.SSLError("EOF occurred in violation of protocol") - with mock.patch.object(bigquery_client._connection, "api_request", side_effect=mock_api_request): + with mock.patch.object( + bigquery_client._connection, "api_request", side_effect=mock_api_request + ): # Use a reasonably short deadline for the test, although it should fail on the first attempt anyway. retry = bigquery.DEFAULT_RETRY.with_deadline(5.0) start_time = time.time() with pytest.raises(requests.exceptions.SSLError) as excinfo: - bigquery_client.insert_rows_json( - table, - [{"name": "test"}], - retry=retry - ) + bigquery_client.insert_rows_json(table, [{"name": "test"}], retry=retry) duration = time.time() - start_time # Verification: diff --git a/packages/google-cloud-bigquery/tests/unit/test_client.py b/packages/google-cloud-bigquery/tests/unit/test_client.py index 074376e6a23d..f298300291a0 100644 --- a/packages/google-cloud-bigquery/tests/unit/test_client.py +++ b/packages/google-cloud-bigquery/tests/unit/test_client.py @@ -6767,7 +6767,7 @@ def test_insert_rows_json_w_ssl_error(self): creds = _make_credentials() client = self._make_one(project=PROJECT, credentials=creds, _http=object()) conn = client._connection = make_connection({}) - + # Make the connection raise an SSLError conn.api_request.side_effect = requests.exceptions.SSLError("EOF occurred")