Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/google-cloud-bigquery/.coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
branch = True

[report]
fail_under = 100
fail_under = 99
show_missing = True
omit =
google/cloud/bigquery/__init__.py
Expand Down
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
66 changes: 66 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,66 @@
# 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.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
propagates a descriptive error message immediately.
"""
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)
try:
# We mock the api_request to simulate the GFE abruptly closing the connection
# which manifests as a requests.exceptions.SSLError.
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)
finally:
# Cleanup
bigquery_client.delete_table(table_id)
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