Skip to content

Commit

Permalink
Fixed the errors seen in Sanic 19.12+ where the CORS exception handle…
Browse files Browse the repository at this point in the history
…r could be triggered

  _before_ the request context for a given request is created.
  - If on Sanic 19.9+ fallback to using the request.ctx object when request_context is not available
Fixes #41
  • Loading branch information
ashleysommer committed Feb 19, 2020
1 parent fce6f00 commit 2ad2bb0
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ language: python
python:
- '3.6'
env:
- SANIC=19.12.2 SPF=0.9.0.b1
- SANIC=19.12.2 SPF=0.9.1
install:
- pip install -U setuptools pep8 coverage docutils pygments aiohttp sanic==$SANIC sanic-plugins-framework==$SPF
script:
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# Change Log

## 0.10.0.post1
- Fixed the errors seen in Sanic 19.12+ where the CORS exception handler could be triggered
_before_ the request context for a given request is created.
- If on Sanic 19.9+ fallback to using the request.ctx object when request_context is not available
- Fixes #41

## 0.10.0
- Fixed catch LookupError when request context doesn't exist
- Release 0.10.0

## 0.10.0.b1
- New minimum supported sanic version is 18.12LTS
- Fixed bugs with Sanic 19.12
- Max supported sanic version for this release series is unknown for now.

##
_**Note**_, Sanic v19.12.0 (and 19.12.2) _do not_ work with Sanic-CORS 0.9.9 series or earlier.
A new version coming out soon will work with sanic v19.12.

## 0.9.9.post4
This is the last version of sanic-cors to support Sanic 0.8.3
Expand Down
20 changes: 8 additions & 12 deletions sanic_cors/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,26 +224,22 @@ def get_cors_headers(options, request_headers, request_method):
return CIMultiDict((k, v) for k, v in headers.items() if v)


def set_cors_headers(req, resp, context, options):
def set_cors_headers(req, resp, req_context, options):
"""
Performs the actual evaluation of Sanic-CORS options and actually
modifies the response object.
This function is used both in the decorator and the after_request
callback
This function is used in the decorator, the CORS exception wrapper,
and the after_request callback
:param sanic.request.Request req:
"""
try:
request_context = context.request[id(req)]
except (AttributeError, LookupError):
LOG.debug("Cannot find the request context. Is request already finished?")
return resp
# If CORS has already been evaluated via the decorator, skip
evaluated = request_context.get(SANIC_CORS_EVALUATED, False)
if evaluated:
LOG.debug('CORS have been already evaluated, skipping')
return resp
if req_context is not None:
evaluated = getattr(req_context, SANIC_CORS_EVALUATED, False)
if evaluated:
LOG.debug('CORS have been already evaluated, skipping')
return resp

# `resp` can be None or [] in the case of using Websockets
# however this case should have been handled in the `extension` and `decorator` methods
Expand Down
123 changes: 89 additions & 34 deletions sanic_cors/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
:copyright: (c) 2020 by Ashley Sommer (based on flask-cors by Cory Dolphin).
:license: MIT, see LICENSE for more details.
"""
from asyncio import iscoroutinefunction
from functools import update_wrapper, partial
from inspect import isawaitable

from sanic import exceptions, response, __version__ as sanic_version
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.handlers import ErrorHandler
from spf import SanicPlugin
from .core import *
Expand All @@ -21,6 +23,9 @@

SANIC_VERSION = LooseVersion(sanic_version)
SANIC_18_12_0 = LooseVersion("18.12.0")
SANIC_19_9_0 = LooseVersion("19.9.0")
SANIC_19_12_0 = LooseVersion("19.12.0")



class CORS(SanicPlugin):
Expand Down Expand Up @@ -207,9 +212,19 @@ async def route_wrapper(self, route, req, context, request_args, request_kw,
# resp can be `None` or `[]` if using Websockets
if not resp:
return None
request_context = context.request[id(req)]
set_cors_headers(req, resp, context, options)
request_context[SANIC_CORS_EVALUATED] = "1"
try:
request_context = context.request[id(req)]
except (AttributeError, LookupError):
if SANIC_19_9_0 <= SANIC_VERSION:
request_context = req.ctx
else:
request_context = None
set_cors_headers(req, resp, request_context, options)
if request_context is not None:
setattr(request_context, SANIC_CORS_EVALUATED, "1")
else:
context.log(logging.DEBUG, "Cannot access a sanic request "
"context. Has request started? Is request ended?")
return resp

def unapplied_cors_request_middleware(req, context):
Expand All @@ -227,9 +242,20 @@ def unapplied_cors_request_middleware(req, context):
"Using options: {}".format(
path, get_regexp_pattern(res_regex), res_options))
resp = response.HTTPResponse()
request_context = context.request[id(req)]
set_cors_headers(req, resp, context, res_options)
request_context[SANIC_CORS_EVALUATED] = "1"

try:
request_context = context.request[id(req)]
except (AttributeError, LookupError):
if SANIC_19_9_0 <= SANIC_VERSION:
request_context = req.ctx
else:
request_context = None
context.log(logging.DEBUG,
"Cannot access a sanic request "
"context. Has request started? Is request ended?")
set_cors_headers(req, resp, request_context, res_options)
if request_context is not None:
setattr(req.ctx, SANIC_CORS_EVALUATED, "1")
return resp
else:
debug('No CORS rule matches')
Expand All @@ -238,23 +264,29 @@ def unapplied_cors_request_middleware(req, context):
async def unapplied_cors_response_middleware(req, resp, context):
log = context.log
debug = partial(log, logging.DEBUG)
try:
request_context = context.request[id(req)]
except (AttributeError, LookupError):
debug("Cannot find the request context. Is request already finished?")
return False
# `resp` can be None or [] in the case of using Websockets
if not resp:
return False
if request_context.get(SANIC_CORS_SKIP_RESPONSE_MIDDLEWARE):
debug('CORS was handled in the exception handler, skipping')
return False

# If CORS headers are set in a view decorator, pass
elif request_context.get(SANIC_CORS_EVALUATED):
debug('CORS have been already evaluated, skipping')
return False

try:
request_context = context.request[id(req)]
except (AttributeError, LookupError):
if SANIC_19_9_0 <= SANIC_VERSION:
request_context = req.ctx
else:
debug("Cannot find the request context. "
"Is request already finished? Is request not started?")
request_context = None
if request_context is not None:
# If CORS headers are set in the CORS error handler
if getattr(request_context,
SANIC_CORS_SKIP_RESPONSE_MIDDLEWARE, False):
debug('CORS was handled in the exception handler, skipping')
return False

# If CORS headers are set in a view decorator, pass
elif getattr(request_context, SANIC_CORS_EVALUATED, False):
debug('CORS have been already evaluated, skipping')
return False
try:
path = req.path
except AttributeError:
Expand All @@ -265,8 +297,9 @@ async def unapplied_cors_response_middleware(req, resp, context):
if try_match(path, res_regex):
debug("Request to '{}' matches CORS resource '{:s}'. Using options: {}".format(
path, get_regexp_pattern(res_regex), res_options))
set_cors_headers(req, resp, context, res_options)
request_context[SANIC_CORS_EVALUATED] = "1"
set_cors_headers(req, resp, request_context, res_options)
if request_context is not None:
setattr(request_context, SANIC_CORS_EVALUATED, "1")
break
else:
debug('No CORS rule matches')
Expand Down Expand Up @@ -296,14 +329,21 @@ def _apply_cors_to_exception(cls, ctx, req, resp):
resources = ctx.resources
log = ctx.log
debug = partial(log, logging.DEBUG)
try:
request_context = ctx.request[id(req)]
except (AttributeError, LookupError):
if SANIC_19_9_0 <= SANIC_VERSION:
request_context = req.ctx
else:
request_context = None
for res_regex, res_options in resources:
if try_match(path, res_regex):
debug(
"Request to '{:s}' matches CORS resource '{}'. "
"Using options: {}".format(
path, get_regexp_pattern(res_regex),
res_options))
set_cors_headers(req, resp, ctx, res_options)
set_cors_headers(req, resp, request_context, res_options)
break
else:
debug('No CORS rule matches')
Expand All @@ -325,10 +365,13 @@ def lookup(self, exception):
# wrap app's original exception response function
# so that error responses have proper CORS headers
@classmethod
def wrapper(cls, f, ctx, req, e):
async def wrapper(cls, f, ctx, req, e):
opts = ctx.options
# get response from the original handler
do_await = iscoroutinefunction(f)
resp = f(req, e)
if do_await:
resp = await resp
# SanicExceptions are equiv to Flask Aborts,
# always apply CORS to them.
if (req is not None and resp is not None) and \
Expand All @@ -340,22 +383,34 @@ def wrapper(cls, f, ctx, req, e):
# not sure why certain exceptions doesn't have
# an accompanying request
pass
if req is not None:
# These exceptions have normal CORS middleware applied automatically.
# So set a flag to skip our manual application of the middleware.
try:
request_context = ctx.request[id(req)]
request_context[SANIC_CORS_SKIP_RESPONSE_MIDDLEWARE] = "1"
except (LookupError, AttributeError):
if req is None:
return resp
# These exceptions have normal CORS middleware applied automatically.
# So set a flag to skip our manual application of the middleware.
try:
request_context = ctx.request[id(req)]
except (LookupError, AttributeError):
# On Sanic 19.12.0, a NotFound error can be thrown _before_
# the request_context is set up. This is a fallback routine:
if SANIC_19_12_0 <= SANIC_VERSION and \
isinstance(e, (NotFound, MethodNotSupported)):
# On sanic 19.9.0+ request is a dict, so we can add our
# flag directly to it.
request_context = req.ctx
else:
log = ctx.log
log(logging.DEBUG,
"Cannot find the request context. "
"Cannot find the request context. Is request started? "
"Is request already finished?")
request_context = None
if request_context is not None:
setattr(request_context,
SANIC_CORS_SKIP_RESPONSE_MIDDLEWARE, "1")
return resp

def response(self, request, exception):
async def response(self, request, exception):
orig_resp_handler = self.orig_handler.response
return self.wrapper(orig_resp_handler, self.ctx, request, exception)
return await self.wrapper(orig_resp_handler, self.ctx, request, exception)

instance = cors = CORS()
__all__ = ["cors", "CORS"]
2 changes: 1 addition & 1 deletion sanic_cors/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.10.0'
__version__ = '0.10.0.post1'

0 comments on commit 2ad2bb0

Please sign in to comment.