sanic icon indicating copy to clipboard operation
sanic copied to clipboard

CORS instructions incorrect for Sanic

Open toddwildey opened this issue 5 months ago • 0 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

Describe the bug

I have found the instructions for configuring CORS listed here to be incorrect: https://sanic.dev/en/guide/how-to/cors.html

When following these instructions, I would run into errors like the following:

Main  2024-09-16 00:32:51 -0400 ERROR:    Exception occurred while handling uri: 'http://localhost:8080/api/sessions/54b75e23-7ef4-4de1-9953-b562437080ee'
Traceback (most recent call last):
  File "handle_request", line 75, in handle_request
    from sanic.logging.setup import setup_logging
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               AttributeError: 'types.SimpleNamespace' object has no attribute 'request_middleware'
self.route
<Route: name=REDACTED.wrapped_handler path=api/sessions/<session_id:str>>
self.route.extra
namespace(ident='REDACTED.wrapped_handler', ignore_body=False, stream=False, hosts=[None], static=False, error_format='', websocket=False)
Main  2024-09-16 00:32:51 -0400 ERROR:    Exception occurred in one of response middleware handlers
Traceback (most recent call last):
  File "handle_request", line 75, in handle_request
    from sanic.logging.setup import setup_logging
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               AttributeError: 'types.SimpleNamespace' object has no attribute 'request_middleware'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/Volumes/workspace/Projects/ontology/oracle/automaton/.venv/lib/python3.11/site-packages/sanic/request/types.py", line 405, in respond
    self.route and self.route.extra and self.route.extra.response_middleware
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                        AttributeError: 'types.SimpleNamespace' object has no attribute 'response_middleware' 

Specifically, I found that removing the app.router.reset() and app.router.finalize() from options.py fixed this issue:

def setup_options(app: Sanic, _):
    # app.router.reset()
    needs_options = _compile_routes_needing_options(app.router.routes_all)
    for uri, methods in needs_options.items():
        app.add_route(
            _options_wrapper(options_handler, methods),
            uri,
            methods=["OPTIONS"],
        )
    # app.router.finalize()

I'm not sure how critical it is for these to be invoked, but it seems everything is working fine without them.

Additionally, I was able to reduce the cors.py and options.py to the following, which are working:

cors.py

from sanic import Request, HTTPResponse

from typing import Iterable

def _add_cors_headers(request: Request, response: HTTPResponse, methods: str) -> None:
    response.headers['Access-Control-Allow-Headers'] = "origin, content-type, accept, authorization, x-xsrf-token, x-request-id"
    response.headers['Access-Control-Allow-Methods'] = methods
    response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin') or '*'

def add_cors_headers(request: Request, response: HTTPResponse):
    _add_cors_headers(request, response, request.app.ctx.uri_methods_mapping[request.route.uri])

options.py

from collections import defaultdict
from typing import Dict

from sanic import empty, Request, HTTPResponse, Sanic
from sanic.router import Route

from .cors import _add_cors_headers

def _compile_routes_needing_options(routes: Dict[str, Route]) -> Dict[str, str]:
    needs_options = defaultdict(list)
    # This is 21.12 and later. You will need to change this for older versions.
    for route in routes:
        if "OPTIONS" not in route.methods:
            needs_options[route.uri].extend(route.methods)

    return {
        uri: ",".join(methods) for uri, methods in dict(needs_options).items()
    }

async def options_handler(request: Request, *args, **kwargs) -> HTTPResponse:
    return empty()

def setup_options(app: Sanic, _):
    uri_methods_mapping = _compile_routes_needing_options(app.router.routes)
    app.ctx.uri_methods_mapping = uri_methods_mapping

    for uri, methods in uri_methods_mapping.items():
        app.add_route(options_handler, uri, methods = ["OPTIONS"])

I understand this may not suite everyone's needs - especially if folks need custom OPTIONS endpoints that have more specific logic. Nonetheless, I figured I would share my working configuration.

The following is the requirements.txt for this project:

aiofiles==24.1.0
annotated-types==0.7.0
anyio==4.4.0
appdirs==1.4.4
distro==1.9.0
fs==2.4.16
groq==0.9.0
h11==0.14.0
html5tagger==1.3.0
httpcore==1.0.5
httptools==0.6.1
httpx==0.27.0
multidict==6.0.5
openai==1.35.13
pydantic-core==2.20.1
pydantic==2.8.2
pygments==2.18.0
pystache==0.6.5
sanic-routing==23.12.0
sanic==24.6.0
setuptools==73.0.1
sniffio==1.3.1
tracerite==1.1.1
ujson==5.10.0
uvloop==0.20.0
websockets==13.0

The following is the requirements-dev.txt for this project:

build~=1.2.1
coverage~=7.5.3
pytest~=8.2.1
tox~=4.15.0

Code snippet

No response

Expected Behavior

No response

How do you run Sanic?

As a script (app.run or Sanic.serve)

Operating System

Linux

Sanic Version

24.6.0

Additional context

No response

toddwildey avatar Sep 16 '24 16:09 toddwildey