Skip to content
25 changes: 16 additions & 9 deletions packages/google-cloud-bigquery/google/cloud/bigquery/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", ()):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
68 changes: 68 additions & 0 deletions packages/google-cloud-bigquery/tests/system/test_ssl_retry.py
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
daniel-sanche marked this conversation as resolved.
Outdated
32 changes: 32 additions & 0 deletions packages/google-cloud-bigquery/tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions packages/google-cloud-bigquery/tests/unit/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading