Open sisp opened 4 years ago
Hi :wave:,
Thanks for using Falcon. The large amount of time and effort needed to maintain the project and develop new features is not sustainable without the generous financial support of community members like you.
Please consider helping us secure the future of the Falcon framework with a one-time or recurring donation.
Thank you for your support!
Hi @sisp , and thank you for reaching out to us on Gitter!
Could you add some concrete examples how the proposed changes would translate to Falcon (and Python)? (Thinking not everyone is necessarily familiar with the mentioned Golang and Node.js frameworks; neither do concepts always map exactly between different languages).
Sorry for the delay.
This is an example inspired by go-chi
:
def custom_middleware_factory(some_config, ...):
# notice that `next` and `handle` have the same signature
def middleware(next):
def handle(req, resp):
# do something before the request is handled, e.g. authenticate user
# the request object can be modified here as well, e.g. add context information
# call the next middleware
# can be omitted if the request should be aborted
next(req, resp)
# do something after the request has been handled, e.g. set a response header
return handle
return middleware
I'd avoid raising HTTP exceptions for creating error responses like raise HTTPUnauthorized()
altogether and rather set the proper response directly. Exceptions should be handled by the user and converted to proper HTTP responses. If an unexpected exception occurs, it should be handled by a "recovery" middleware, which catches any uncaught exceptions, returns HTTP 500
, and prevents the process from exiting. If you need to maintain a compatibility bridge, I guess each middleware call could be wrapped in a try
/except
block to convert HTTP exceptions to error responses, but I still think it's too much "magic" at the cost of transparency.
A few more (simplified) examples:
# access log middleware without configuration
def access_log_middleware(next):
def handle(req, resp):
time_start = time.time()
next(req, resp)
time_end = time.time()
logger.info('{0} {1} {2} - {3}sec'.format(req.method, req.relative_uri, resp.status[:3], time_end - time_start))
return handle
# basic auth middleware with fixed credentials and custom error handler/serializer
def basic_auth_middleware(username, password, error_handler):
def middleware(next):
def handle(req, resp):
token = req.get_header('Authorization')
try:
credentials = decode_token(token)
except BasicAuthDecodeError as exc:
# create custom error response according to the user's desired format
error_handler(req, resp, exc)
else:
if credentials['username'] != username or credentials['password] != password:
error_handler(req, resp, BasicAuthInvalidCredentialsError())
else:
req.context['auth'] = {'username': username, 'password': password}
next(req, resp)
return handle
return middleware
# recovery middleware
def recovery_middleware(next):
def handle(req, resp):
try:
next(req, resp)
except Exception as exc:
# log exception
logger.error('internal error: {}'.format(exc))
# write error response
# a custom error handler/serializer like in the `basic_auth_middleware` could also be used here
resp.status = falcon.HTTP_500
resp.body = 'An unexpected error occurred'
return handle
Then, a middleware stack can be added to an API:
def basic_auth_error_handler(req, resp, exc):
resp.status = falcon.HTTP_403
resp.body = str(exc) # or JSON or ...
api = falcon.API(middlewares=[
access_log_middleware,
recovery_middleware,
basic_auth_middleware('admin', 's3cr3t', lambda req, basic_auth_error_handler)
])
The above middleware concept uses higher-order functions. One level of nested functions could be removed by moving the next
argument to the handle
function:
def custom_middleware_factory(some_config, ...):
def handle(req, resp, next):
# do something before the request is handled, e.g. authenticate user
# the request object can be modified here as well, e.g. add context information
# call the next middleware
# can be omitted if the request should be aborted
next(req, resp)
# do something after the request has been handled, e.g. set a response header
return handle
The downside of this approach is that handle
and next
don't have the same signature anymore.
This higher-order function concept can also be implemented using classes:
class CustomMiddleware:
def __init__(self, some_config, ...):
self._some_config = some_config
# ...
def handle(self, req, resp, next):
# do something before the request is handled, e.g. authenticate user
# the request object can be modified here as well, e.g. add context information
# call the next middleware
# can be omitted if the request should be aborted
next(req, resp)
# do something after the request has been handled, e.g. set a response header
Still, what makes, e.g., go-chi
so powerful and composable is the fact that request handlers, middlewares, and routers all implement the standard Go http.Handler
interface. This way, routers can be composed and sub-routers and individual request handlers can have their own middlewares as needed (like hooks, but with support for dependency injection - something I consider extremely important and totally missing in the design of most other Python web frameworks like Flask). Resource-oriented design could be a convenience API on top of this highly flexible foundation.
Opinions?
Thanks for the examples!
Express.js is very similar, but IIRC next
does different things depending on the number of arguments (or what are the argument).
Does next
returns something in go-chi
? I don't remember if it did in express.js
.
In go-chi
, next
is of type http.Handler
:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
When in my example next(req, resp)
is called, in go-chi
next.ServeHTTP(w, r.withContext(ctx))
is called and it doesn't return anything. See an example in the go-chi
README.
This issue summarizes a discussion with @CaselIT and @vytas7 at https://gitter.im/falconry/dev on August 10 about adding an Express-style (see also
chi
and Negroni in Golang) API for Falcon middlewares. This issue is related to several others:This is how it started:
The main topics of discussion were about:
Possible performance degradation because of the more composable pattern
Backwards compatibility
Eventual deprecation of the current middleware(-like) options (middlewares, hooks, error handlers) in favor of a unified Express-style middleware (see
chi
for a very nice API)Next steps should include concrete examples of what a new middleware API could look like in Falcon.