Compare commits
3 Commits
3d24b53fdf
...
f94f0f5134
Author | SHA1 | Date | |
---|---|---|---|
f94f0f5134 | |||
7ac76883fc | |||
12f6c726c9 |
@ -1,3 +1,5 @@
|
|||||||
|
from spiderweb import HttpResponse
|
||||||
|
|
||||||
# writing your own middleware
|
# writing your own middleware
|
||||||
|
|
||||||
Sometimes you want to run the same code on every request or every response (or both!). Lots of processing happens in the middleware layer, and if you want to write your own, all you have to do is write a quick class and put it in a place that Spiderweb can find it. A piece of middleware only needs two things to be successful:
|
Sometimes you want to run the same code on every request or every response (or both!). Lots of processing happens in the middleware layer, and if you want to write your own, all you have to do is write a quick class and put it in a place that Spiderweb can find it. A piece of middleware only needs two things to be successful:
|
||||||
@ -55,13 +57,19 @@ Unlike `process_request`, returning a value here doesn't change anything. We're
|
|||||||
|
|
||||||
This is a helper function that is available for you to override; it's not often used by middleware, but there are some ([like the pydantic middleware](middleware/pydantic.md)) that call `on_error` when there is a validation failure.
|
This is a helper function that is available for you to override; it's not often used by middleware, but there are some ([like the pydantic middleware](middleware/pydantic.md)) that call `on_error` when there is a validation failure.
|
||||||
|
|
||||||
## post_process(self, request: Request, rendered_response: str) -> str:
|
## post_process(self, request: Request, response: HttpResponse, rendered_response: str) -> str:
|
||||||
|
|
||||||
> New in 1.3.0!
|
> New in 1.3.0!
|
||||||
|
|
||||||
After `process_request` and `process_response` run, the response is rendered out into the raw text that is going to be sent to the client. Right before that happens, `post_process` is called on each middleware in the same order as `process_response` (so the closer something is to the beginning of the middleware list, the more important it is).
|
After `process_request` and `process_response` run, the response is rendered out into the raw text that is going to be sent to the client. Right before that happens, `post_process` is called on each middleware in the same order as `process_response` (so the closer something is to the beginning of the middleware list, the more important it is).
|
||||||
|
|
||||||
Note that this function *must* return something. Each invocation of `post_process` overwrites the entire output of the response, so make sure to return everything that you want to send. For example, here's a middleware that ~~breaks~~ adjusts the capitalization of the response and also demonstrates passing variables into the middleware:
|
There are three things passed to `post_process`:
|
||||||
|
|
||||||
|
- `request`: the request object. It's provided here purely for reference purposes; while you can technically change it here, it won't have any effect on the response.
|
||||||
|
- `response`: the response object. The full HTML of the response has already been rendered, but the headers can still be modified here. This object can be modified in place, like in `process_response`.
|
||||||
|
- `rendered_response`: the full HTML of the response as a string. This is the final output that will be sent to the client. Every instance of `post_process` must return the full HTML of the response, so if you want to make changes, you'll need to return the modified string.
|
||||||
|
|
||||||
|
Note that this function *must* return the full HTML of the response (provided at the start as `rendered_response`. Each invocation of `post_process` overwrites the entire output of the response, so make sure to return everything that you want to send. For example, here's a middleware that ~~breaks~~ adjusts the capitalization of the response and also demonstrates passing variables into the middleware and modifies the headers with the type of transformation:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import random
|
import random
|
||||||
@ -74,7 +82,7 @@ from spiderweb.exceptions import ConfigError
|
|||||||
class CaseTransformMiddleware(SpiderwebMiddleware):
|
class CaseTransformMiddleware(SpiderwebMiddleware):
|
||||||
# this breaks everything, but it's hilarious so it's worth it.
|
# this breaks everything, but it's hilarious so it's worth it.
|
||||||
# Blame Sam.
|
# Blame Sam.
|
||||||
def post_process(self, request: Request, rendered_response: str) -> str:
|
def post_process(self, request: Request, response: HttpResponse, rendered_response: str) -> str:
|
||||||
valid_options = ["spongebob", "random"]
|
valid_options = ["spongebob", "random"]
|
||||||
# grab the value from the extra data passed into the server object
|
# grab the value from the extra data passed into the server object
|
||||||
# during instantiation
|
# during instantiation
|
||||||
@ -86,12 +94,14 @@ class CaseTransformMiddleware(SpiderwebMiddleware):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if method == "spongebob":
|
if method == "spongebob":
|
||||||
|
response.headers["X-Case-Transform"] = "spongebob"
|
||||||
return "".join(
|
return "".join(
|
||||||
char.upper()
|
char.upper()
|
||||||
if i % 2 == 0
|
if i % 2 == 0
|
||||||
else char.lower() for i, char in enumerate(rendered_response)
|
else char.lower() for i, char in enumerate(rendered_response)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
response.headers["X-Case-Transform"] = "random"
|
||||||
return "".join(
|
return "".join(
|
||||||
char.upper()
|
char.upper()
|
||||||
if random.random() > 0.5
|
if random.random() > 0.5
|
||||||
|
@ -32,9 +32,15 @@ class ExplodingMiddleware(SpiderwebMiddleware):
|
|||||||
class CaseTransformMiddleware(SpiderwebMiddleware):
|
class CaseTransformMiddleware(SpiderwebMiddleware):
|
||||||
# this breaks everything, but it's hilarious so it's worth it.
|
# this breaks everything, but it's hilarious so it's worth it.
|
||||||
# Blame Sam.
|
# Blame Sam.
|
||||||
def post_process(self, request: Request, rendered_response: str) -> str:
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
valid_options = ["spongebob", "random"]
|
valid_options = ["spongebob", "random"]
|
||||||
method = self.server.extra_data.get("case_transform_middleware_type", "spongebob")
|
# grab the value from the extra data passed into the server object
|
||||||
|
# during instantiation
|
||||||
|
method = self.server.extra_data.get(
|
||||||
|
"case_transform_middleware_type", "spongebob"
|
||||||
|
)
|
||||||
if method not in valid_options:
|
if method not in valid_options:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
f"Invalid method '{method}' for CaseTransformMiddleware."
|
f"Invalid method '{method}' for CaseTransformMiddleware."
|
||||||
@ -42,10 +48,14 @@ class CaseTransformMiddleware(SpiderwebMiddleware):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if method == "spongebob":
|
if method == "spongebob":
|
||||||
|
response.headers["X-Case-Transform"] = "spongebob"
|
||||||
return "".join(
|
return "".join(
|
||||||
char.upper() if i % 2 == 0 else char.lower() for i, char in enumerate(rendered_response)
|
char.upper() if i % 2 == 0 else char.lower()
|
||||||
|
for i, char in enumerate(rendered_response)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
response.headers["X-Case-Transform"] = "random"
|
||||||
return "".join(
|
return "".join(
|
||||||
char.upper() if random.random() > 0.5 else char for char in rendered_response
|
char.upper() if random.random() > 0.5 else char
|
||||||
|
for char in rendered_response
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "spiderweb-framework"
|
name = "spiderweb-framework"
|
||||||
version = "1.3.0"
|
version = "1.3.1"
|
||||||
description = "A small web framework, just big enough for a spider."
|
description = "A small web framework, just big enough for a spider."
|
||||||
authors = ["Joe Kaufeld <opensource@joekaufeld.com>"]
|
authors = ["Joe Kaufeld <opensource@joekaufeld.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -2,7 +2,7 @@ from peewee import DatabaseProxy
|
|||||||
|
|
||||||
DEFAULT_ALLOWED_METHODS = ["POST", "GET", "PUT", "PATCH", "DELETE"]
|
DEFAULT_ALLOWED_METHODS = ["POST", "GET", "PUT", "PATCH", "DELETE"]
|
||||||
DEFAULT_ENCODING = "UTF-8"
|
DEFAULT_ENCODING = "UTF-8"
|
||||||
__version__ = "1.3.0"
|
__version__ = "1.3.1"
|
||||||
|
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||||
REGEX_COOKIE_NAME = r"^[a-zA-Z0-9\s\(\)<>@,;:\/\\\[\]\?=\{\}\"\t]*$"
|
REGEX_COOKIE_NAME = r"^[a-zA-Z0-9\s\(\)<>@,;:\/\\\[\]\?=\{\}\"\t]*$"
|
||||||
|
@ -36,7 +36,7 @@ class LocalServerMixin:
|
|||||||
|
|
||||||
def start(self, blocking=False):
|
def start(self, blocking=False):
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
self.log.info(f"Starting server on {self.addr}:{self.port}")
|
self.log.info(f"Starting server on http://{self.addr}:{self.port}")
|
||||||
self.log.info("Press CTRL+C to stop the server.")
|
self.log.info("Press CTRL+C to stop the server.")
|
||||||
self._server = self.create_server()
|
self._server = self.create_server()
|
||||||
self._thread = threading.Thread(target=self._server.serve_forever)
|
self._thread = threading.Thread(target=self._server.serve_forever)
|
||||||
|
@ -201,6 +201,17 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||||||
|
|
||||||
def fire_response(self, start_response, request: Request, resp: HttpResponse):
|
def fire_response(self, start_response, request: Request, resp: HttpResponse):
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
rendered_output: str = resp.render()
|
||||||
|
final_output: str | list[str] = self.post_process_middleware(
|
||||||
|
request, resp, rendered_output
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Fatal error!")
|
||||||
|
self.log.error(e)
|
||||||
|
self.log.error(traceback.format_exc())
|
||||||
|
return [f"Internal Server Error: {e}".encode(DEFAULT_ENCODING)]
|
||||||
|
|
||||||
status = get_http_status_by_code(resp.status_code)
|
status = get_http_status_by_code(resp.status_code)
|
||||||
cookies = []
|
cookies = []
|
||||||
varies = []
|
varies = []
|
||||||
@ -218,24 +229,13 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||||||
for v in varies:
|
for v in varies:
|
||||||
headers.append(("vary", str(v)))
|
headers.append(("vary", str(v)))
|
||||||
|
|
||||||
start_response(status, headers)
|
|
||||||
|
|
||||||
try:
|
|
||||||
rendered_output: str = resp.render()
|
|
||||||
final_output: str | list[str] = self.post_process_middleware(request, rendered_output)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error("Fatal error!")
|
|
||||||
self.log.error(e)
|
|
||||||
self.log.error(traceback.format_exc())
|
|
||||||
return [f"Internal Server Error: {e}".encode(DEFAULT_ENCODING)]
|
|
||||||
|
|
||||||
if not isinstance(final_output, list):
|
if not isinstance(final_output, list):
|
||||||
final_output = [final_output]
|
final_output = [final_output]
|
||||||
encoded_resp = [
|
encoded_resp = [
|
||||||
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
|
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
|
||||||
for chunk in final_output
|
for chunk in final_output
|
||||||
]
|
]
|
||||||
|
start_response(status, headers)
|
||||||
return encoded_resp
|
return encoded_resp
|
||||||
except APIError:
|
except APIError:
|
||||||
raise
|
raise
|
||||||
|
@ -61,13 +61,17 @@ class MiddlewareMixin:
|
|||||||
self.middleware.remove(middleware)
|
self.middleware.remove(middleware)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def post_process_middleware(self, request: Request, response: str) -> str:
|
def post_process_middleware(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
# run them in reverse order, same as process_response. The top of the middleware
|
# run them in reverse order, same as process_response. The top of the middleware
|
||||||
# stack should be the first and last middleware to run.
|
# stack should be the first and last middleware to run.
|
||||||
for middleware in reversed(self.middleware):
|
for middleware in reversed(self.middleware):
|
||||||
try:
|
try:
|
||||||
response = middleware.post_process(request, response)
|
rendered_response = middleware.post_process(
|
||||||
|
request, response, rendered_response
|
||||||
|
)
|
||||||
except UnusedMiddleware:
|
except UnusedMiddleware:
|
||||||
self.middleware.remove(middleware)
|
self.middleware.remove(middleware)
|
||||||
continue
|
continue
|
||||||
return response
|
return rendered_response
|
||||||
|
@ -29,9 +29,7 @@ class SpiderwebMiddleware:
|
|||||||
# the request and return a response immediately.
|
# the request and return a response immediately.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def process_response(
|
def process_response(self, request: Request, response: HttpResponse) -> None:
|
||||||
self, request: Request, response: HttpResponse
|
|
||||||
) -> None:
|
|
||||||
# This method is called after the view has returned a response. You can modify
|
# This method is called after the view has returned a response. You can modify
|
||||||
# the response in this method. The response will be returned to the client after
|
# the response in this method. The response will be returned to the client after
|
||||||
# all middleware has been processed.
|
# all middleware has been processed.
|
||||||
@ -43,7 +41,9 @@ class SpiderwebMiddleware:
|
|||||||
# will be re-raised.
|
# will be re-raised.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def post_process(self, request: Request, rendered_response: str) -> str:
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
# This method is called after all the middleware has been processed and receives
|
# This method is called after all the middleware has been processed and receives
|
||||||
# the final rendered response in str form. You can modify the response here.
|
# the final rendered response in str form. You can modify the response here.
|
||||||
return rendered_response
|
return rendered_response
|
||||||
|
@ -19,5 +19,22 @@ class InterruptingMiddleware(SpiderwebMiddleware):
|
|||||||
|
|
||||||
|
|
||||||
class PostProcessingMiddleware(SpiderwebMiddleware):
|
class PostProcessingMiddleware(SpiderwebMiddleware):
|
||||||
def post_process(self, request: Request, response: str) -> str:
|
def post_process(
|
||||||
return response + " Moo!"
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
return rendered_response + " Moo!"
|
||||||
|
|
||||||
|
|
||||||
|
class PostProcessingWithHeaderManipulation(SpiderwebMiddleware):
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
response.headers["X-Moo"] = "true"
|
||||||
|
return rendered_response
|
||||||
|
|
||||||
|
|
||||||
|
class ExplodingPostProcessingMiddleware(SpiderwebMiddleware):
|
||||||
|
def post_process(
|
||||||
|
self, request: Request, response: HttpResponse, rendered_response: str
|
||||||
|
) -> str:
|
||||||
|
raise UnusedMiddleware("Unfinished!")
|
||||||
|
@ -314,6 +314,41 @@ def test_post_process_middleware():
|
|||||||
assert app(environ, start_response) == [bytes("Hi! Moo!", DEFAULT_ENCODING)]
|
assert app(environ, start_response) == [bytes("Hi! Moo!", DEFAULT_ENCODING)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_process_header_manip():
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
middleware=[
|
||||||
|
"spiderweb.tests.middleware.PostProcessingWithHeaderManipulation",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi!", DEFAULT_ENCODING)]
|
||||||
|
assert start_response.get_headers()["x-moo"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unused_post_process_middleware():
|
||||||
|
app, environ, start_response = setup(
|
||||||
|
middleware=[
|
||||||
|
"spiderweb.tests.middleware.ExplodingPostProcessingMiddleware",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_route("/", text_view)
|
||||||
|
|
||||||
|
environ["HTTP_USER_AGENT"] = "hi"
|
||||||
|
environ["REMOTE_ADDR"] = "/"
|
||||||
|
environ["REQUEST_METHOD"] = "GET"
|
||||||
|
|
||||||
|
assert app(environ, start_response) == [bytes("Hi!", DEFAULT_ENCODING)]
|
||||||
|
# make sure it kicked out the middleware and isn't just ignoring it
|
||||||
|
assert len(app.middleware) == 0
|
||||||
|
|
||||||
|
|
||||||
class TestCorsMiddleware:
|
class TestCorsMiddleware:
|
||||||
# adapted from:
|
# adapted from:
|
||||||
# https://github.com/adamchainz/django-cors-headers/blob/main/tests/test_middleware.py
|
# https://github.com/adamchainz/django-cors-headers/blob/main/tests/test_middleware.py
|
||||||
|
Loading…
Reference in New Issue
Block a user