aiohttp middleware and updating the url for a request - python

I am looking for help with an aiohttp middleware I'm working on that will automatically add trailing slashes to a uri if it's missing. Aiohttp requires that when you define a url, you define two routes for each path in your list of routes, one with a trailing slash and one without. They have an example middleware that addresses this by finding if the uri does not end with a / and adding it if not, but then they use an http 302 redirect to tell the client to go to the new uri. Otherwise a relative uri like /endpoint will 404 while /endpoint/ will work.
This is their middleware:
https://github.com/aio-libs/aiohttp/blob/master/aiohttp/web_middlewares.py
Using their middleware as a basis, I'm trying to accomplish the same thing but without a redirect on the client side. I want the handling to only be on the server. My current attempt at this is shown below and seems to update the request in flight, but I still get a 404:
from aiohttp.web_urldispatcher import SystemRoute
def middleware(f):
f.__middleware_version__ = 1
return f
def trailing_slashes():
#middleware
async def impl(request, handler):
if isinstance(request.match_info.route, SystemRoute):
rel_url = str(request.rel_url)
if '?' in rel_url:
path, query = rel_url.split('?', 1)
query = f'?{query}'
else:
path = rel_url
query = ''
if not path.endswith('/'):
rel_url = f'{path}/{query}'
request = request.clone(rel_url=rel_url)
return await handler(request)
return impl
This is a class that implements the middleware and illustrates the problem.
from aiohttp import web
import slashes_mw
class ClassName(object):
def __init__(self):
self.app = web.Application(middlewares=[slashes_mw.trailing_slashes()])
self.app.add_routes(self.get_routes())
web.run_app(self.app, port=80, host='0.0.0.0')
def get_routes(self):
return [
web.get('/', self.handler),
web.get('/{name}/', self.handler)
]
def handler(self, request):
return web.Response(text='hello')
ClassName()
Any ideas? Thanks for your help.

I think the redirect approach that they propose is quite correct, as the routes are simply not the same. But you might try to just register the second "version" of the route as well:
def get_routes(self):
return [
web.get('/', self.handler),
web.get('/{name}/', self.handler),
web.get('/{name}', self.handler)
]
This should handle your problem server-side (NOT TESTED).
As I understand the framework, your approach does not work because the specific handler is selected BEFORE the different middlewares are called. If you call the handler function in the middleware, there is no "routing" lookup done anymore. Or at least that is how I understand their documentation: https://docs.aiohttp.org/en/stable/web_advanced.html#middlewares
Might be interesting as well: https://docs.aiohttp.org/en/stable/web_quickstart.html#variable-resources

Related

How to set "/*" path to capture all routes in FastAPI?

Like in ExpressJS, app.get("/*") works for all routes.
My code -
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
#app.get('/*')
def user_lost():
return "Sorry You Are Lost !"
I tried it but the webpage result shows {"detail":"Not Found"}
How can I do the same in FastApi?
You can use /{full_path} to capture all routes in one path (See documentation).
#app.route("/{full_path:path}")
async def capture_routes(request: Request, full_path: str):
...
I edited Yagiz's code to:
#app.get("/{full_path:path}")
async def capture_routes(request: Request, full_path: str):
...
Best explained in https://sureshdsk.dev/how-to-implement-catch-all-route-in-fast-api
The only additional comment is;
if you want to mix catch-all and handle certain paths like /hello or /api .. make sure that catch all method definition is at the last .... order of route definitions matter

Flask-RESTplus CORS request not adding headers into the response

I have an issue trying to setup CORS for my REST API.
I'm currently using the Flask-Restplus package. Here's what my endpoints look like :
#api_ns.route('/some/endpoint')
#api_ns.response(code=400, description='Bad Request.')
class AEndpointResource(Resource):
#api_ns.param(**api_req_fields.POST_DOC)
#api_ns.expect(POST_REQUIRED_BODY)
#api_ns.marshal_with(code=201,
fields=my_api_models.MyEndpointResponse.get_serializer(),
description=my_api_models.MyEndpointResponse.description)
def post(self) -> Tuple[my_api_models.MyEndpointResponse, int]:
"""
The post body
"""
# Some logic here
return response, 200
If I code a small javascript snippet and I try to launch it in a browser, I will get an error because there's no CORS headers. I'm seeing the Flask-Restplus is already handling the OPTIONS request without me telling him anything. (This makes sense according to this link, mentioning that since Flask 0.6, the OPTIONS requests are handled automatically)
My problem is that even when I try to decorate my endpoint using :
from flask-restplus import cors # <--- Adding this import
...
class AnEndpointResource(Resource):
...
#my_other_decorators
...
#cors.crossdomain(origin='*') # <--- Adding this new decorator on my endpoint
def post(self) -> Tuple[my_api_models.MyEndpointResponse, int]:
...
Nothing changes and I still get the same result as before. I get an HTTP 200 from the OPTIONS request automatically handled as before, but I don't see my new headers (i.e. Access-Control-Allow-Origin) in the response.
Am I missing something ?
Using Flask-CORS, it works:
from flask import Flask, request
from flask_restplus import Resource, Api, fields
from flask_cors import CORS
# configuration
DEBUG = True
# instantiate the app
app = Flask(__name__)
api = Api(app)
app.config.from_object(__name__)
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
When doing local testing on my laptop and in my browser, I was able to solve the cors problem by adding the header in the response.
before: return state, 200
after: return state, 200, {'Access-Control-Allow-Origin': '*'}
I tested and the headers you are looking for are added to the response to the subsequent GET request. The decorator #cors.crossdomain has an option automatic_options which is set to be True by default. This means your OPTIONS request will still be handled automatically.
See this test to check how it should work.
The flask_restplus.cors module is not documented so not sure if you should use it.
I had a CORS problem as well and solved it this way:
from flask import Flask
from flask_restplus import Api
app = Flask('name')
api = Api(app)
// your api code here
#app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
return response
The problem is that flask-restplus only assigns CORS headers to the GET request. When the browser makes an OPTIONS request, this method isn't handled by a method on the Resource class. Then you get a flickering CORS header: one request it works, the next it doesn't, etc
When a 404 is raised, this also skips the CORS decorator.
A workaround for these bugs is using something like:
def exception_to_response(func):
#wraps(func)
def _exc_to_resp_decorator(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception as e:
return self.api.handle_error(e)
return _exc_to_resp_decorator
#api.route("/your-endpoint")
class SomeEndpoint(Resource):
#cors.crossdomain(origin='*')
def options(self):
# Make sure the OPTIONS request is also handled for CORS
# The "automatic_options" will handle this, no need to define a return here:
return
#api.expect(my_schema)
#api.response(200, "Success")
#cors.crossdomain(origin='*')
#exception_to_response
def get(self):
return {"json-fields": 123}, 200

Tornado routing to a "base" handler

I use tornado 4.5.2 with routing implementation.
My server have two versions of API, let them call base and fancy. So a client is able to use both of them:
GET /base/foo
GET /base/baz
GET /fancy/foo
GET /fancy/baz
However, some fancy handlers may not be implemented; In this case a base one should be used.
In example:
application = web.Application([
(r"/base/foo", handlers.BaseFooHandler, {"some": "settings"}),
(r"/base/baz", handlers.BaseBazHandler, {"some": "settings"}),
(r"/fancy/foo", handlers.FancyFooHandler, {"some": "settings"}),
])
when cilent requests GET /fancy/baz the BaseBazHandler should do the job.
How can I achieve that with tornado routing?
Since you're registering your routes using a decorator, you can create a custom router that will respond to all the unmatched/unregistered /fancy/.* routes. For this to work correctly, you'll have to register your router at the end.
That way your custom router will be matched only if there isn't already a /fancy/... route registered. So, that means the custom router class will need to do these things:
Check if a fallback BaseBazHandler exists or not.
If exists, forward the request to it.
Else, return a 404 error.
Before proceeding any further, you'll have to create a custom class to handle 404 requests. This is necessary because if not handler is found, then this is the easiest way to return a 404 error.
class Handle404(RequestHandler):
def get(self):
self.set_status(404)
self.write('404 Not Found')
Okay, now let's write the custom router:
from tornado.routing import Router
class MyRouter(Router):
def __init__(self, app):
self.app = app
def find_handler(self, request, **kwargs):
endpoint = request.path.split('/')[2] # last part of the path
fallback_handler = 'Base%sHandler' % endpoint.title()
# fallback_handler will look like this - 'BaseBazHandler'
# now check if the handler exists in the current file
try:
handler = globals()[fallback_handler]
except KeyError:
handler = Handle404
return self.app.get_handler_delegate(request, handler)
Finally, after you've added all other routes, you can register your custom router:
from tornado.routing import PathMatches
application.add_handlers(r'.*', # listen for all hosts
[
(PathMatches(r"/fancy/.*"), MyRouter(application)),
]
)
I should point out that MyRouter.find_handler, only check checks for handlers in the current module (file). Modify the code to search for handlers in different modules, if you want.

Routing with python Falcon

I am new to Falcon framework of python. I have a question regarding the usage of middleware class of Falcon. Is it wise to use custom routers and authentication of requests in the middleware or should this be handled only on the routing
**main.py**
import falcon
import falcon_jsonify
import root
from waitress import serve
if __name__ == "__main__":
app = falcon.API(
middleware=[falcon_jsonify.Middleware(help_messages=True),
root.customRequestParser()]
)
serve(app, host="0.0.0.0", port=5555)
root.py where I am planning to write the custom routes
import json
import falcon
class Home(object):
#classmethod
def getResponse(self):
return {"someValue": "someOtherValue"}
def process_request_path(path):
path = path.lstrip("/").split("/")
return path
class customRequestParser(object):
def process_request(self, req, resp):
print process_request_path(req.path)
I also saw examples using app = falcon.API(router=CustomRouter()). I saw a documentation on the falcon official documentation page - http://falcon.readthedocs.io/en/stable/api/routing.html
Please let me know if there are any references that I can look through.
To quote the Falcon Community FAQ
How do I authenticate requests?
Hooks and middleware components can be used together to authenticate and authorize requests. For example, a middleware component could be used to parse incoming credentials and place the results in req.context. Downstream components or hooks could then use this information to authorize the request, taking into account the user’s role and the requested resource.
Falcon's Hooks are decorators used on the either a particular request function (i.e. on_get) or on an entire class. They're great for validating incoming requests, so as the FAQ says, authentication could be done at this point.
Here's an (untested) example I knocked up:
def AuthParsingMiddleware(object):
def process_request(self, req, resp):
req.context['GodMode'] = req.headers.get('Auth-Token') == 'GodToken':
# Might need process_resource & process_response
def validate_god_mode(req, resp, resource, params):
if not req.context['GodMode']:
raise falcon.HTTPBadRequest('Not authorized', 'You are not god')
def GodLikeResource(object):
#falcon.before(validate_god_mode):
def on_get(self, req, resp):
resp.body = 'You have god mode; I prostrate myself'
app = falcon.API(
middleware=[falcon_jsonify.Middleware(help_messages=True),
AuthParsingMiddleware()]
)
app.add_route('/godlikeresource', GodLikeResource())
Or better...
There is a falcon-auth package.

Can a WSGI middleware modify a request body and then pass it along?

I'm trying to write a middleware that rewrites POST requests to a different method when it finds a "_method" parameter in the body. Someone on the Internet wrote this piece of code:
from werkzeug import Request
class MethodRewriteMiddleware(object):
def __init__(self, app, input_name='_method'):
self.app = app
self.input_name = input_name
def __call__(self, environ, start_response):
request = Request(environ)
if self.input_name in request.form:
method = request.form[self.input_name].upper()
if method in ['GET', 'POST', 'PUT', 'DELETE']:
environ['REQUEST_METHOD'] = method
return self.app(environ, start_response)
As I understand the code, it parses the form, retrieves a possible "_method" parameter and if it's found and whitelisted it will overwrite the current method. It works fine for DELETE requests and it rewrites the method with no problems. However, when I try to send a regular, non-rewritten POST this middleware makes the whole app hang. My best guess is that since I accessed the body in the middleware, the body is no longer available for the application so it hangs forever. However, this doesn't seem to impact rewritten requests, so the code deepest code path (checking the whitelist) works correctly but the other code path is somehow destroying/blocking the request.
I don't think it's relevant, but I'm mounting this middleware on top of a Flask app.
EDIT: I think trying to access the request from a handler in Flask is blocking. Does Flask use mutexes or something like that internally?
I'm not even sure how to debug this.
environ is a dictionary with a wsgi.input key that is a stream object. The manual way to do what you're looking for is to read the data, make any changes and then create a new string to replace the old stream in the environ dictionary. The example below (without any error handling or other important things) allows you to work with the request body:
def __call__(self, environ, start_response):
body = environ['wsgi.input'].read()
... do stuff to body ...
new_stream = io.ByteIO(modified_body)
environ['wsgi.input'] = new_stream
return self.app(environ, start_response)

Categories