Help with setting content-type to application/json (Pylons 1.0)

199 views
Skip to first unread message

Joel

unread,
Jan 10, 2013, 6:34:40 PM1/10/13
to pylons-...@googlegroups.com
Maybe I'm missing something, but I can't seem to set the content type of my response to 'application/json'.  (Actually, I don't think I am able to change the value of response.content_type at all...)

I'm using Pylons 1.0.

If I use the jsonify decorator then, as expected, the content type of the response is 'application/json'.  However, if I simply try to specify the JSON content type in my controller action (either via response.headers['content_type'] = 'application/json' or response.content_type = 'application/json'), then, according to my nosetests, the content type remains text/html; charset=utf-8.

Btw, I can't really just use the jsonify decorator because (a) I pass a custom json.JSONEncoder subclass to json.dumps in order to jsonify my SQLAlchemy model objects and (b) the content type of the response of a single action can vary (e.g., sometimes JSON, sometimes, e.g., 'image/jpeg').

Below I provide an example of a controller action followed by a test method that shows the content type remains unaffected by my specifications.

    @restrict('GET')
    @h.authenticate
    def retrieve(self, id):
        """Return the file data (binary stream) for the file in files/ with
        name=id or an error message if the file does not exist or the user is
        not authorized to access it.
        """
        response.headers['content_type'] = 'application/json'
        #response.content_type = 'application/json'    # this doesn't work either
        file = Session.query(File).filter(File.name==id).first()
        if file:
            unrestrictedUsers = h.getUnrestrictedUsers()
            if h.userIsAuthorizedToAccessModel(session['user'], file, unrestrictedUsers):
                response.content_type = file.MIMEtype    # I don't think this has any effect either: FileApp is setting the content type here ...
                filePath = os.path.join(config['app_conf']['permanent_store'], id)
                result = forward(FileApp(filePath))
            else:
                response.status_int = 403
                result = h.unauthorizedJSONMsg
        else:
            response.status_int = 404
            result = json.dumps({'error': 'There is no file with name %s' % id})
        return result

Here is the test for the above controller action:

    #@nottest
    def test_retrieve(self):
        # Attempt to retrieve the restricted file data as a contributor; expect to fail and receive a JSON object giving details.
        response = self.app.get(url(controller='files', action='retrieve', id=wavFileName),
            headers=self.json_headers, extra_environ=extra_environ_contrib, status=403)
        resp = json.loads(response.body)
        assert resp['error'] == u'You are not authorized to access this resource.'
        log.debug(response.headers['Content-Type'])     # This prints "text/html; charset=utf-8"

I've looked at the code for the jsonify decorator and it doesn't seem to be doing anything special that I'm not doing.  Thanks in advance for the assistance,

Joel

Ronan Amicel

unread,
Jan 11, 2013, 1:43:50 PM1/11/13
to pylons-...@googlegroups.com
Hi Joel,

Try passing the Content-Type header to FileApp like this:


@restrict('GET')
@h.authenticate
def retrieve(self, id):
    """Return the file data (binary stream) for the file in files/ with
    name=id or an error message if the file does not exist or the user is
    not authorized to access it.
    """
    file = Session.query(File).filter(File.name==id).first()
    if file:
        unrestrictedUsers = h.getUnrestrictedUsers()
        if h.userIsAuthorizedToAccessModel(session['user'], file, unrestrictedUsers):
            filePath = os.path.join(config['app_conf']['permanent_store'], id)
            headers = [('Content-Type', 'application/json')]
            result = forward(FileApp(filePath, headers=headers))

        else:
            response.status_int = 403
            result = h.unauthorizedJSONMsg
    else:
        response.status_int = 404
        result = json.dumps({'error': 'There is no file with name %s' % id})
    return result

Ronan Amicel

Joel

unread,
Jan 11, 2013, 6:14:33 PM1/11/13
to pylons-...@googlegroups.com
Hey Ronan,

Thanks.  However, the FileApp stuff is a bit of a red herring.  FileApp correctly sets the content type (i.e., to the MIMEtype of the file being served) without any interference on my part.

The problem is that I can't get my JSON responses to have the correct content type header.  I'm returning JSON in nearly every controller action and no matter what I do I get text/html; charset=utf-8 as the content type ...

Ronan Amicel

unread,
Jan 11, 2013, 6:26:12 PM1/11/13
to pylons-...@googlegroups.com
Does it work if you use the jsonify decorator?


--
You received this message because you are subscribed to the Google Groups "pylons-discuss" group.
To view this discussion on the web visit https://groups.google.com/d/msg/pylons-discuss/-/Zxo2qWgfAEMJ.

To post to this group, send email to pylons-...@googlegroups.com.
To unsubscribe from this group, send email to pylons-discus...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/pylons-discuss?hl=en.

Joel

unread,
Jan 11, 2013, 8:44:05 PM1/11/13
to pylons-...@googlegroups.com
Yeah, the jsonify decorator works.  But I need to be able to pass a custom JSONEncoder subclass to json.dumps, e.g., return json.dumps(result, cls=h.JSONOLDEncoder) so I think that rules out using @jsonify, right?  Also, I've looked at the jsonify code and it's not doing anything special that I'm not doing ...  I'm quite puzzled about why response.content_type = 'application/json' has no effect in my controller actions.  I feel like I must be missing something quite obvious ...

Ronan Amicel

unread,
Jan 13, 2013, 2:39:40 PM1/13/13
to pylons-...@googlegroups.com
Maybe you can start by copying the jsonify decorator, and modify it to suit your needs.


To view this discussion on the web visit https://groups.google.com/d/msg/pylons-discuss/-/7sE6FBrIzuYJ.

Joel

unread,
Jan 18, 2013, 8:30:02 PM1/18/13
to pylons-...@googlegroups.com
Ok, so here's what I've ended up doing.  I created my own version of jsonify which simply passes a JSONEncoder sublcass to simplejson.dump's cls kw argument as so:

    @decorator
    def jsonify(func, *args, **kwargs):
        pylons = get_pylons(args)
        pylons.response.headers['Content-Type'] = 'application/json'
        data = func(*args, **kwargs)
        return json.dumps(data, cls=JSONEncoder)

Now, the above decorator will set the Content-Type header to application/json so long as the status int is 200.  If an error code (e.g., 400, 401, 403, 404) is returned from one of my actions, then Pylons overrides my content type to text/html; charset=utf8.  I don't want that because I include JSON error objects in the body of my responses (I don't know if that's intrinsically a bad idea, but that's what I want to do, so anyways...)

My hacky way of forcing this to happen is to create a piece of middleware to do it for me, sticking it in config/middleware.py (cf. Chapter 16, 'Changing the Status and Headers' of the Pylons Book).  (If there's a better way to do this, please let me know).  Here it is:

    class HTML2JSONContentType(object):
        """Brute force text/html Content-Type in response to application/json.
        Take that Pylons!
        """
        def __init__(self, app):
            self.app = app
   
        def __call__(self, environ, start_response):
   
            def custom_start_response(status, headers, exc_info=None):
                if dict(headers)['Content-Type'] == 'text/html; charset=utf-8':
                    newHeaders = dict(headers)
                    newHeaders['Content-Type'] = 'application/json'
                    headers = newHeaders.items()
                return start_response(status, headers, exc_info)
   
            return self.app(environ, custom_start_response)

... and in config/middleware.py after the SessionMiddleware I have:

    app = HTML2JSONContentType(app)

I haven't yet tested how this "solution" interacts with FileApp's setting of the content type.  We'll see...
Reply all
Reply to author
Forward
0 new messages