How to set up CORS in CherryPy
Asked Answered
P

1

5

Overview

When creating a post request from my website to my Python server running CherryPy, I receive the error Access to XMLHttpRequest has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response. . I was able to get away with the problem temporarily with one of the "CORS Everywhere" browser extensions, but

  1. Due to recent updates, the extensions have not yet been updated to be working again.
  2. The website involved needs to eventually be used by many in my local complex without the browser extension, so once the extensions get updated, it does not really matter one way or another, as I cannot rely on these extensions, and force everyone to use them (when there is obviously a fix that would make an extension not necessary). I figure that perhaps the solutions are outdated, but am not sure.

Here is the relevant code:

On the server side (CherryPy/Python):

The CherryPy Python function being called, from the website post request

@cherrypy.expose
@cherrypy.tools.json_in()
def add_meeting(self):
        data = None
        id = None
        start_time = None
        end_time = None
        title = None
        userlist = None
        result = {"operation": "request", "result": "success"}
        if cherrypy.request.method == "POST":
            data = cherrypy.request.json
            id = data["id"]
            start_time = data["start_time"]
            end_time = data["end_time"]
            title = data["title"]
            userlist = data["userlist"]         

        # Rest of relevant code in function is left out, to take up less
        # space and not post irrelevant code. That being said, I am
        # positive the logic is correct, as it originally ran smoothly
        # with a "Cors Everywhere" Browser Extension.

        return result

Here is the area where I set up and run CherryPy

def main():
    # Create the configuration file parser object and start the CherryPy server
    config = ConfigParser.ConfigParser()
    config.read(CONFIG_FILE)
    port = config.getint('Meta', 'port')
    host = config.get('Meta', 'host')
    cherrypy.config.update({'server.socket_port': port,
                            'server.socket_host': host,
                            'tools.CORS.on': True})
    cherrypy.quickstart(Coordinator(config))
main()

Here is the config file mentioned in the code above (CONFIG_FILE)

[Meta]
host = 0.0.0.0
port = 3000


# Rest is left out, as it is irrelevant with problem

The solutions I have tried implementing

  1. The inclusion of the following function above the main function:
def CORS():
    cherrypy.response.headers["Access-Control-Allow-Origin"] = "*"


with cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS)

2. Adding " 'cors.expose.on': True " to cherrypy.config.update above
3. Using this cherrypy-cors Python library I found online: https://pypi.org/project/cherrypy-cors/
4. The inclusion of headers in the config.update portion of the Python file
5. Adding "@cherrypy.tools.accept(media='application/json')" before "def add_meeting"

Conclusion

I've tried the solutions above together, separately, some with and without the others, and I am still stuck. Maybe some of these solutions are partially correct, and there is something extra needed with my code. I am not sure; I just cannot get it working. I do not have much experience with web development before this, so maybe (and hopefully) the solution is extremely simple. I know the code works, I just cannot get it running without a working "Cors Everywhere" browser extension for every user.

As for the versions I am running: I am using CherryPy 14.2.0 and Python 2.7.6

Any help would mean the absolute world to me, thank you.

Pontifical answered 8/8, 2019 at 15:37 Comment(17)
#32500573 Does this help?Hicks
@JammyDodger Thank you so much for your response. Unfortunately all the servers are supposed to be running on Python CherryPy, and while many of the ideas used in this solution are what I have been trying to implement (in CherryPy, not like that specific link), implementing it in Python CherryPy has been a nightmare. The problem has to do more with getting it to work on CherryPy.Pontifical
Just use pypi.org/project/cherrypy-corsPoplin
@webKnjaZ I tried implementing cherrypy-cors by including the 'cors.expose.on' to the "cherrypy.config.update" above, as well as adding a "cherrypy_cors.install()" line above it, and after installing through pip, I still have the same issue. I tried this solution a while back, but it does not seem to work. Thank you for your response, though.Pontifical
Plz post a full snippet.Poplin
@webKnjaZ Sorry, I went on a short vacation and left my computer back. I attached the full file (the backend), as requested, here: gofile.io/?c=gtxGKQ The two relevant functions (that involve a CORS issue without a working extension) are "add_meeting" and "remove_meeting." Thank you again for offering to help.Pontifical
FYI the best practice is to use something like gist.github.com instead of making others download files locally.Poplin
Also, your example contains a lot of unrelated stuff, refs to unavailable modules and so on. And you need to also show the client code: I don't know how you make queries.Poplin
In general, the best practice is to provide MCVE: stackoverflow.com/help/minimal-reproducible-examplePoplin
This probably means that 1-2 methods is enough and you should drop any business logic and probably drop HTTP handler contents in favor of some dummy hardcoded values.Poplin
Oh, and also drop any mentions of the DB. It's totally unrelated.Poplin
Good to know. Here are my client side snippets and my MCVE coordinator file.Pontifical
Still too much logic there. I'll simplify it for testing in my fork. FYI you can put multiple files in the same gist, no need to maintain separate ones.Poplin
I've turned your gists into an MCVE and adjusted JS to not have any third-party deps: gist.github.com/webknjaz/a0c46a282958b1a747737872e32f5d81. This is a working example. For more usage details (like using with a method dispatcher) see docstrings in the source of cherrypy-cors: github.com/yougov/cherrypy-cors/blob/ebf705c/…Poplin
@webKnjaZ Thank you so much for taking the time to help me with this. You made my summer. I appreciate your patience with teaching me some of the StackOverflow conventions, as well. Cheers.Pontifical
FYI there's a CherryPy user chat @ gitter.im/cherrypy/cherrypy which fits better for debugging questions. SO is more about generic things which would be useful to a wider audience because of its format. I'll try to compile a proper answer on this page to make it useful to others.Poplin
@User133311175 I've posted a full-sized answer to this question below. Please mark it as accepted.Poplin
Y
8

So first, you need to set pre-flight headers when processing OPTIONS request, you can list allowed methods there. Then, you also need to enable the cors.expose tool.

There's some usage hints in the docstring of cherrypy-cors. For example, when using a MethodDispatcher, you could just decorate an OPTIONS handler method with @cherrypy_cors.tools.preflight() instead of doing this in every HTTP handler.

Here's a simple traversal example (without a method dispatcher). To test it, visit http://127.0.0.1/ and it will make requests against http://localhost:3333/add_meeting which is a different Origin in terms of CORS ('localhost' != '127.0.0.1').

"""Example of CORS setup using cherrypy-cors library."""

import cherrypy
import cherrypy_cors


# Python 2 compat: make all classes new-style by default
__metaclass__ = type  # pylint: disable=invalid-name


class WebRoot:
    """Root node for HTTP handlers."""

    @cherrypy.expose
    def index(self):  # pylint: disable=no-self-use
        """Render a web page handling request against ``/``.

        Contains client JS snippet which will query the API endpoint.
        It will be executed by the browser while loading the page.
        """
        return """<html>
            <script type="text/javascript">
                async function addMeeting() {
                  /*
                   * Example coroutine for querying /add_meeing
                   * HTTP endpoint. It uses localhost as in the URL.
                   * For testing CORS, make sure to visit
                   * http://127.0.0.1/ which is a different origin
                   * from browser's perspective.
                   * /
                  const request_payload = {
                    some: 'data',
                    listed: ['h', 'er', 'e'],
                  }
                  try {
                    const resp = await fetch(
                      'http://localhost:3333/add_meeting',
                      {
                        method: 'POST',
                        mode: 'cors',  // Required for customizing HTTP request headers
                        credentials: 'same-origin',
                        headers: {
                          'Content-Type': 'application/json; charset=UTF-8',  // Required for ``cherrypy.tools.json_in`` to identify JSON payload and parse it automatically
                        },
                        body: JSON.stringify(request_payload),
                      },
                    )
                    const json_resp = await resp.json()
                    console.log(json_resp)  // Will print: {"method": "POST", "payload": {"listed": ["h", "er", "e"], "some": "data"}}
                  } catch (e) {
                    console.warn('Exception: ' + e)
                  }
                }

                async function main() {
                  await addMeeting()
                }

                main()  // Entry point
            </script>
        </html>"""  # noqa: E501

    @cherrypy.expose
    @cherrypy.tools.json_in()  # turn HTTP payload into an object; also checking the Content-Type header
    @cherrypy.tools.json_out()  # turn ``return``ed Python object into a JSON string; also setting corresponding Content-Type
    def add_meeting(self):
        """Handle HTTP requests against ``/add_meeting`` URI."""
        if cherrypy.request.method == 'OPTIONS':
            # This is a request that browser sends in CORS prior to
            # sending a real request.

            # Set up extra headers for a pre-flight OPTIONS request.
            cherrypy_cors.preflight(allowed_methods=['GET', 'POST'])

        if cherrypy.request.method == 'POST':
            return {'method': 'POST', 'payload': cherrypy.request.json}

        return {'method': 'non-POST'}


def main():
    """Set up and run the web app.

    Initializes CORS tools.
    Sets up web server socket.
    Enables the CORS tool.
    """
    cherrypy_cors.install()
    cherrypy.config.update({
        'server.socket_host': '127.0.0.1',
        'server.socket_port': 3333,
        'cors.expose.on': True,
    })
    cherrypy.quickstart(WebRoot())


__name__ == '__main__' and main()  # pylint: disable=expression-not-assigned
Yuhas answered 26/8, 2019 at 7:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.