Search code examples
pythonajaxcross-domaincherrypy

415 exception Cherrypy webservice


I'm trying to build a Cherrypy/Python webservice. I already spend the whole day on finding out how to make a cross domain ajax request possible. That's finally working, but now I have the next issue. I think I already know the solution, but I don't know how to implement it. The problem is that when I'm sending the ajax request, the Cherrypy server responds with:

415 Unsupported Media Type

Expected an entity of content type application/json, text/javascript

Traceback (most recent call last):  File "/Library/Python/2.7/site-packages/cherrypy/_cprequest.py", line 663, in respond    self.body.process()  File "/Library/Python/2.7/site-packages/cherrypy/_cpreqbody.py", line 996, in process    super(RequestBody, self).process()  File "/Library/Python/2.7/site-packages/cherrypy/_cpreqbody.py", line 538, in process    self.default_proc()  File "/Library/Python/2.7/site-packages/cherrypy/_cperror.py", line 411, in __call__    raise selfHTTPError: (415, u'Expected an entity of content type application/json, text/javascript')    

The solution I found, and trying to test, is adding this line to the configuration:

'tools.json_in.force': False

So I tried to implement it in this code:

import cherrypy
import json
import sys

class RelatedDocuments:

def index(self):
    return "Hello World!"

@cherrypy.tools.json_out()
@cherrypy.tools.json_in()
def findRelated(self, **raw):
    #Get JSON message form request
    request = cherrypy.request.json
    result = []

    #SOME CODE...

    return result;

# Expose the index method through the web. CherryPy will never
# publish methods that don't have the exposed attribute set to True.
index.exposed = True
findRelated.exposed = True

def CORS():
    cherrypy.response.headers["Access-Control-Allow-Origin"] = "*"

import os.path
tutconf = os.path.join(os.path.dirname(__file__), 'webserver.conf')
config = {
    'global': {
        'server.socket_host':'127.0.0.1',
        'server.socket_port': 8080,
        'log.error_file' : 'Web.log',
        'log.access_file' : 'Access.log'
    },
    '/': {
        'tools.CORS.on': True
    }
}

if __name__ == '__main__':
    cherrypy.tools.CORS = cherrypy.Tool('before_finalize', CORS)

    cherrypy.quickstart(RelatedDocuments(),config=config)

I added the config line under the tools.CORS.on line, but that didn't work. Next i tried this:

cherrypy.config.update({
    'tools.json_in.force': False,
});

Didn't work eiter..next I tried to implement this right above the findRelated method:

@cherrypy.config(**{'tools.json_in.force': False})

All of the implementations gave me a 500 error, I really appreciate it if somebody can help me. Thanks in advance!


Solution

  • I've realised that the question is in fact about CORS preflight request. CORS specification defines the following condition for a simple CORS request:

    • Method: GET, HEAD, POST
    • Headers: Accept, Accept-Language, Content-Language, Content-Type
    • Cotent-type header value: application/x-www-form-urlencoded, multipart/form-data, text/plain

    Otherwise CORS request isn't simple, and use preflight OPTIONS request before actual request to ensure it's eligible. Here is good CORS how-to.

    So if you want to keep things simple you may want to revert to normal application/x-www-form-urlencoded. Otherwise you need to handle preflight requests correctly. Here's working example (don't forget to add localhost alias).

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    '''
    Add localhost alias, `proxy` , in /etc/hosts.
    '''
    
    
    import cherrypy
    
    
    config = {
      'global' : {
        'server.socket_host' : '127.0.0.1',
        'server.socket_port' : 8080,
        'server.thread_pool' : 8
      }
    }
    
    
    def cors():
      if cherrypy.request.method == 'OPTIONS':
        # preflign request 
        # see http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
        cherrypy.response.headers['Access-Control-Allow-Methods'] = 'POST'
        cherrypy.response.headers['Access-Control-Allow-Headers'] = 'content-type'
        cherrypy.response.headers['Access-Control-Allow-Origin']  = '*'
        # tell CherryPy no avoid normal handler
        return True
      else:
        cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
    
    cherrypy.tools.cors = cherrypy._cptools.HandlerTool(cors)
    
    
    class App:
    
      @cherrypy.expose
      def index(self):
        return '''<!DOCTYPE html>
          <html>
          <head>
          <meta content='text/html; charset=utf-8' http-equiv='content-type'>
          <title>CORS AJAX JSON request</title>
          <script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'></script>
          <script type='text/javascript'>
            $(document).ready(function()
            {
              $('button').on('click', function()
              {
                $.ajax({
                  'type'        : 'POST',
                  'dataType'    : 'JSON',
                  'contentType' : 'application/json',
                  'url'         : 'http://proxy:8080/endpoint',
                  'data'        : JSON.stringify({'foo': 'bar'}),
                  'success'     : function(response)
                  {
                    console.log(response);  
                  }
                });
              })
            });
          </script>
          </head>
          <body>
            <button>make request</button>
          </body>
          </html>
        '''
    
      @cherrypy.expose
      @cherrypy.config(**{'tools.cors.on': True})
      @cherrypy.tools.json_in()
      @cherrypy.tools.json_out()
      def endpoint(self):
        data = cherrypy.request.json
        return data.items()
    
    
    if __name__ == '__main__':
      cherrypy.quickstart(App(), '/', config)