Repository: tiangolo/fastapi

Issue:
route_class not preserved when calling APIRouter.include_router
**Describe the bug**

When calling `APIRouter.include_router`, the class of the included routes should be preserved. Right now, this is not the case -- the `route_class` of the parent router is used. This means that the `route_class` is lost on inclusion if one isn't careful. Worse, it means that even if you manually create a router with multiple route-types present, there is no way to include that router in another without changing the route type of some of the routes.

See the `include_typed_router` function from #521 for an example of how this currently requires a workaround.

I have addressed this in the short PR #538, but wanted to create an issue for discussion in case there is any reason to preserve the current behavior.


Discussion:

Resolving patch:
diff --git a/fastapi/routing.py b/fastapi/routing.py
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -345,8 +345,10 @@ def add_api_route(
         include_in_schema: bool = True,
         response_class: Type[Response] = None,
         name: str = None,
+        route_class_override: Optional[Type[APIRoute]] = None,
     ) -> None:
-        route = self.route_class(
+        route_class = route_class_override or self.route_class
+        route = route_class(
             path,
             endpoint=endpoint,
             response_model=response_model,
@@ -484,6 +486,7 @@ def include_router(
                     include_in_schema=route.include_in_schema,
                     response_class=route.response_class or default_response_class,
                     name=route.name,
+                    route_class_override=type(route),
                 )
             elif isinstance(route, routing.Route):
                 self.add_route(


Test patch:
diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py
new file mode 100644
--- /dev/null
+++ b/tests/test_custom_route_class.py
@@ -0,0 +1,114 @@
+import pytest
+from fastapi import APIRouter, FastAPI
+from fastapi.routing import APIRoute
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+class APIRouteA(APIRoute):
+    x_type = "A"
+
+
+class APIRouteB(APIRoute):
+    x_type = "B"
+
+
+class APIRouteC(APIRoute):
+    x_type = "C"
+
+
+router_a = APIRouter(route_class=APIRouteA)
+router_b = APIRouter(route_class=APIRouteB)
+router_c = APIRouter(route_class=APIRouteC)
+
+
+@router_a.get("/")
+def get_a():
+    return {"msg": "A"}
+
+
+@router_b.get("/")
+def get_b():
+    return {"msg": "B"}
+
+
+@router_c.get("/")
+def get_c():
+    return {"msg": "C"}
+
+
+router_b.include_router(router=router_c, prefix="/c")
+router_a.include_router(router=router_b, prefix="/b")
+app.include_router(router=router_a, prefix="/a")
+
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/a/": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    }
+                },
+                "summary": "Get A",
+                "operationId": "get_a_a__get",
+            }
+        },
+        "/a/b/": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    }
+                },
+                "summary": "Get B",
+                "operationId": "get_b_a_b__get",
+            }
+        },
+        "/a/b/c/": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    }
+                },
+                "summary": "Get C",
+                "operationId": "get_c_a_b_c__get",
+            }
+        },
+    },
+}
+
+
+@pytest.mark.parametrize(
+    "path,expected_status,expected_response",
+    [
+        ("/a", 200, {"msg": "A"}),
+        ("/a/b", 200, {"msg": "B"}),
+        ("/a/b/c", 200, {"msg": "C"}),
+        ("/openapi.json", 200, openapi_schema),
+    ],
+)
+def test_get_path(path, expected_status, expected_response):
+    response = client.get(path)
+    assert response.status_code == expected_status
+    assert response.json() == expected_response
+
+
+def test_route_classes():
+    routes = {}
+    r: APIRoute
+    for r in app.router.routes:
+        routes[r.path] = r
+    assert routes["/a/"].x_type == "A"
+    assert routes["/a/b/"].x_type == "B"
+    assert routes["/a/b/c/"].x_type == "C"