Skip to main content

Dependency Injection and Security

Dependency injection in this codebase is centered around the Depends and Security classes located in fastapi/params.py. These tools allow for the modularization of shared logic—such as database sessions, authentication, and configuration—by injecting them directly into path operation functions.

The Dependency Injection Mechanism

The primary way to inject logic is through the Depends class. In modern usage within this project, it is typically paired with Python's Annotated type hint to keep the function signature clean and compatible with static analysis tools.

When a dependency is declared, FastAPI manages its execution lifecycle. By default, dependencies are cached within the scope of a single request. If multiple components in the same request require the same dependency, it is executed only once, and the result is reused.

Caching and Performance

The use_cache parameter in Depends (defaulting to True) controls this behavior. This is particularly useful for expensive operations like database connections. However, there are scenarios where you might want to force re-execution.

As demonstrated in tests/test_dependency_cache.py, disabling the cache ensures that every injection point triggers a fresh call to the dependency:

async def dep_counter():
counter_holder["counter"] += 1
return counter_holder["counter"]

@app.get("/sub-counter-no-cache/")
async def get_sub_counter_no_cache(
subcount: int = Depends(super_dep),
count: int = Depends(dep_counter, use_cache=False),
):
return {"counter": count, "subcounter": subcount}

In this example, if super_dep also depended on dep_counter, the count parameter would receive a different value than what super_dep received because use_cache=False was specified.

Lifecycle Management with Scopes

A significant design feature in this implementation is the introduction of the scope parameter for dependencies, specifically for those using the yield pattern. The scope can be either "function" or "request".

Function vs. Request Scope

The choice of scope determines when the "teardown" logic (the code after the yield) is executed:

  • function scope: The dependency is torn down as soon as the path operation function returns.
  • request scope: The dependency is torn down only after the entire request is finished, including after the response has been sent to the client.

This distinction is critical when using StreamingResponse. If a dependency provides a resource (like a database session) that the stream needs to access, using function scope would close the session before the stream even starts.

As seen in tests/test_dependency_yield_scope.py:

def dep_session() -> Any:
s = Session()
yield s
s.open = False

SessionFuncDep = Annotated[Session, Depends(dep_session, scope="function")]
SessionRequestDep = Annotated[Session, Depends(dep_session, scope="request")]

@app.get("/two-scopes")
def get_stream_session(
function_session: SessionFuncDep, request_session: SessionRequestDep
) -> Any:
def iter_data():
# function_session.open will be False here
# request_session.open will be True here
yield json.dumps(
{"func_is_open": function_session.open, "req_is_open": request_session.open}
)

return StreamingResponse(iter_data())

Scope Constraints

The implementation enforces a strict hierarchy: a dependency with request scope cannot depend on a dependency with function scope. Attempting to do so will result in a FastAPIError during application startup. This prevents scenarios where a long-lived resource tries to use a resource that has already been disposed of.

Security and Scopes

The Security class is a specialized subclass of Depends designed for authentication and authorization. It adds a scopes parameter, which is used to define specific permissions (often OAuth2 scopes) required for a path operation.

Integrating SecurityScopes

When using Security, the dependency function can request a SecurityScopes object. This object contains the list of all scopes required by the current dependency and any of its dependents.

A common pattern in docs_src/security/tutorial005_an_py310.py shows how to enforce these scopes:

async def get_current_user(
security_scopes: SecurityScopes,
token: Annotated[str, Depends(oauth2_scheme)]
):
# ... logic to decode token ...
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return user

@app.get("/users/me/items/")
async def read_own_items(
current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])],
):
return [{"item_id": "Foo", "owner": current_user.username}]

Design Tradeoffs in Security

It is important to note that the Security class itself does not perform the scope validation. Instead, it acts as a metadata carrier. The responsibility for inspecting the SecurityScopes and raising an HTTPException lies entirely within the dependency function (e.g., get_current_user).

This design provides maximum flexibility, allowing developers to implement complex permission logic (like hierarchical scopes or dynamic permissions) that would be difficult to capture in a purely declarative syntax. However, it requires the developer to be disciplined in ensuring that every security dependency actually checks the provided scopes.