wagtail: Preview mode doesn't work under WSGI/Gunicorn

Issue Summary

Attempt to preview any page results in exception.

Steps to Reproduce

  1. Open any page in the WagTail CMS for editing
  2. Click Preview button

Site is running under nginx/gunicorn. When I run Django development server, this problem doesn’t happen, preview works as expected. This is clearly related to gunicorn, but when I run local instance of gunicorn, it doesn’t happen either. Only on the remote site.

It seems that tricks that Wagtail uses to load preview in a new window causes the problem.

After reading Wagtail source code I found this:

        preview_mode = request.GET.get('mode', page.default_preview_mode)
        response = page.serve_preview(page.dummy_request(request), preview_mode)
        response['X-Wagtail-Preview'] = 'ok'
        return response

in wagtail/wagtailadmin/views/pages.py

Apparently, differences in requests under local django devserver and gunicorn causes some malfunction in the Django authentication middleware and it fails to load currently logged in user while in preview mode.

Since this happens only in WagTail preview and all other views and dependencies functioned perfectly well in this setup for years, I assume this is a Wagtail bug.

I have “fixed” this issue in my case by changing the line from: response = page.serve_preview(page.dummy_request(request), preview_mode) to: response = page.serve_preview(request, preview_mode) On line 562 in wagtail/wagtailadmin/views/pages.py

I see that master branch uses the same dummy request technique that probably will also fail when running under gunicorn.

Technical details

AttributeError at /cms/pages/29/edit/preview/
'WSGIRequest' object has no attribute 'user'

Request Method: POST
Request URL: https://staging.royalorchard.com/cms/pages/29/edit/preview/
Django Version: 1.10.6
Python Executable: /home/rolc/.virtualenvs/dev/bin/python3
Python Version: 3.5.2
Python Path: ['/home/rolc/dev', '/home/rolc/.virtualenvs/dev/bin', '/home/rolc/.virtualenvs/dev/lib/python35.zip', '/home/rolc/.virtualenvs/dev/lib/python3.5', '/home/rolc/.virtualenvs/dev/lib/python3.5/plat-x86_64-linux-gnu', '/home/rolc/.virtualenvs/dev/lib/python3.5/lib-dynload', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages']
Server time: Fri, 14 Apr 2017 03:21:22 -0400
Installed Applications:
('django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.messages',
 'django.contrib.sessions',
 'django.contrib.sites',
 'django.contrib.humanize',
 'django.contrib.staticfiles',
 'django.contrib.admindocs',
 'django.contrib.flatpages',
 'tagging',
 'django_extensions',
 'filebrowser',
 'crispy_forms',
 'floppyforms',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'reversion',
 'papertrail',
 'wagtail.wagtailforms',
 'wagtail.wagtailredirects',
 'wagtail.wagtailembeds',
 'wagtail.wagtailsites',
 'wagtail.wagtailusers',
 'wagtail.wagtailsnippets',
 'wagtail.wagtaildocs',
 'wagtail.wagtailimages',
 'wagtail.wagtailsearch',
 'wagtail.wagtailadmin',
 'wagtail.wagtailcore',
 'wagtail.contrib.table_block',
 'modelcluster',
 'taggit',
 'taggit_templatetags2',
 'royal_orchard.users',
 'royal_orchard.reports',
 'royal_orchard.documents',
 'royal_orchard.emails',
 'royal_orchard.blog',
 'royal_orchard.reservations',
 'royal_orchard.vendors',
 'royal_orchard.flickr',
 'royal_orchard.search',
 'royal_orchard.userpages',
 'tinymce',
 'sorl.thumbnail',
 'constance',
 'constance.backends.database',
 'djmail',
 'bootstrap_pagination')
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.contrib.admindocs.middleware.XViewMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'wagtail.wagtailcore.middleware.SiteMiddleware',
 'wagtail.wagtailredirects.middleware.RedirectMiddleware')


Traceback:  

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  42.             response = get_response(request)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/core/handlers/base.py" in _legacy_get_response
  249.             response = self._get_response(request)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  217.                 response = self.process_exception_by_middleware(e, request)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  215.                 response = response.render()

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/template/response.py" in render
  109.             self.content = self.rendered_content

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/template/response.py" in rendered_content
  86.         content = template.render(context, self._request)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/template/backends/django.py" in render
  66.             return self.template.render(context)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/django/template/base.py" in render
  206.                 with context.bind_template(self):

File "/usr/lib/python3.5/contextlib.py" in __enter__
  59.             return next(self.gen)

File "/home/rolc/.virtualenvs/dev/lib/python3.5/site-packages/debug_toolbar/panels/templates/panel.py" in _request_context_bind_template
  49.         context = processor(self.request)

File "/home/rolc/dev/royal_orchard/blog/context_processors.py" in context
  10.     user = request.user

Exception Type: AttributeError at /cms/pages/29/edit/preview/
Exception Value: 'WSGIRequest' object has no attribute 'user'
Request information:
USER: Robert ***********

GET: No GET data

POST:
seo_title = ''
expire_at = ''
csrfmiddlewaretoken = '******'
body-0-type = 'paragraph'
body-count = '1'
go_live_at = ''
body-0-deleted = ''
search_description = ''
body-0-order = '0'
slug = '******-history'
title = '****** History'
next = ''
body-0-value = '<p>I feel so very privileged to be invited into your home and if we also feel a little intimidated because we are talking to folks who have generational knowledge of this wonderful house and I know as we do so we are little bit nervous about giving you advice on wh</p>'
  • Python version: 3.5.2.
  • Django version: 1.10.6.
  • Wagtail version: 1.9.
  • Browser version: Firefox 52.0.2 on Ubuntu Linux.
  • Gunicorn: 19.7.1
  • Nginx: 1.10.0

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 1
  • Comments: 27 (13 by maintainers)

Commits related to this issue

Most upvoted comments

@gasman Thank you! Your suggestion was right. I’ve found the problem: Wagtail uses its own Site model which is set to localhost by default. Since, on staging/production sites there’s no localhost in settings.ALLOWED_HOSTS, CommonMiddleware was failing and exception message was silently suppressed. So, no session and authentication middleware were run, but exception was lost somewhere, so it was almost impossible to find the real cause of the problem.

I think this is still a bug. It should be either documented somewhere to set proper hostname for the wagtail Site (can it take value from Django Site which is properly set to staging hostname?) or the real exception should be displayed to the user, not silently ignored.

See #5427 for a full diagnosis and (hopefully) fix.

@BenSturmfels @SalahAdDin The problem only occurs when wagtail site hostname mismatches the real hostname. You need to check if you have the site hostname set correctly in the Django admin.

Reopening this, since the current behaviour is a regression from the pre-Django-1.10 middleware where we ignored any responses (error or otherwise) originating from middleware. I’m not sure of the best way to fix it, though…

ignore middleware errors during processing dummy wagtail request

I can’t see a good way to do that with Django’s current middleware implementation - in the old-style middleware, we called process_request on each middleware class in turn, so we knew when a middleware was returning its own response. In the new style, the outer classes completely wrap the inner ones, so there’s no way to tell from outside whether the response originates from the middleware or the actual view.

remove common middleware from the list of middleware when processing dummy request

This might work, but it feels like a “band-aid” fix - it’s possible that other middlewares will have similar issues. (In fact, I’m not sure that CommonMiddleware is the culprit here - checking ALLOWED_HOSTS seems to happen in the Request object itself, so it could occur in any place that tries to retrieve hostname.)

do not use middleware at all when processing fake request

This isn’t an option - the page template will often rely on variables that are inserted in middleware, such as request.user or request.site. For similar reasons I don’t think we can avoid the dummy_request mechanism - the template author should be free to use any properties of the request object, so it’s Wagtail’s responsibility to make sure that the request object is as close as possible to the one that would be seen on a real page request.

One possibility is to add a system check that generates a warning on startup if any Site records have hostnames that aren’t covered by ALLOWED_HOSTS. However, one goal I’ve always had with the design of Wagtail is that Site records should just work out of the box for single-site installations, and you shouldn’t have to care about configuring them until you come to build a multi-site project. (For example, that’s why Page.get_url() returns a local URL without the hostname on single-site installations). So maybe it’s better to pre-emptively check ALLOWED_HOSTS in dummy_request, and silently fix up the host header if we can see it’s going to break…

Incidentally, the unhelpfulness of the 'WSGIRequest' object has no attribute 'user' error is not Wagtail’s fault - it’s because you have code in context_processors.py that’s not robust enough against error conditions (i.e. it’s expecting to find attributes of request that aren’t guaranteed to be there).

Quick fix for my site was to add localhost to the ALLOWED_HOSTS string. Worked like a charm.