Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem: No easy way to create custom per-request context #32

Closed
swistakm opened this issue Nov 8, 2016 · 1 comment
Closed

Problem: No easy way to create custom per-request context #32

swistakm opened this issue Nov 8, 2016 · 1 comment

Comments

@swistakm
Copy link
Owner

swistakm commented Nov 8, 2016

Generally having access to falcon's req and resp parameters is a nice feature to have when you want to read/manipulate headers or provide some custom per-request context value. Typical use case for per-request context variables are database session/connection objects that cannot be stored as resource instance attributes (needs to be unique for every request).

These are usually implemented as middlewares that are able to update req.context context dictionary. Unfortunately when using generic API resource classes (e.g. ListCreateAPI, PaginatedListAPI) user is expected to provide only basic resource manipulation methods like list(), retrieve(), create() etc. These methods accept only following set of arguments:

  • params: dictionary of deserialised keyword parameters (defined using resource-level param classes)
  • meta: dictionary with additional metadata that will be included in output response
  • validated (only on create/update handlers): deserialised resource representation provided by client in request body
  • **kwargs: additional keyword arguments retrieved from falcon's URI template associated to resource route.

Because basic resource manipulation methods (list/retrieve/create etc.) accept only specific set of arguments and none of them represents falcon.Request object. The only place where custom context can be provided is **kwargs dictionary that was primarily supposed to only hold keyword arguments retrieved from URI template.

Currently available approach - overriding HTTP method handlers

Right now, the simplest way to provide custom context to every request is to directly override falcon's HTTP method handlers in custom resource classes (inherited from generic and basic resources classes) and pass them further in **kwargs dictionary using super(). Example:

# assume Session is some db layer abstraction

def MyResource(ListCreateAPI):
    def on_get(req, resp, **kwargs):  # <= overrides default HTTP  GET handler
        super().on_get(req, resp, db_session=Session(), **kwargs)

   def list(req,req, db_session, **kwargs):  # <= called by super().on_get()
       return db_session.all()

    def on_post(req, resp, **kwargs):  # <= overrides default HTTP  POST handler
        super().on_post(req, resp, custom_var="some_value", **kwargs)

    def create(req,req, validated, db_session, **kwargs):  # <= called by super().on_patch()
        return db_session.insert(validated)

Of course this is very impractical because every method handler that requires such additional context needs to be overridden. Also **kwargs are now only expected to hold only values from URI template and their purpose is documented exactly as URI template keyword values.

Currently available approach - using middleware/hooks

Overriding every HTTP method handler in exactly the same way is not a clean approach if specific context is required by every API endpoint. This will require a lot of redundant and messy code. One could reduce amount of boilerplate by providing custom base classes (based on graceful generics) with HTTP method handlers overridden to provide additional context. This will still require a lot of code duplication and will be hard to maintain in the long term.

In many frameworks (including falcon) the custom context is usually provided using two methods:

  • use global middleware that is able to provide custom context for every request/response in the application: it is fine if same context is needed in every API endpoint. Usually great for things like db sessions/connections, or user sessions.
  • use per-method or per-resource hooks: implemented in similar fashion to middleware but offer more fine-grained control and can be attached to specific resources or resource's HTTP methods (usually using decorators).

Both middleware and hooks from falcon can be used in graceful resources but their usage is very limited. The only parts of above features that could be used to provide context are:

  • process_resource(self, req, resp, resource, params) method of middleware class: the params dictionary is the save object unpacked as **kwargs keyword arguments. We cannot use anything else because req & resp are unavailable in basic resource manipulation handlers (list/create/retrieve etc.) and the resource instance can be shared between multiple worker threads.
  • action argument of falcon.before(action) decorator with signature of action(req, resp, resource, params): same as for middlewares -- only params parameter that translates directly to **kwargs can be used as a container for new context values.

Additionally usage of hooks in graceful is limited even further. They can be attached to whole resource:

@falcon.before(context_action)
def MyResource(ListCreateAPI):
    pass

But cannot be easily attached to specific resource manipulation method. The falcon.before() expects decorated function to have signature of a HTTP method handler (i.e. on_get(req, resp, **kwargs), on_post(req, resp, **kwargs) and so on). Due to this the only way to attach falcon.before hooks right now is through following boilerplate.

def MyResource(ListCreateAPI):
    @falcon.before(action)
    def on_get(req, resp, **kwargs):
        super().on_get(req, resp, **kwargs)

    @falcon.before(action)
    def on_post(req, resp, **kwargs):
        super().on_post(req, resp, **kwargs)

So it is too verbose and also counterintuitive. Note that compatibility with falcon hooks is another feature we would like to have so we could support any falcon contrib package that provides hooks. This anyway should be discussed as a separate issue (see #31 )

Summary

Existing ways of handling custom context values are too verbose, require too much boilerplate and generally exploit **kwargs dictionary of list/retrieve/update/create methods that has completely different purpose in both falcon and graceful.

I in my opinion the best approach would be to expose somehow the Request.context object in the basic resources. The best solution should:

  • Solve all the problems of existing approaches (preferably by supporting every method explained earlier)
  • Allow to provide context in the most convenient way to the user. So it should not favour any of the existing approaches.
  • Do not introduce any backwards incompatibility (best) or at least be an opt-in feature with only minor backwards incompatibility when toggled (still fine).
@swistakm
Copy link
Owner Author

swistakm commented Nov 9, 2016

Idea - simply add context argument to every implementation of http method calls

Implementation:
Implementation would require modifying every HTTP method handler in the resource.mixins module. Example:

class ListMixin(BaseMixin):
    ...

    def on_get(self, req, resp, handler=None, **kwargs):
        kwargs['context'] = req.context

        self.handle(
            handler or self.list, req, resp, **kwargs
        )

As the effect developer may use three methods to provide custom per-request context variables:

  • using HTTP method overrides

    class MyResource(ListAPI):
        def on_get(req, resp, **kwargs):
            req.context['session'] = Session()
            super().on_get(req, resp, **kwargs)
    
        def list(self, params, meta, context, **kwargs):
            # context object content available here
            ...
  • using middleware

    class SessionMiddleware:
        def process_resource(self, req, resp, resource, params):
            req.context['session'] = Session()
  • using hooks:

    def create_session(req, resp, resource, params):
        req.context['session'] = Session()
    
    @falcon.before(create_session)
    class MyResource(ListAPI):
        def on_get(req, resp, **kwargs):
            req.context['session'] = Session()
            super().on_get(req, resp, **kwargs)
    
        def list(self, params, meta, context, **kwargs):
            # context object content available here
            ...

Pros of this approach are:

  • simple implementation
  • low risk of breaking backwards incompatibility
  • compatible with existing techniques of providing context in falcon
  • compatible with any existing falcon extension built using hooks or middleware

Also if we improve support for falcon hooks (per-method falcon hooks with falcon.before) this solution will still work as expected.

Backward compatibility concerns:

Good practice is to allow passing any set of keyword arguments to resource manipulation methods using generic and basic resources in graceful. Anyway we cannot be sure that every developer uses this approach. We should expect that graceful users sometimes do following:

class MyResourceList(ListAPI):
    def list(params, meta):
        pass

# or

class MyResourceItem(RetrieveAPI):
    def retrieve(params, meta, object_id):  # <= object id from URI template
        pass

We could ensure backwards compatibility by using class keyword arguments:

class MyResourceItemWithCtx(RetrieveAPI, with_context=True):
    def retrieve(params, meta, context, **kwargs):
        pass

class MyResourceItemWithoutCtx(RetrieveAPI, with_context=False):
    def retrieve(params, meta, **kwargs):
        pass

This is feature of Python 3 but we do not target anything below Python 3.3. Of course default for with_context for 0.x branch would be False and for 1.x would be True

@swistakm swistakm added this to the 0.3.0 milestone Nov 14, 2016
swistakm added a commit that referenced this issue Nov 16, 2016
initial support of optional contexts (refs #32)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants