Skip to content

Commit b2edf89

Browse files
authored
Merge pull request #2853 from minrk/api-403
Update error handling on APIHandlers
2 parents 92354d5 + 4467dc9 commit b2edf89

File tree

10 files changed

+45
-66
lines changed

10 files changed

+45
-66
lines changed

notebook/base/handlers.py

+36-25
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import re
1111
import sys
1212
import traceback
13+
import types
14+
import warnings
1315
try:
1416
# py3
1517
from http.client import responses
@@ -450,6 +452,34 @@ def prepare(self):
450452
raise web.HTTPError(404)
451453
return super(APIHandler, self).prepare()
452454

455+
def write_error(self, status_code, **kwargs):
456+
"""APIHandler errors are JSON, not human pages"""
457+
self.set_header('Content-Type', 'application/json')
458+
message = responses.get(status_code, 'Unknown HTTP Error')
459+
reply = {
460+
'message': message,
461+
}
462+
exc_info = kwargs.get('exc_info')
463+
if exc_info:
464+
e = exc_info[1]
465+
if isinstance(e, HTTPError):
466+
reply['message'] = e.log_message or message
467+
else:
468+
reply['message'] = 'Unhandled error'
469+
reply['traceback'] = ''.join(traceback.format_exception(*exc_info))
470+
self.log.warning(reply['message'])
471+
self.finish(json.dumps(reply))
472+
473+
def get_current_user(self):
474+
"""Raise 403 on API handlers instead of redirecting to human login page"""
475+
# preserve _user_cache so we don't raise more than once
476+
if hasattr(self, '_user_cache'):
477+
return self._user_cache
478+
self._user_cache = user = super(APIHandler, self).get_current_user()
479+
if user is None:
480+
raise web.HTTPError(403)
481+
return user
482+
453483
@property
454484
def content_security_policy(self):
455485
csp = '; '.join([
@@ -547,32 +577,14 @@ def json_errors(method):
547577
2. Create and return a JSON body with a message field describing
548578
the error in a human readable form.
549579
"""
580+
warnings.warn('@json_errors is deprecated in notebook 5.2.0. Subclass APIHandler instead.',
581+
DeprecationWarning,
582+
stacklevel=2,
583+
)
550584
@functools.wraps(method)
551-
@gen.coroutine
552585
def wrapper(self, *args, **kwargs):
553-
try:
554-
result = yield gen.maybe_future(method(self, *args, **kwargs))
555-
except web.HTTPError as e:
556-
self.set_header('Content-Type', 'application/json')
557-
status = e.status_code
558-
message = e.log_message
559-
self.log.warning(message)
560-
self.set_status(e.status_code)
561-
reply = dict(message=message, reason=e.reason)
562-
self.finish(json.dumps(reply))
563-
except Exception:
564-
self.set_header('Content-Type', 'application/json')
565-
self.log.error("Unhandled error in API request", exc_info=True)
566-
status = 500
567-
message = "Unknown server error"
568-
t, value, tb = sys.exc_info()
569-
self.set_status(status)
570-
tb_text = ''.join(traceback.format_exception(t, value, tb))
571-
reply = dict(message=message, reason=None, traceback=tb_text)
572-
self.finish(json.dumps(reply))
573-
else:
574-
# FIXME: can use regular return in generators in py3
575-
raise gen.Return(result)
586+
self.write_error = types.MethodType(APIHandler.write_error, self)
587+
return method(self, *args, **kwargs)
576588
return wrapper
577589

578590

@@ -643,7 +655,6 @@ def validate_absolute_path(self, root, absolute_path):
643655

644656
class APIVersionHandler(APIHandler):
645657

646-
@json_errors
647658
def get(self):
648659
# not authenticated, so give as few info as possible
649660
self.finish(json.dumps({"version":notebook.__version__}))

notebook/services/api/handlers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from tornado import gen, web
1010

11-
from ...base.handlers import IPythonHandler, APIHandler, json_errors
11+
from ...base.handlers import IPythonHandler, APIHandler
1212
from notebook._tz import utcfromtimestamp, isoformat
1313

1414
import os
@@ -28,7 +28,6 @@ class APIStatusHandler(APIHandler):
2828

2929
_track_activity = False
3030

31-
@json_errors
3231
@web.authenticated
3332
@gen.coroutine
3433
def get(self):

notebook/services/config/handlers.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,21 @@
99
from tornado import web
1010

1111
from ipython_genutils.py3compat import PY3
12-
from ...base.handlers import APIHandler, json_errors
12+
from ...base.handlers import APIHandler
1313

1414
class ConfigHandler(APIHandler):
1515

16-
@json_errors
1716
@web.authenticated
1817
def get(self, section_name):
1918
self.set_header("Content-Type", 'application/json')
2019
self.finish(json.dumps(self.config_manager.get(section_name)))
2120

22-
@json_errors
2321
@web.authenticated
2422
def put(self, section_name):
2523
data = self.get_json_body() # Will raise 400 if content is not valid JSON
2624
self.config_manager.set(section_name, data)
2725
self.set_status(204)
2826

29-
@json_errors
3027
@web.authenticated
3128
def patch(self, section_name):
3229
new_data = self.get_json_body()

notebook/services/contents/handlers.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from jupyter_client.jsonutil import date_default
1515

1616
from notebook.base.handlers import (
17-
IPythonHandler, APIHandler, json_errors, path_regex,
17+
IPythonHandler, APIHandler, path_regex,
1818
)
1919

2020

@@ -87,7 +87,6 @@ def _finish_model(self, model, location=True):
8787
self.set_header('Content-Type', 'application/json')
8888
self.finish(json.dumps(model, default=date_default))
8989

90-
@json_errors
9190
@web.authenticated
9291
@gen.coroutine
9392
def get(self, path=''):
@@ -115,7 +114,6 @@ def get(self, path=''):
115114
validate_model(model, expect_content=content)
116115
self._finish_model(model, location=False)
117116

118-
@json_errors
119117
@web.authenticated
120118
@gen.coroutine
121119
def patch(self, path=''):
@@ -168,7 +166,6 @@ def _save(self, model, path):
168166
validate_model(model, expect_content=False)
169167
self._finish_model(model)
170168

171-
@json_errors
172169
@web.authenticated
173170
@gen.coroutine
174171
def post(self, path=''):
@@ -204,7 +201,6 @@ def post(self, path=''):
204201
else:
205202
yield self._new_untitled(path)
206203

207-
@json_errors
208204
@web.authenticated
209205
@gen.coroutine
210206
def put(self, path=''):
@@ -230,7 +226,6 @@ def put(self, path=''):
230226
else:
231227
yield gen.maybe_future(self._new_untitled(path))
232228

233-
@json_errors
234229
@web.authenticated
235230
@gen.coroutine
236231
def delete(self, path=''):
@@ -244,7 +239,6 @@ def delete(self, path=''):
244239

245240
class CheckpointsHandler(APIHandler):
246241

247-
@json_errors
248242
@web.authenticated
249243
@gen.coroutine
250244
def get(self, path=''):
@@ -254,7 +248,6 @@ def get(self, path=''):
254248
data = json.dumps(checkpoints, default=date_default)
255249
self.finish(data)
256250

257-
@json_errors
258251
@web.authenticated
259252
@gen.coroutine
260253
def post(self, path=''):
@@ -271,7 +264,6 @@ def post(self, path=''):
271264

272265
class ModifyCheckpointsHandler(APIHandler):
273266

274-
@json_errors
275267
@web.authenticated
276268
@gen.coroutine
277269
def post(self, path, checkpoint_id):
@@ -281,7 +273,6 @@ def post(self, path, checkpoint_id):
281273
self.set_status(204)
282274
self.finish()
283275

284-
@json_errors
285276
@web.authenticated
286277
@gen.coroutine
287278
def delete(self, path, checkpoint_id):
@@ -310,7 +301,6 @@ def get(self, path):
310301
class TrustNotebooksHandler(IPythonHandler):
311302
""" Handles trust/signing of notebooks """
312303

313-
@json_errors
314304
@web.authenticated
315305
@gen.coroutine
316306
def post(self,path=''):

notebook/services/kernels/handlers.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,20 @@
1818
from ipython_genutils.py3compat import cast_unicode
1919
from notebook.utils import url_path_join, url_escape
2020

21-
from ...base.handlers import APIHandler, json_errors
21+
from ...base.handlers import APIHandler
2222
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
2323

2424
from jupyter_client import protocol_version as client_protocol_version
2525

2626
class MainKernelHandler(APIHandler):
2727

28-
@json_errors
2928
@web.authenticated
3029
@gen.coroutine
3130
def get(self):
3231
km = self.kernel_manager
3332
kernels = yield gen.maybe_future(km.list_kernels())
3433
self.finish(json.dumps(kernels, default=date_default))
3534

36-
@json_errors
3735
@web.authenticated
3836
@gen.coroutine
3937
def post(self):
@@ -56,15 +54,13 @@ def post(self):
5654

5755
class KernelHandler(APIHandler):
5856

59-
@json_errors
6057
@web.authenticated
6158
def get(self, kernel_id):
6259
km = self.kernel_manager
6360
km._check_kernel_id(kernel_id)
6461
model = km.kernel_model(kernel_id)
6562
self.finish(json.dumps(model, default=date_default))
6663

67-
@json_errors
6864
@web.authenticated
6965
@gen.coroutine
7066
def delete(self, kernel_id):
@@ -76,7 +72,6 @@ def delete(self, kernel_id):
7672

7773
class KernelActionHandler(APIHandler):
7874

79-
@json_errors
8075
@web.authenticated
8176
@gen.coroutine
8277
def post(self, kernel_id, action):

notebook/services/kernelspecs/handlers.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from tornado import web
1515

16-
from ...base.handlers import APIHandler, json_errors
16+
from ...base.handlers import APIHandler
1717
from ...utils import url_path_join, url_unescape
1818

1919
def kernelspec_model(handler, name):
@@ -45,7 +45,6 @@ def kernelspec_model(handler, name):
4545

4646
class MainKernelSpecHandler(APIHandler):
4747

48-
@json_errors
4948
@web.authenticated
5049
def get(self):
5150
ksm = self.kernel_spec_manager
@@ -66,7 +65,6 @@ def get(self):
6665

6766
class KernelSpecHandler(APIHandler):
6867

69-
@json_errors
7068
@web.authenticated
7169
def get(self, kernel_name):
7270
try:

notebook/services/nbconvert/handlers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
from tornado import web
44

5-
from ...base.handlers import APIHandler, json_errors
5+
from ...base.handlers import APIHandler
66

77
class NbconvertRootHandler(APIHandler):
88

9-
@json_errors
109
@web.authenticated
1110
def get(self):
1211
try:

notebook/services/security/handlers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from tornado import web
77

8-
from ...base.handlers import APIHandler, json_errors
8+
from ...base.handlers import APIHandler
99
from . import csp_report_uri
1010

1111
class CSPReportHandler(APIHandler):
@@ -21,7 +21,6 @@ def check_xsrf_cookie(self):
2121
# don't check XSRF for CSP reports
2222
return
2323

24-
@json_errors
2524
@web.authenticated
2625
def post(self):
2726
'''Log a content security policy violation report'''

notebook/services/sessions/handlers.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111

1212
from tornado import gen, web
1313

14-
from ...base.handlers import APIHandler, json_errors
14+
from ...base.handlers import APIHandler
1515
from jupyter_client.jsonutil import date_default
1616
from notebook.utils import url_path_join
1717
from jupyter_client.kernelspec import NoSuchKernel
1818

1919

2020
class SessionRootHandler(APIHandler):
2121

22-
@json_errors
2322
@web.authenticated
2423
@gen.coroutine
2524
def get(self):
@@ -28,7 +27,6 @@ def get(self):
2827
sessions = yield gen.maybe_future(sm.list_sessions())
2928
self.finish(json.dumps(sessions, default=date_default))
3029

31-
@json_errors
3230
@web.authenticated
3331
@gen.coroutine
3432
def post(self):
@@ -90,7 +88,6 @@ def post(self):
9088

9189
class SessionHandler(APIHandler):
9290

93-
@json_errors
9491
@web.authenticated
9592
@gen.coroutine
9693
def get(self, session_id):
@@ -99,7 +96,6 @@ def get(self, session_id):
9996
model = yield gen.maybe_future(sm.get_session(session_id=session_id))
10097
self.finish(json.dumps(model, default=date_default))
10198

102-
@json_errors
10399
@web.authenticated
104100
@gen.coroutine
105101
def patch(self, session_id):
@@ -153,7 +149,6 @@ def patch(self, session_id):
153149
)
154150
self.finish(json.dumps(model, default=date_default))
155151

156-
@json_errors
157152
@web.authenticated
158153
@gen.coroutine
159154
def delete(self, session_id):

0 commit comments

Comments
 (0)