Skip to content

Commit 0ddfa46

Browse files
committed
Raise a consistent exception on input encoding errors
Currently, when invalid input is passed a query argument, asyncpg will raise whatever exception was triggered by the codec function, which can be `TypeError`, `ValueError`, `decimal.InvalidOperation` etc. Additionally, these exceptions lack sufficient context as to which argument actually triggered an error. Fix this by consistently raising the new `asyncpg.DataError` exception, which is a subclass of `asyncpg.InterfaceError` and `ValueError`, and include the position of the offending argument as well as the passed value, e.g: asyncpg.exceptions.DataError: invalid input for query argument $1: 'aaa' (a bytes-like object is required, not 'str') Fixes: #260
1 parent 482a186 commit 0ddfa46

File tree

6 files changed

+75
-50
lines changed

6 files changed

+75
-50
lines changed

asyncpg/exceptions/_base.py

+4
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ def __init__(self, msg, *, detail=None, hint=None):
209209
Exception.__init__(self, msg)
210210

211211

212+
class DataError(InterfaceError, ValueError):
213+
"""An error caused by invalid query input."""
214+
215+
212216
class InterfaceWarning(InterfaceMessage, UserWarning):
213217
"""A warning caused by an improper use of asyncpg API."""
214218

asyncpg/protocol/codecs/float.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ cdef float4_encode(ConnectionSettings settings, WriteBuffer buf, obj):
1212
cdef double dval = cpython.PyFloat_AsDouble(obj)
1313
cdef float fval = <float>dval
1414
if math.isinf(fval) and not math.isinf(dval):
15-
raise ValueError('float value too large to be encoded as FLOAT4')
15+
raise ValueError('value out of float32 range')
1616

1717
buf.write_int32(4)
1818
buf.write_float(fval)

asyncpg/protocol/codecs/int.pyx

+4-8
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ cdef int2_encode(ConnectionSettings settings, WriteBuffer buf, obj):
2828
overflow = 1
2929

3030
if overflow or val < INT16_MIN or val > INT16_MAX:
31-
raise OverflowError(
32-
'int16 value out of range: {!r}'.format(obj))
31+
raise OverflowError('value out of int16 range')
3332

3433
buf.write_int32(2)
3534
buf.write_int16(<int16_t>val)
@@ -50,8 +49,7 @@ cdef int4_encode(ConnectionSettings settings, WriteBuffer buf, obj):
5049

5150
# "long" and "long long" have the same size for x86_64, need an extra check
5251
if overflow or (sizeof(val) > 4 and (val < INT32_MIN or val > INT32_MAX)):
53-
raise OverflowError(
54-
'int32 value out of range: {!r}'.format(obj))
52+
raise OverflowError('value out of int32 range')
5553

5654
buf.write_int32(4)
5755
buf.write_int32(<int32_t>val)
@@ -72,8 +70,7 @@ cdef uint4_encode(ConnectionSettings settings, WriteBuffer buf, obj):
7270

7371
# "long" and "long long" have the same size for x86_64, need an extra check
7472
if overflow or (sizeof(val) > 4 and val > UINT32_MAX):
75-
raise OverflowError(
76-
'uint32 value out of range: {!r}'.format(obj))
73+
raise OverflowError('value out of uint32 range')
7774

7875
buf.write_int32(4)
7976
buf.write_int32(<int32_t>val)
@@ -95,8 +92,7 @@ cdef int8_encode(ConnectionSettings settings, WriteBuffer buf, obj):
9592

9693
# Just in case for systems with "long long" bigger than 8 bytes
9794
if overflow or (sizeof(val) > 8 and (val < INT64_MIN or val > INT64_MAX)):
98-
raise OverflowError(
99-
'int64 value out of range: {!r}'.format(obj))
95+
raise OverflowError('value out of int64 range')
10096

10197
buf.write_int32(8)
10298
buf.write_int64(<int64_t>val)

asyncpg/protocol/codecs/tid.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ cdef tid_encode(ConnectionSettings settings, WriteBuffer buf, obj):
2424

2525
# "long" and "long long" have the same size for x86_64, need an extra check
2626
if overflow or (sizeof(block) > 4 and block > UINT32_MAX):
27-
raise OverflowError(
28-
'tuple id block value out of range: {!r}'.format(obj[0]))
27+
raise OverflowError('tuple id block value out of uint32 range')
2928

3029
try:
3130
offset = cpython.PyLong_AsUnsignedLong(obj[1])
@@ -34,8 +33,7 @@ cdef tid_encode(ConnectionSettings settings, WriteBuffer buf, obj):
3433
overflow = 1
3534

3635
if overflow or offset > 65535:
37-
raise OverflowError(
38-
'tuple id offset value out of range: {!r}'.format(obj[1]))
36+
raise OverflowError('tuple id offset value out of uint16 range')
3937

4038
buf.write_int32(6)
4139
buf.write_int32(<int32_t>block)

asyncpg/protocol/prepared_stmt.pyx

+22-4
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ cdef class PreparedStatementState:
127127

128128
if self.have_text_args:
129129
writer.write_int16(self.args_num)
130-
for idx from 0 <= idx < self.args_num:
130+
for idx in range(self.args_num):
131131
codec = <Codec>(self.args_codecs[idx])
132132
writer.write_int16(codec.format)
133133
else:
@@ -136,17 +136,35 @@ cdef class PreparedStatementState:
136136

137137
writer.write_int16(self.args_num)
138138

139-
for idx from 0 <= idx < self.args_num:
139+
for idx in range(self.args_num):
140140
arg = args[idx]
141141
if arg is None:
142142
writer.write_int32(-1)
143143
else:
144144
codec = <Codec>(self.args_codecs[idx])
145-
codec.encode(self.settings, writer, arg)
145+
try:
146+
codec.encode(self.settings, writer, arg)
147+
except (AssertionError, exceptions.InternalClientError):
148+
# These are internal errors and should raise as-is.
149+
raise
150+
except exceptions.InterfaceError:
151+
# This is already a descriptive error.
152+
raise
153+
except Exception as e:
154+
# Everything else is assumed to be an encoding error
155+
# due to invalid input.
156+
value_repr = repr(arg)
157+
if len(value_repr) > 40:
158+
value_repr = value_repr[:40] + '...'
159+
160+
raise exceptions.DataError(
161+
'invalid input for query argument'
162+
' ${n}: {v} ({msg})'.format(
163+
n=idx + 1, v=value_repr, msg=e)) from e
146164

147165
if self.have_text_cols:
148166
writer.write_int16(self.cols_num)
149-
for idx from 0 <= idx < self.cols_num:
167+
for idx in range(self.cols_num):
150168
codec = <Codec>(self.rows_codecs[idx])
151169
writer.write_int16(codec.format)
152170
else:

tests/test_codecs.py

+42-33
Original file line numberDiff line numberDiff line change
@@ -542,17 +542,19 @@ async def test_numeric(self):
542542
"SELECT $1::numeric", decimal.Decimal('sNaN'))
543543
self.assertTrue(res.is_nan())
544544

545-
with self.assertRaisesRegex(ValueError, 'numeric type does not '
546-
'support infinite values'):
545+
with self.assertRaisesRegex(asyncpg.DataError,
546+
'numeric type does not '
547+
'support infinite values'):
547548
await self.con.fetchval(
548549
"SELECT $1::numeric", decimal.Decimal('-Inf'))
549550

550-
with self.assertRaisesRegex(ValueError, 'numeric type does not '
551-
'support infinite values'):
551+
with self.assertRaisesRegex(asyncpg.DataError,
552+
'numeric type does not '
553+
'support infinite values'):
552554
await self.con.fetchval(
553555
"SELECT $1::numeric", decimal.Decimal('+Inf'))
554556

555-
with self.assertRaises(decimal.InvalidOperation):
557+
with self.assertRaisesRegex(asyncpg.DataError, 'invalid'):
556558
await self.con.fetchval(
557559
"SELECT $1::numeric", 'invalid')
558560

@@ -578,91 +580,95 @@ async def test_unhandled_type_fallback(self):
578580

579581
async def test_invalid_input(self):
580582
cases = [
581-
('bytea', TypeError, 'a bytes-like object is required', [
583+
('bytea', 'a bytes-like object is required', [
582584
1,
583585
'aaa'
584586
]),
585-
('bool', TypeError, 'a boolean is required', [
587+
('bool', 'a boolean is required', [
586588
1,
587589
]),
588-
('int2', TypeError, 'an integer is required', [
590+
('int2', 'an integer is required', [
589591
'2',
590592
'aa',
591593
]),
592-
('smallint', OverflowError, 'int16 value out of range', [
594+
('smallint', 'value out of int16 range', [
593595
2**256, # check for the same exception for any big numbers
594596
decimal.Decimal("2000000000000000000000000000000"),
595597
0xffff,
596598
0xffffffff,
597599
32768,
598600
-32769
599601
]),
600-
('float4', ValueError, 'float value too large', [
602+
('float4', 'value out of float32 range', [
601603
4.1 * 10 ** 40,
602604
-4.1 * 10 ** 40,
603605
]),
604-
('int4', TypeError, 'an integer is required', [
606+
('int4', 'an integer is required', [
605607
'2',
606608
'aa',
607609
]),
608-
('int', OverflowError, 'int32 value out of range', [
610+
('int', 'value out of int32 range', [
609611
2**256, # check for the same exception for any big numbers
610612
decimal.Decimal("2000000000000000000000000000000"),
611613
0xffffffff,
612614
2**31,
613615
-2**31 - 1,
614616
]),
615-
('int8', TypeError, 'an integer is required', [
617+
('int8', 'an integer is required', [
616618
'2',
617619
'aa',
618620
]),
619-
('bigint', OverflowError, 'int64 value out of range', [
621+
('bigint', 'value out of int64 range', [
620622
2**256, # check for the same exception for any big numbers
621623
decimal.Decimal("2000000000000000000000000000000"),
622624
0xffffffffffffffff,
623625
2**63,
624626
-2**63 - 1,
625627
]),
626-
('text', TypeError, 'expected str, got bytes', [
628+
('text', 'expected str, got bytes', [
627629
b'foo'
628630
]),
629-
('text', TypeError, 'expected str, got list', [
631+
('text', 'expected str, got list', [
630632
[1]
631633
]),
632-
('tid', TypeError, 'list or tuple expected', [
634+
('tid', 'list or tuple expected', [
633635
b'foo'
634636
]),
635-
('tid', ValueError, 'invalid number of elements in tid tuple', [
637+
('tid', 'invalid number of elements in tid tuple', [
636638
[],
637639
(),
638640
[1, 2, 3],
639641
(4,),
640642
]),
641-
('tid', OverflowError, 'tuple id block value out of range', [
643+
('tid', 'tuple id block value out of uint32 range', [
642644
(-1, 0),
643645
(2**256, 0),
644646
(0xffffffff + 1, 0),
645647
(2**32, 0),
646648
]),
647-
('tid', OverflowError, 'tuple id offset value out of range', [
649+
('tid', 'tuple id offset value out of uint16 range', [
648650
(0, -1),
649651
(0, 2**256),
650652
(0, 0xffff + 1),
651653
(0, 0xffffffff),
652654
(0, 65536),
653655
]),
654-
('oid', OverflowError, 'uint32 value out of range', [
656+
('oid', 'value out of uint32 range', [
655657
2 ** 32,
656658
-1,
657659
]),
658660
]
659661

660-
for typname, errcls, errmsg, data in cases:
662+
for typname, errmsg, data in cases:
661663
stmt = await self.con.prepare("SELECT $1::" + typname)
662664

663665
for sample in data:
664666
with self.subTest(sample=sample, typname=typname):
665-
with self.assertRaisesRegex(errcls, errmsg):
667+
full_errmsg = (
668+
r'invalid input for query argument \$1:.*' + errmsg)
669+
670+
with self.assertRaisesRegex(
671+
asyncpg.DataError, full_errmsg):
666672
await stmt.fetchval(sample)
667673

668674
async def test_arrays(self):
@@ -733,37 +739,39 @@ class SomeContainer:
733739
def __contains__(self, item):
734740
return False
735741

736-
with self.assertRaisesRegex(TypeError,
742+
with self.assertRaisesRegex(asyncpg.DataError,
737743
'sized iterable container expected'):
738744
result = await self.con.fetchval("SELECT $1::int[]",
739745
SomeContainer())
740746

741-
with self.assertRaisesRegex(ValueError, 'dimensions'):
747+
with self.assertRaisesRegex(asyncpg.DataError, 'dimensions'):
742748
await self.con.fetchval(
743749
"SELECT $1::int[]",
744750
[[[[[[[1]]]]]]])
745751

746-
with self.assertRaisesRegex(ValueError, 'non-homogeneous'):
752+
with self.assertRaisesRegex(asyncpg.DataError, 'non-homogeneous'):
747753
await self.con.fetchval(
748754
"SELECT $1::int[]",
749755
[1, [1]])
750756

751-
with self.assertRaisesRegex(ValueError, 'non-homogeneous'):
757+
with self.assertRaisesRegex(asyncpg.DataError, 'non-homogeneous'):
752758
await self.con.fetchval(
753759
"SELECT $1::int[]",
754760
[[1], 1, [2]])
755761

756-
with self.assertRaisesRegex(ValueError, 'invalid array element'):
762+
with self.assertRaisesRegex(asyncpg.DataError,
763+
'invalid array element'):
757764
await self.con.fetchval(
758765
"SELECT $1::int[]",
759766
[1, 't', 2])
760767

761-
with self.assertRaisesRegex(ValueError, 'invalid array element'):
768+
with self.assertRaisesRegex(asyncpg.DataError,
769+
'invalid array element'):
762770
await self.con.fetchval(
763771
"SELECT $1::int[]",
764772
[[1], ['t'], [2]])
765773

766-
with self.assertRaisesRegex(TypeError,
774+
with self.assertRaisesRegex(asyncpg.DataError,
767775
'sized iterable container expected'):
768776
await self.con.fetchval(
769777
"SELECT $1::int[]",
@@ -887,11 +895,11 @@ async def test_range_types(self):
887895
self.assertEqual(result, expected)
888896

889897
with self.assertRaisesRegex(
890-
TypeError, 'list, tuple or Range object expected'):
898+
asyncpg.DataError, 'list, tuple or Range object expected'):
891899
await self.con.fetch("SELECT $1::int4range", 'aa')
892900

893901
with self.assertRaisesRegex(
894-
ValueError, 'expected 0, 1 or 2 elements'):
902+
asyncpg.DataError, 'expected 0, 1 or 2 elements'):
895903
await self.con.fetch("SELECT $1::int4range", (0, 2, 3))
896904

897905
cases = [(asyncpg.Range(0, 1), asyncpg.Range(0, 1), 1),
@@ -933,7 +941,8 @@ async def test_extra_codec_alias(self):
933941

934942
self.assertEqual(res, {'foo': '2', 'bar': '3'})
935943

936-
with self.assertRaisesRegex(ValueError, 'null value not allowed'):
944+
with self.assertRaisesRegex(asyncpg.DataError,
945+
'null value not allowed'):
937946
await self.con.fetchval('''
938947
SELECT $1::hstore AS result
939948
''', {None: '1'})

0 commit comments

Comments
 (0)