##// END OF EJS Templates
error-page: use custom causes for 500+ errors
marcink -
r2116:91c04731 default
parent child Browse files
Show More
@@ -1,545 +1,551 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 import traceback
26 26 from collections import OrderedDict
27 27
28 28 from paste.registry import RegistryManager
29 29 from paste.gzipper import make_gzip_middleware
30 30 from pylons.wsgiapp import PylonsApp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.wsgi import wsgiapp
35 35 from pyramid.httpexceptions import (
36 36 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
37 37 from pyramid.events import ApplicationCreated
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import rhodecode
41 41
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config import utils as config_utils
45 45 from rhodecode.config.routing import STATIC_FILE_PREFIX
46 46 from rhodecode.config.environment import (
47 47 load_environment, load_pyramid_environment)
48 48
49 49 from rhodecode.lib.vcs import VCSCommunicationError
50 50 from rhodecode.lib.exceptions import VCSServerUnavailable
51 51 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
52 52 from rhodecode.lib.middleware.error_handling import (
53 53 PylonsErrorHandlingMiddleware)
54 54 from rhodecode.lib.middleware.https_fixup import HttpsFixup
55 55 from rhodecode.lib.middleware.vcs import VCSMiddleware
56 56 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
57 57 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
58 58 from rhodecode.subscribers import (
59 59 scan_repositories_if_enabled, write_js_routes_if_enabled,
60 60 write_metadata_if_needed, inject_app_settings)
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # this is used to avoid avoid the route lookup overhead in routesmiddleware
67 67 # for certain routes which won't go to pylons to - eg. static files, debugger
68 68 # it is only needed for the pylons migration and can be removed once complete
69 69 class SkippableRoutesMiddleware(RoutesMiddleware):
70 70 """ Routes middleware that allows you to skip prefixes """
71 71
72 72 def __init__(self, *args, **kw):
73 73 self.skip_prefixes = kw.pop('skip_prefixes', [])
74 74 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
75 75
76 76 def __call__(self, environ, start_response):
77 77 for prefix in self.skip_prefixes:
78 78 if environ['PATH_INFO'].startswith(prefix):
79 79 # added to avoid the case when a missing /_static route falls
80 80 # through to pylons and causes an exception as pylons is
81 81 # expecting wsgiorg.routingargs to be set in the environ
82 82 # by RoutesMiddleware.
83 83 if 'wsgiorg.routing_args' not in environ:
84 84 environ['wsgiorg.routing_args'] = (None, {})
85 85 return self.app(environ, start_response)
86 86
87 87 return super(SkippableRoutesMiddleware, self).__call__(
88 88 environ, start_response)
89 89
90 90
91 91 def make_app(global_conf, static_files=True, **app_conf):
92 92 """Create a Pylons WSGI application and return it
93 93
94 94 ``global_conf``
95 95 The inherited configuration for this application. Normally from
96 96 the [DEFAULT] section of the Paste ini file.
97 97
98 98 ``app_conf``
99 99 The application's local configuration. Normally specified in
100 100 the [app:<name>] section of the Paste ini file (where <name>
101 101 defaults to main).
102 102
103 103 """
104 104 # Apply compatibility patches
105 105 patches.kombu_1_5_1_python_2_7_11()
106 106 patches.inspect_getargspec()
107 107
108 108 # Configure the Pylons environment
109 109 config = load_environment(global_conf, app_conf)
110 110
111 111 # The Pylons WSGI app
112 112 app = PylonsApp(config=config)
113 113
114 114 # Establish the Registry for this application
115 115 app = RegistryManager(app)
116 116
117 117 app.config = config
118 118
119 119 return app
120 120
121 121
122 122 def make_pyramid_app(global_config, **settings):
123 123 """
124 124 Constructs the WSGI application based on Pyramid and wraps the Pylons based
125 125 application.
126 126
127 127 Specials:
128 128
129 129 * We migrate from Pylons to Pyramid. While doing this, we keep both
130 130 frameworks functional. This involves moving some WSGI middlewares around
131 131 and providing access to some data internals, so that the old code is
132 132 still functional.
133 133
134 134 * The application can also be integrated like a plugin via the call to
135 135 `includeme`. This is accompanied with the other utility functions which
136 136 are called. Changing this should be done with great care to not break
137 137 cases when these fragments are assembled from another place.
138 138
139 139 """
140 140 # The edition string should be available in pylons too, so we add it here
141 141 # before copying the settings.
142 142 settings.setdefault('rhodecode.edition', 'Community Edition')
143 143
144 144 # As long as our Pylons application does expect "unprepared" settings, make
145 145 # sure that we keep an unmodified copy. This avoids unintentional change of
146 146 # behavior in the old application.
147 147 settings_pylons = settings.copy()
148 148
149 149 sanitize_settings_and_apply_defaults(settings)
150 150
151 151 config = Configurator(settings=settings)
152 152 load_pyramid_environment(global_config, settings)
153 153
154 154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
155 155
156 156 includeme_first(config)
157 157 includeme(config)
158 158
159 159 pyramid_app = config.make_wsgi_app()
160 160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
161 161 pyramid_app.config = config
162 162
163 163 # creating the app uses a connection - return it after we are done
164 164 meta.Session.remove()
165 165
166 166 return pyramid_app
167 167
168 168
169 169 def make_not_found_view(config):
170 170 """
171 171 This creates the view which should be registered as not-found-view to
172 172 pyramid. Basically it contains of the old pylons app, converted to a view.
173 173 Additionally it is wrapped by some other middlewares.
174 174 """
175 175 settings = config.registry.settings
176 176 vcs_server_enabled = settings['vcs.server.enable']
177 177
178 178 # Make pylons app from unprepared settings.
179 179 pylons_app = make_app(
180 180 config.registry._pylons_compat_global_config,
181 181 **config.registry._pylons_compat_settings)
182 182 config.registry._pylons_compat_config = pylons_app.config
183 183
184 184 # Appenlight monitoring.
185 185 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
186 186 pylons_app, settings)
187 187
188 188 # The pylons app is executed inside of the pyramid 404 exception handler.
189 189 # Exceptions which are raised inside of it are not handled by pyramid
190 190 # again. Therefore we add a middleware that invokes the error handler in
191 191 # case of an exception or error response. This way we return proper error
192 192 # HTML pages in case of an error.
193 193 reraise = (settings.get('debugtoolbar.enabled', False) or
194 194 rhodecode.disable_error_handler)
195 195 pylons_app = PylonsErrorHandlingMiddleware(
196 196 pylons_app, error_handler, reraise)
197 197
198 198 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
199 199 # view to handle the request. Therefore it is wrapped around the pylons
200 200 # app. It has to be outside of the error handling otherwise error responses
201 201 # from the vcsserver are converted to HTML error pages. This confuses the
202 202 # command line tools and the user won't get a meaningful error message.
203 203 if vcs_server_enabled:
204 204 pylons_app = VCSMiddleware(
205 205 pylons_app, settings, appenlight_client, registry=config.registry)
206 206
207 207 # Convert WSGI app to pyramid view and return it.
208 208 return wsgiapp(pylons_app)
209 209
210 210
211 211 def add_pylons_compat_data(registry, global_config, settings):
212 212 """
213 213 Attach data to the registry to support the Pylons integration.
214 214 """
215 215 registry._pylons_compat_global_config = global_config
216 216 registry._pylons_compat_settings = settings
217 217
218 218
219 219 def error_handler(exception, request):
220 220 import rhodecode
221 221 from rhodecode.lib import helpers
222 222
223 223 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
224 224
225 225 base_response = HTTPInternalServerError()
226 226 # prefer original exception for the response since it may have headers set
227 227 if isinstance(exception, HTTPException):
228 228 base_response = exception
229 229 elif isinstance(exception, VCSCommunicationError):
230 230 base_response = VCSServerUnavailable()
231 231
232 232 def is_http_error(response):
233 233 # error which should have traceback
234 234 return response.status_code > 499
235 235
236 236 if is_http_error(base_response):
237 237 log.exception(
238 238 'error occurred handling this request for path: %s', request.path)
239 239
240 240 error_explanation = base_response.explanation or str(base_response)
241 241 if base_response.status_code == 404:
242 242 error_explanation += " Or you don't have permission to access it."
243 243 c = AttributeDict()
244 244 c.error_message = base_response.status
245 245 c.error_explanation = error_explanation
246 246 c.visual = AttributeDict()
247 247
248 248 c.visual.rhodecode_support_url = (
249 249 request.registry.settings.get('rhodecode_support_url') or
250 250 request.route_url('rhodecode_support')
251 251 )
252 252 c.redirect_time = 0
253 253 c.rhodecode_name = rhodecode_title
254 254 if not c.rhodecode_name:
255 255 c.rhodecode_name = 'Rhodecode'
256 256
257 257 c.causes = []
258 if is_http_error(base_response):
259 c.causes.append('Server is overloaded.')
260 c.causes.append('Server database connection is lost.')
261 c.causes.append('Server expected unhandled error.')
262
258 263 if hasattr(base_response, 'causes'):
259 264 c.causes = base_response.causes
265
260 266 c.messages = helpers.flash.pop_messages(request=request)
261 267 c.traceback = traceback.format_exc()
262 268 response = render_to_response(
263 269 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
264 270 response=base_response)
265 271
266 272 return response
267 273
268 274
269 275 def includeme(config):
270 276 settings = config.registry.settings
271 277
272 278 # plugin information
273 279 config.registry.rhodecode_plugins = OrderedDict()
274 280
275 281 config.add_directive(
276 282 'register_rhodecode_plugin', register_rhodecode_plugin)
277 283
278 284 if asbool(settings.get('appenlight', 'false')):
279 285 config.include('appenlight_client.ext.pyramid_tween')
280 286
281 287 if 'mako.default_filters' not in settings:
282 288 # set custom default filters if we don't have it defined
283 289 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
284 290 settings['mako.default_filters'] = 'h_filter'
285 291
286 292 # Includes which are required. The application would fail without them.
287 293 config.include('pyramid_mako')
288 294 config.include('pyramid_beaker')
289 295
290 296 config.include('rhodecode.authentication')
291 297 config.include('rhodecode.integrations')
292 298
293 299 # apps
294 300 config.include('rhodecode.apps._base')
295 301 config.include('rhodecode.apps.ops')
296 302
297 303 config.include('rhodecode.apps.admin')
298 304 config.include('rhodecode.apps.channelstream')
299 305 config.include('rhodecode.apps.login')
300 306 config.include('rhodecode.apps.home')
301 307 config.include('rhodecode.apps.journal')
302 308 config.include('rhodecode.apps.repository')
303 309 config.include('rhodecode.apps.repo_group')
304 310 config.include('rhodecode.apps.user_group')
305 311 config.include('rhodecode.apps.search')
306 312 config.include('rhodecode.apps.user_profile')
307 313 config.include('rhodecode.apps.my_account')
308 314 config.include('rhodecode.apps.svn_support')
309 315 config.include('rhodecode.apps.ssh_support')
310 316 config.include('rhodecode.apps.gist')
311 317
312 318 config.include('rhodecode.apps.debug_style')
313 319 config.include('rhodecode.tweens')
314 320 config.include('rhodecode.api')
315 321
316 322 config.add_route(
317 323 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
318 324
319 325 config.add_translation_dirs('rhodecode:i18n/')
320 326 settings['default_locale_name'] = settings.get('lang', 'en')
321 327
322 328 # Add subscribers.
323 329 config.add_subscriber(inject_app_settings, ApplicationCreated)
324 330 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
325 331 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
326 332 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
327 333
328 334 config.add_request_method(
329 335 'rhodecode.lib.partial_renderer.get_partial_renderer',
330 336 'get_partial_renderer')
331 337
332 338 # events
333 339 # TODO(marcink): this should be done when pyramid migration is finished
334 340 # config.add_subscriber(
335 341 # 'rhodecode.integrations.integrations_event_handler',
336 342 # 'rhodecode.events.RhodecodeEvent')
337 343
338 344 # Set the authorization policy.
339 345 authz_policy = ACLAuthorizationPolicy()
340 346 config.set_authorization_policy(authz_policy)
341 347
342 348 # Set the default renderer for HTML templates to mako.
343 349 config.add_mako_renderer('.html')
344 350
345 351 config.add_renderer(
346 352 name='json_ext',
347 353 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
348 354
349 355 # include RhodeCode plugins
350 356 includes = aslist(settings.get('rhodecode.includes', []))
351 357 for inc in includes:
352 358 config.include(inc)
353 359
354 360 # This is the glue which allows us to migrate in chunks. By registering the
355 361 # pylons based application as the "Not Found" view in Pyramid, we will
356 362 # fallback to the old application each time the new one does not yet know
357 363 # how to handle a request.
358 364 config.add_notfound_view(make_not_found_view(config))
359 365
360 366 if not settings.get('debugtoolbar.enabled', False):
361 367 # disabled debugtoolbar handle all exceptions via the error_handlers
362 368 config.add_view(error_handler, context=Exception)
363 369
364 370 config.add_view(error_handler, context=HTTPError)
365 371
366 372
367 373 def includeme_first(config):
368 374 # redirect automatic browser favicon.ico requests to correct place
369 375 def favicon_redirect(context, request):
370 376 return HTTPFound(
371 377 request.static_path('rhodecode:public/images/favicon.ico'))
372 378
373 379 config.add_view(favicon_redirect, route_name='favicon')
374 380 config.add_route('favicon', '/favicon.ico')
375 381
376 382 def robots_redirect(context, request):
377 383 return HTTPFound(
378 384 request.static_path('rhodecode:public/robots.txt'))
379 385
380 386 config.add_view(robots_redirect, route_name='robots')
381 387 config.add_route('robots', '/robots.txt')
382 388
383 389 config.add_static_view(
384 390 '_static/deform', 'deform:static')
385 391 config.add_static_view(
386 392 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
387 393
388 394
389 395 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
390 396 """
391 397 Apply outer WSGI middlewares around the application.
392 398
393 399 Part of this has been moved up from the Pylons layer, so that the
394 400 data is also available if old Pylons code is hit through an already ported
395 401 view.
396 402 """
397 403 settings = config.registry.settings
398 404
399 405 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
400 406 pyramid_app = HttpsFixup(pyramid_app, settings)
401 407
402 408 # Add RoutesMiddleware to support the pylons compatibility tween during
403 409 # migration to pyramid.
404 410
405 411 # TODO(marcink): remove after migration to pyramid
406 412 if hasattr(config.registry, '_pylons_compat_config'):
407 413 routes_map = config.registry._pylons_compat_config['routes.map']
408 414 pyramid_app = SkippableRoutesMiddleware(
409 415 pyramid_app, routes_map,
410 416 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
411 417
412 418 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
413 419
414 420 if settings['gzip_responses']:
415 421 pyramid_app = make_gzip_middleware(
416 422 pyramid_app, settings, compress_level=1)
417 423
418 424 # this should be the outer most middleware in the wsgi stack since
419 425 # middleware like Routes make database calls
420 426 def pyramid_app_with_cleanup(environ, start_response):
421 427 try:
422 428 return pyramid_app(environ, start_response)
423 429 finally:
424 430 # Dispose current database session and rollback uncommitted
425 431 # transactions.
426 432 meta.Session.remove()
427 433
428 434 # In a single threaded mode server, on non sqlite db we should have
429 435 # '0 Current Checked out connections' at the end of a request,
430 436 # if not, then something, somewhere is leaving a connection open
431 437 pool = meta.Base.metadata.bind.engine.pool
432 438 log.debug('sa pool status: %s', pool.status())
433 439
434 440 return pyramid_app_with_cleanup
435 441
436 442
437 443 def sanitize_settings_and_apply_defaults(settings):
438 444 """
439 445 Applies settings defaults and does all type conversion.
440 446
441 447 We would move all settings parsing and preparation into this place, so that
442 448 we have only one place left which deals with this part. The remaining parts
443 449 of the application would start to rely fully on well prepared settings.
444 450
445 451 This piece would later be split up per topic to avoid a big fat monster
446 452 function.
447 453 """
448 454
449 455 # Pyramid's mako renderer has to search in the templates folder so that the
450 456 # old templates still work. Ported and new templates are expected to use
451 457 # real asset specifications for the includes.
452 458 mako_directories = settings.setdefault('mako.directories', [
453 459 # Base templates of the original Pylons application
454 460 'rhodecode:templates',
455 461 ])
456 462 log.debug(
457 463 "Using the following Mako template directories: %s",
458 464 mako_directories)
459 465
460 466 # Default includes, possible to change as a user
461 467 pyramid_includes = settings.setdefault('pyramid.includes', [
462 468 'rhodecode.lib.middleware.request_wrapper',
463 469 ])
464 470 log.debug(
465 471 "Using the following pyramid.includes: %s",
466 472 pyramid_includes)
467 473
468 474 # TODO: johbo: Re-think this, usually the call to config.include
469 475 # should allow to pass in a prefix.
470 476 settings.setdefault('rhodecode.api.url', '/_admin/api')
471 477
472 478 # Sanitize generic settings.
473 479 _list_setting(settings, 'default_encoding', 'UTF-8')
474 480 _bool_setting(settings, 'is_test', 'false')
475 481 _bool_setting(settings, 'gzip_responses', 'false')
476 482
477 483 # Call split out functions that sanitize settings for each topic.
478 484 _sanitize_appenlight_settings(settings)
479 485 _sanitize_vcs_settings(settings)
480 486
481 487 # configure instance id
482 488 config_utils.set_instance_id(settings)
483 489
484 490 return settings
485 491
486 492
487 493 def _sanitize_appenlight_settings(settings):
488 494 _bool_setting(settings, 'appenlight', 'false')
489 495
490 496
491 497 def _sanitize_vcs_settings(settings):
492 498 """
493 499 Applies settings defaults and does type conversion for all VCS related
494 500 settings.
495 501 """
496 502 _string_setting(settings, 'vcs.svn.compatible_version', '')
497 503 _string_setting(settings, 'git_rev_filter', '--all')
498 504 _string_setting(settings, 'vcs.hooks.protocol', 'http')
499 505 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
500 506 _string_setting(settings, 'vcs.server', '')
501 507 _string_setting(settings, 'vcs.server.log_level', 'debug')
502 508 _string_setting(settings, 'vcs.server.protocol', 'http')
503 509 _bool_setting(settings, 'startup.import_repos', 'false')
504 510 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
505 511 _bool_setting(settings, 'vcs.server.enable', 'true')
506 512 _bool_setting(settings, 'vcs.start_server', 'false')
507 513 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
508 514 _int_setting(settings, 'vcs.connection_timeout', 3600)
509 515
510 516 # Support legacy values of vcs.scm_app_implementation. Legacy
511 517 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
512 518 # which is now mapped to 'http'.
513 519 scm_app_impl = settings['vcs.scm_app_implementation']
514 520 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
515 521 settings['vcs.scm_app_implementation'] = 'http'
516 522
517 523
518 524 def _int_setting(settings, name, default):
519 525 settings[name] = int(settings.get(name, default))
520 526
521 527
522 528 def _bool_setting(settings, name, default):
523 529 input = settings.get(name, default)
524 530 if isinstance(input, unicode):
525 531 input = input.encode('utf8')
526 532 settings[name] = asbool(input)
527 533
528 534
529 535 def _list_setting(settings, name, default):
530 536 raw_value = settings.get(name, default)
531 537
532 538 old_separator = ','
533 539 if old_separator in raw_value:
534 540 # If we get a comma separated list, pass it to our own function.
535 541 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
536 542 else:
537 543 # Otherwise we assume it uses pyramids space/newline separation.
538 544 settings[name] = aslist(raw_value)
539 545
540 546
541 547 def _string_setting(settings, name, default, lower=True):
542 548 value = settings.get(name, default)
543 549 if lower:
544 550 value = value.lower()
545 551 settings[name] = value
General Comments 0
You need to be logged in to leave comments. Login now