Skip to content

Commit aea75f5

Browse files
committed
Propagate errors from callbacks correctly; Fix upgrade (#3)
1 parent b3a86dd commit aea75f5

File tree

2 files changed

+257
-19
lines changed

2 files changed

+257
-19
lines changed

httptools/parser/parser.pyx

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ cdef class HttpParser:
3434
_proto_on_message_complete, _proto_on_chunk_header, \
3535
_proto_on_chunk_complete, _proto_on_message_begin
3636

37+
object _last_error
38+
3739
Py_buffer py_buf
3840

3941
def __cinit__(self):
@@ -90,6 +92,8 @@ cdef class HttpParser:
9092
protocol, 'on_chunk_complete', None)
9193
self._csettings.on_chunk_complete = cb_on_chunk_complete
9294

95+
self._last_error = None
96+
9397
cdef _maybe_call_on_header(self):
9498
if self._current_header_name is not None:
9599
current_header_name = self._current_header_name
@@ -157,14 +161,18 @@ cdef class HttpParser:
157161

158162
PyBuffer_Release(&self.py_buf)
159163

164+
if self._cparser.http_errno != cparser.HPE_OK:
165+
ex = parser_error_from_errno(
166+
<cparser.http_errno> self._cparser.http_errno)
167+
if isinstance(ex, HttpParserCallbackError):
168+
if self._last_error is not None:
169+
ex.__context__ = self._last_error
170+
self._last_error = None
171+
raise ex
172+
160173
if self._cparser.upgrade:
161174
raise HttpParserUpgrade(nb)
162175

163-
# TODO: Handle parser->upgrade
164-
165-
if self._cparser.http_errno != cparser.HPE_OK:
166-
raise parser_error_from_errno(
167-
<cparser.http_errno> self._cparser.http_errno)
168176
if nb != data_len:
169177
raise HttpParserError('not all of the data was parsed')
170178

@@ -199,57 +207,120 @@ cdef class HttpResponseParser(HttpParser):
199207

200208
cdef int cb_on_message_begin(cparser.http_parser* parser) except -1:
201209
cdef HttpParser pyparser = <HttpParser>parser.data
202-
pyparser._proto_on_message_begin()
210+
try:
211+
pyparser._proto_on_message_begin()
212+
except BaseException as ex:
213+
pyparser._last_error = ex
214+
return -1
215+
else:
216+
return 0
203217

204218

205219
cdef int cb_on_url(cparser.http_parser* parser,
206220
const char *at, size_t length) except -1:
207221
cdef HttpParser pyparser = <HttpParser>parser.data
208-
pyparser._proto_on_url(at[:length])
222+
try:
223+
pyparser._proto_on_url(at[:length])
224+
except BaseException as ex:
225+
pyparser._last_error = ex
226+
return -1
227+
else:
228+
return 0
209229

210230

211231
cdef int cb_on_status(cparser.http_parser* parser,
212232
const char *at, size_t length) except -1:
213233
cdef HttpParser pyparser = <HttpParser>parser.data
214-
pyparser._proto_on_status(at[:length])
234+
try:
235+
pyparser._proto_on_status(at[:length])
236+
except BaseException as ex:
237+
pyparser._last_error = ex
238+
return -1
239+
else:
240+
return 0
215241

216242

217243
cdef int cb_on_header_field(cparser.http_parser* parser,
218244
const char *at, size_t length) except -1:
219245
cdef HttpParser pyparser = <HttpParser>parser.data
220-
pyparser._on_header_field(at[:length])
246+
try:
247+
pyparser._on_header_field(at[:length])
248+
except BaseException as ex:
249+
pyparser._last_error = ex
250+
return -1
251+
else:
252+
return 0
221253

222254

223255
cdef int cb_on_header_value(cparser.http_parser* parser,
224256
const char *at, size_t length) except -1:
225257
cdef HttpParser pyparser = <HttpParser>parser.data
226-
pyparser._on_header_value(at[:length])
258+
try:
259+
pyparser._on_header_value(at[:length])
260+
except BaseException as ex:
261+
pyparser._last_error = ex
262+
return -1
263+
else:
264+
return 0
227265

228266

229267
cdef int cb_on_headers_complete(cparser.http_parser* parser) except -1:
230268
cdef HttpParser pyparser = <HttpParser>parser.data
231-
pyparser._on_headers_complete()
269+
try:
270+
pyparser._on_headers_complete()
271+
except BaseException as ex:
272+
pyparser._last_error = ex
273+
return -1
274+
else:
275+
if pyparser._cparser.upgrade:
276+
return 1
277+
else:
278+
return 0
232279

233280

234281
cdef int cb_on_body(cparser.http_parser* parser,
235282
const char *at, size_t length) except -1:
236283
cdef HttpParser pyparser = <HttpParser>parser.data
237-
pyparser._proto_on_body(at[:length])
284+
try:
285+
pyparser._proto_on_body(at[:length])
286+
except BaseException as ex:
287+
pyparser._last_error = ex
288+
return -1
289+
else:
290+
return 0
238291

239292

240293
cdef int cb_on_message_complete(cparser.http_parser* parser) except -1:
241294
cdef HttpParser pyparser = <HttpParser>parser.data
242-
pyparser._proto_on_message_complete()
295+
try:
296+
pyparser._proto_on_message_complete()
297+
except BaseException as ex:
298+
pyparser._last_error = ex
299+
return -1
300+
else:
301+
return 0
243302

244303

245304
cdef int cb_on_chunk_header(cparser.http_parser* parser) except -1:
246305
cdef HttpParser pyparser = <HttpParser>parser.data
247-
pyparser._on_chunk_header()
306+
try:
307+
pyparser._on_chunk_header()
308+
except BaseException as ex:
309+
pyparser._last_error = ex
310+
return -1
311+
else:
312+
return 0
248313

249314

250315
cdef int cb_on_chunk_complete(cparser.http_parser* parser) except -1:
251316
cdef HttpParser pyparser = <HttpParser>parser.data
252-
pyparser._on_chunk_complete()
317+
try:
318+
pyparser._on_chunk_complete()
319+
except BaseException as ex:
320+
pyparser._last_error = ex
321+
return -1
322+
else:
323+
return 0
253324

254325

255326
cdef parser_error_from_errno(cparser.http_errno errno):

tests/test_parser.py

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@
5757
5858
Hot diggity dogg'''
5959

60+
UPGRADE_RESPONSE1 = b'''HTTP/1.1 101 Switching Protocols
61+
UPGRADE: websocket
62+
SEC-WEBSOCKET-ACCEPT: rVg+XakFNFOxk3ZH0lzrZBmg0aU=
63+
TRANSFER-ENCODING: chunked
64+
CONNECTION: upgrade
65+
DATE: Sat, 07 May 2016 23:44:32 GMT
66+
SERVER: Python/3.4 aiohttp/1.0.3
67+
68+
data'''.replace(b'\n', b'\r\n')
69+
6070

6171
class TestResponseParser(unittest.TestCase):
6272

@@ -67,7 +77,7 @@ def test_parser_response_1(self):
6777
m.on_header.side_effect = headers.__setitem__
6878

6979
p = httptools.HttpResponseParser(m)
70-
p.feed_data(RESPONSE1_HEAD)
80+
p.feed_data(memoryview(RESPONSE1_HEAD))
7181

7282
self.assertEqual(p.get_http_version(), '1.1')
7383
self.assertEqual(p.get_status_code(), 200)
@@ -92,9 +102,8 @@ def test_parser_response_1(self):
92102
self.assertFalse(m.on_chunk_complete.called)
93103

94104
with self.assertRaisesRegex(
95-
httptools.HttpParserError,
96-
'data received after completed connection'):
97-
105+
httptools.HttpParserError,
106+
'data received after completed connection'):
98107
p.feed_data(b'12123123')
99108

100109
def test_parser_response_2(self):
@@ -136,6 +145,78 @@ def test_parser_response_5(self):
136145

137146
m.on_message_complete.assert_called_once_with()
138147

148+
def test_parser_response_cb_on_status_1(self):
149+
class Error(Exception):
150+
pass
151+
152+
m = mock.Mock()
153+
m.on_status.side_effect = Error()
154+
155+
p = httptools.HttpResponseParser(m)
156+
try:
157+
p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY)
158+
except httptools.HttpParserCallbackError as ex:
159+
self.assertIsInstance(ex.__context__, Error)
160+
else:
161+
self.fail('HttpParserCallbackError was not raised')
162+
163+
def test_parser_response_cb_on_body_1(self):
164+
class Error(Exception):
165+
pass
166+
167+
m = mock.Mock()
168+
m.on_body.side_effect = Error()
169+
170+
p = httptools.HttpResponseParser(m)
171+
try:
172+
p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY)
173+
except httptools.HttpParserCallbackError as ex:
174+
self.assertIsInstance(ex.__context__, Error)
175+
else:
176+
self.fail('HttpParserCallbackError was not raised')
177+
178+
def test_parser_response_cb_on_message_complete_1(self):
179+
class Error(Exception):
180+
pass
181+
182+
m = mock.Mock()
183+
m.on_message_complete.side_effect = Error()
184+
185+
p = httptools.HttpResponseParser(m)
186+
try:
187+
p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY)
188+
except httptools.HttpParserCallbackError as ex:
189+
self.assertIsInstance(ex.__context__, Error)
190+
else:
191+
self.fail('HttpParserCallbackError was not raised')
192+
193+
def test_parser_upgrade_response_1(self):
194+
m = mock.Mock()
195+
196+
headers = {}
197+
m.on_header.side_effect = headers.__setitem__
198+
199+
p = httptools.HttpResponseParser(m)
200+
try:
201+
p.feed_data(UPGRADE_RESPONSE1)
202+
except httptools.HttpParserUpgrade as ex:
203+
offset = ex.args[0]
204+
else:
205+
self.fail('HttpParserUpgrade was not raised')
206+
207+
self.assertEqual(UPGRADE_RESPONSE1[offset:], b'data')
208+
209+
self.assertEqual(p.get_http_version(), '1.1')
210+
self.assertEqual(p.get_status_code(), 101)
211+
212+
m.on_status.assert_called_once_with(b'Switching Protocols')
213+
214+
m.on_headers_complete.assert_called_once_with()
215+
self.assertEqual(m.on_header.call_count, 6)
216+
self.assertEqual(len(headers), 6)
217+
218+
m.on_message_complete.assert_called_once_with()
219+
139220

140221
class TestRequestParser(unittest.TestCase):
141222

@@ -194,6 +275,36 @@ def test_parser_request_chunked_2(self):
194275
b'Host': b'bar',
195276
b'Vary': b'*'})
196277

278+
def test_parser_request_chunked_cb_error_1(self):
279+
class Error(Exception):
280+
pass
281+
282+
m = mock.Mock()
283+
m.on_chunk_header.side_effect = Error()
284+
285+
p = httptools.HttpRequestParser(m)
286+
try:
287+
p.feed_data(CHUNKED_REQUEST1_1)
288+
except httptools.HttpParserCallbackError as ex:
289+
self.assertIsInstance(ex.__context__, Error)
290+
else:
291+
self.fail('HttpParserCallbackError was not raised')
292+
293+
def test_parser_request_chunked_cb_error_2(self):
294+
class Error(Exception):
295+
pass
296+
297+
m = mock.Mock()
298+
m.on_chunk_complete.side_effect = Error()
299+
300+
p = httptools.HttpRequestParser(m)
301+
try:
302+
p.feed_data(CHUNKED_REQUEST1_1)
303+
except httptools.HttpParserCallbackError as ex:
304+
self.assertIsInstance(ex.__context__, Error)
305+
else:
306+
self.fail('HttpParserCallbackError was not raised')
307+
197308
def test_parser_request_chunked_3(self):
198309
m = mock.Mock()
199310
p = httptools.HttpRequestParser(m)
@@ -237,6 +348,62 @@ def test_parser_request_upgrade_1(self):
237348
b'Host': b'example.com',
238349
b'Upgrade': b'WebSocket'})
239350

351+
def test_parser_request_error_in_on_header(self):
352+
class Error(Exception):
353+
pass
354+
m = mock.Mock()
355+
m.on_header.side_effect = Error()
356+
p = httptools.HttpRequestParser(m)
357+
358+
try:
359+
p.feed_data(UPGRADE_REQUEST1)
360+
except httptools.HttpParserCallbackError as ex:
361+
self.assertIsInstance(ex.__context__, Error)
362+
else:
363+
self.fail('HttpParserCallbackError was not raised')
364+
365+
def test_parser_request_error_in_on_message_begin(self):
366+
class Error(Exception):
367+
pass
368+
m = mock.Mock()
369+
m.on_message_begin.side_effect = Error()
370+
p = httptools.HttpRequestParser(m)
371+
372+
try:
373+
p.feed_data(UPGRADE_REQUEST1)
374+
except httptools.HttpParserCallbackError as ex:
375+
self.assertIsInstance(ex.__context__, Error)
376+
else:
377+
self.fail('HttpParserCallbackError was not raised')
378+
379+
def test_parser_request_error_in_cb_on_url(self):
380+
class Error(Exception):
381+
pass
382+
m = mock.Mock()
383+
m.on_url.side_effect = Error()
384+
p = httptools.HttpRequestParser(m)
385+
386+
try:
387+
p.feed_data(UPGRADE_REQUEST1)
388+
except httptools.HttpParserCallbackError as ex:
389+
self.assertIsInstance(ex.__context__, Error)
390+
else:
391+
self.fail('HttpParserCallbackError was not raised')
392+
393+
def test_parser_request_error_in_cb_on_headers_complete(self):
394+
class Error(Exception):
395+
pass
396+
m = mock.Mock()
397+
m.on_headers_complete.side_effect = Error()
398+
p = httptools.HttpRequestParser(m)
399+
400+
try:
401+
p.feed_data(UPGRADE_REQUEST1)
402+
except httptools.HttpParserCallbackError as ex:
403+
self.assertIsInstance(ex.__context__, Error)
404+
else:
405+
self.fail('HttpParserCallbackError was not raised')
406+
240407
def test_parser_request_2(self):
241408
p = httptools.HttpRequestParser(None)
242409
with self.assertRaises(httptools.HttpParserInvalidMethodError):

0 commit comments

Comments
 (0)