Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.6.1] - 2026-??-??
## [2.6.1] - 2026-02-22 :cat:

- Fix missing escaping in `multipart/form-data` filenames and content-disposition headers.
- Fix [#193](https://github.com/Neoteroi/BlackSheep/issues/193), adding support for [`a2wsgi`](https://github.com/abersheeran/a2wsgi).
- `raw_path` is optional in ASGI specification. If not present, now the `instantiate_request` method automatically obtains it from `path` and sets it in the scope.
- Static files are now served with `Content-Length` header instead of `Transfer-Encoding: chunked` when file size is known. This improves compatibility with `WSGI` servers via `a2wsgi`.
- Automatically run the `Application` start logic if the `__call__` method is called with **http** or **websocket** messages. This is useful when `lifespan` events are not supported, like when using `WSGI`.
- Fix the issue [#396](https://github.com/Neoteroi/BlackSheep/issues/396). Requests for mounted apps are redirected to a directory (ending with '/') only if the request includes a `Sec-Fetch-Mode: navigate`, which is used by modern browsers to inform the server the request is for navigation. This way, mounted apps serving HTML documents containing relative links work properly (their path must end with `/`). Reported by @satori1995.
- Fix the issue [#256](https://github.com/Neoteroi/BlackSheep/issues/256): add support
for configuring names for routes, and for obtaining URLs by route name. Example:

```python
from blacksheep.routing import URLResolver


@app.router.get("/cats/{cat_id}", name="cat-detail")
async def get_cat_detail(cat_id: int) -> Response:
return Response(200)

@app.router.get("/redirect")
async def redirect_handler(url_resolver: URLResolver) -> Response:
return redirect(url_resolver.url_for("cat-detail", cat_id="42"))
```

## [2.6.0] - 2026-02-15 :cupid:

Expand Down
1 change: 1 addition & 0 deletions blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from .server.responses import unauthorized as unauthorized
from .server.routing import Route as Route
from .server.routing import RouteException as RouteException
from .server.routing import RouteNotFound as RouteNotFound
from .server.routing import Router as Router
from .server.routing import RoutesRegistry as RoutesRegistry
from .server.routing import connect as connect
Expand Down
16 changes: 13 additions & 3 deletions blacksheep/server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ async def start(self):
if not self.router.fallback:
self.router.fallback = default_fallback

self.register_default_di_types()
self.router.apply_routes()

if self.on_start:
Expand All @@ -773,6 +774,14 @@ async def stop(self):
self.started = False
self._started_complete.clear()

def register_default_di_types(self):
"""
Registers default DI types in the Application DI controller.
By default, only the Application object is registered in the DI controller.
"""
if Application not in self._services:
self._services.register(Application, instance=self)

async def _handle_lifespan(self, receive, send) -> None:
message = await receive()
assert message["type"] == "lifespan.startup"
Expand Down Expand Up @@ -912,7 +921,9 @@ def handle_mount_path(self, scope, route_match):
# Update root_path per the ASGI spec: the child app must know its mount
# prefix so it can generate correct absolute URLs (analogous to WSGI
# SCRIPT_NAME). root_path = parent root_path + the stripped mount prefix.
mount_prefix = scope["path"][: -len(tail)] if tail != "/" else scope["path"].rstrip("/")
mount_prefix = (
scope["path"][: -len(tail)] if tail != "/" else scope["path"].rstrip("/")
)
scope["root_path"] = scope.get("root_path", "") + mount_prefix

scope["path"] = tail
Expand Down Expand Up @@ -952,8 +963,7 @@ async def _handle_redirect_to_mount_root(self, scope, send):
)
await send_asgi_response(response, send)

def _get_request_scope(self, scope):
...
def _get_request_scope(self, scope): ...

async def __call__(self, scope, receive, send):
if scope["type"] == "lifespan":
Expand Down
29 changes: 27 additions & 2 deletions blacksheep/server/bindings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
https://www.neoteroi.dev/blacksheep/binders/
"""

import warnings
from abc import abstractmethod
from collections.abc import Iterable as IterableAbc
from functools import partial
Expand All @@ -27,10 +26,11 @@
from guardpost import Identity
from rodi import CannotResolveTypeException, ContainerProtocol

from blacksheep import Request
from blacksheep.contents import FormPart
from blacksheep.exceptions import BadRequest
from blacksheep.messages import Request
from blacksheep.server.bindings.converters import class_converters, converters
from blacksheep.server.routing import Router, URLResolver
from blacksheep.server.websocket import WebSocket
from blacksheep.url import URL

Expand Down Expand Up @@ -870,3 +870,28 @@ class FilesBinder(Binder):

async def get_value(self, request: Request) -> list[FormPart]:
return await request.files()


class URLResolverBinder(Binder):
"""
Binder that injects a URLResolver into request handlers.
The URLResolver is constructed per-request from the singleton Router and the
current Request, so it correctly reflects the request's base_path for
generating URLs relative to the mount root.
"""

type_alias = URLResolver

def __init__(self, router: Router, implicit: bool = True):
super().__init__(URLResolver, implicit=implicit)
self._router = router

@classmethod
def from_alias(cls, services: ContainerProtocol) -> "URLResolverBinder":
from blacksheep.server import Application

app: Application = services.resolve(Application)
return cls(app.router)

async def get_value(self, request: Request) -> URLResolver:
return URLResolver(self._router, request)
2 changes: 1 addition & 1 deletion blacksheep/server/files/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def serve_files_dynamic(
*,
discovery: bool,
cache_time: int,
extensions: Set[str | None],
extensions: Set[str] | None,
root_path: str,
index_document: str | None,
fallback_document: str | None,
Expand Down
Loading
Loading