Skip to content

Commit e23114c

Browse files
authored
fix: remove query text from exception message, use exception.debug_message instead (#1105)
Since query text can potentially contain sensitive information, remove it from the default exception message. This information is useful for debugging, so provide a `debug_message` attribute, which is not included in the exception representation (and thus the logs). Fixes internal issue 211616590
1 parent 18ee0b7 commit e23114c

File tree

2 files changed

+29
-10
lines changed

2 files changed

+29
-10
lines changed

google/cloud/bigquery/job/query.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767

6868
_CONTAINS_ORDER_BY = re.compile(r"ORDER\s+BY", re.IGNORECASE)
69+
_EXCEPTION_FOOTER_TEMPLATE = "{message}\n\nLocation: {location}\nJob ID: {job_id}\n"
6970
_TIMEOUT_BUFFER_SECS = 0.1
7071

7172

@@ -1196,17 +1197,17 @@ def _blocking_poll(self, timeout=None, **kwargs):
11961197
super(QueryJob, self)._blocking_poll(timeout=timeout, **kwargs)
11971198

11981199
@staticmethod
1199-
def _format_for_exception(query, job_id):
1200+
def _format_for_exception(message: str, query: str):
12001201
"""Format a query for the output in exception message.
12011202
12021203
Args:
1204+
message (str): The original exception message.
12031205
query (str): The SQL query to format.
1204-
job_id (str): The ID of the job that ran the query.
12051206
12061207
Returns:
12071208
str: A formatted query text.
12081209
"""
1209-
template = "\n\n(job ID: {job_id})\n\n{header}\n\n{ruler}\n{body}\n{ruler}"
1210+
template = "{message}\n\n{header}\n\n{ruler}\n{body}\n{ruler}"
12101211

12111212
lines = query.splitlines()
12121213
max_line_len = max(len(line) for line in lines)
@@ -1223,7 +1224,7 @@ def _format_for_exception(query, job_id):
12231224
"{:4}:{}".format(n, line) for n, line in enumerate(lines, start=1)
12241225
)
12251226

1226-
return template.format(job_id=job_id, header=header, ruler=ruler, body=body)
1227+
return template.format(message=message, header=header, ruler=ruler, body=body)
12271228

12281229
def _begin(self, client=None, retry=DEFAULT_RETRY, timeout=None):
12291230
"""API call: begin the job via a POST request
@@ -1248,7 +1249,10 @@ def _begin(self, client=None, retry=DEFAULT_RETRY, timeout=None):
12481249
try:
12491250
super(QueryJob, self)._begin(client=client, retry=retry, timeout=timeout)
12501251
except exceptions.GoogleAPICallError as exc:
1251-
exc.message += self._format_for_exception(self.query, self.job_id)
1252+
exc.message = _EXCEPTION_FOOTER_TEMPLATE.format(
1253+
message=exc.message, location=self.location, job_id=self.job_id
1254+
)
1255+
exc.debug_message = self._format_for_exception(exc.message, self.query)
12521256
exc.query_job = self
12531257
raise
12541258

@@ -1447,7 +1451,10 @@ def do_get_result():
14471451
do_get_result()
14481452

14491453
except exceptions.GoogleAPICallError as exc:
1450-
exc.message += self._format_for_exception(self.query, self.job_id)
1454+
exc.message = _EXCEPTION_FOOTER_TEMPLATE.format(
1455+
message=exc.message, location=self.location, job_id=self.job_id
1456+
)
1457+
exc.debug_message = self._format_for_exception(exc.message, self.query) # type: ignore
14511458
exc.query_job = self # type: ignore
14521459
raise
14531460
except requests.exceptions.Timeout as exc:

tests/unit/job/test_query.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -1360,13 +1360,19 @@ def test_result_error(self):
13601360
exc_job_instance = getattr(exc_info.exception, "query_job", None)
13611361
self.assertIs(exc_job_instance, job)
13621362

1363+
# Query text could contain sensitive information, so it must not be
1364+
# included in logs / exception representation.
13631365
full_text = str(exc_info.exception)
13641366
assert job.job_id in full_text
1365-
assert "Query Job SQL Follows" in full_text
1367+
assert "Query Job SQL Follows" not in full_text
13661368

1369+
# It is useful to have query text available, so it is provided in a
1370+
# debug_message property.
1371+
debug_message = exc_info.exception.debug_message
1372+
assert "Query Job SQL Follows" in debug_message
13671373
for i, line in enumerate(query.splitlines(), start=1):
13681374
expected_line = "{}:{}".format(i, line)
1369-
assert expected_line in full_text
1375+
assert expected_line in debug_message
13701376

13711377
def test_result_transport_timeout_error(self):
13721378
query = textwrap.dedent(
@@ -1452,13 +1458,19 @@ def test__begin_error(self):
14521458
exc_job_instance = getattr(exc_info.exception, "query_job", None)
14531459
self.assertIs(exc_job_instance, job)
14541460

1461+
# Query text could contain sensitive information, so it must not be
1462+
# included in logs / exception representation.
14551463
full_text = str(exc_info.exception)
14561464
assert job.job_id in full_text
1457-
assert "Query Job SQL Follows" in full_text
1465+
assert "Query Job SQL Follows" not in full_text
14581466

1467+
# It is useful to have query text available, so it is provided in a
1468+
# debug_message property.
1469+
debug_message = exc_info.exception.debug_message
1470+
assert "Query Job SQL Follows" in debug_message
14591471
for i, line in enumerate(query.splitlines(), start=1):
14601472
expected_line = "{}:{}".format(i, line)
1461-
assert expected_line in full_text
1473+
assert expected_line in debug_message
14621474

14631475
def test__begin_w_timeout(self):
14641476
PATH = "/projects/%s/jobs" % (self.PROJECT,)

0 commit comments

Comments
 (0)