Repository: litestar-org/litestar
Pull request #3363 (2024-04-10 03:39:48)

chore(rebase): rebase v3 onto the latest `main`
<!--
By submitting this pull request, you agree to:
- follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md)
- follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md)
- follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/)
-->
## Description

- rebase `v3.0` branch onto latest `main`

<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234

Ensure you are using a supported keyword to properly link an issue:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
-->
## Closes



Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000..92666fc9a
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,306 @@
+version: v1
+
+labels:
+  # -- types -------------------------------------------------------------------
+  - label: 'type/feat'
+    sync: true
+    matcher:
+      title: '^feat(\([^)]+\))?!?:'
+
+  - label: 'type/bug'
+    sync: true
+    matcher:
+      title: '^fix(\([^)]+\))?!?:'
+
+  - label: 'type/docs'
+    sync: true
+    matcher:
+      title: '^docs(\([^)]+\))?:'
+
+  - label: 'Breaking 🔨'
+    sync: true
+    matcher:
+      title: '^(feat|fix)(\([^)]+\))?!:'
+
+  # -- distinct areas ----------------------------------------------------------
+  - label: '3.x'
+    sync: true
+    matcher:
+      baseBranch: '^v3$'
+
+  - label: 'area/docs'
+    sync: true
+    matcher:
+      files:
+        any: ['docs/*', 'docs/**/*', '**/*.rst', '**/*.md']
+
+  - label: 'area/unit-tests'
+    sync: true
+    matcher:
+      files:
+        any: ['test/unit/*', 'test/unit/**/*']
+
+  - label: 'area/end-to-end-tests'
+    sync: true
+    matcher:
+      files:
+        any: ['test/e2e/*', 'test/e2e/**/*']
+
+  - label: 'area/test-apps'
+    sync: true
+    matcher:
+      files:
+        any: ['test/test_apps/*', 'test/test_apps/**/*']
+
+  - label: 'area/docs'
+    sync: true
+    matcher:
+      files:
+        any: ['docs/*', 'docs/**/*', '**/*.rst', '**/*.md']
+
+  - label: 'area/unit-tests'
+    sync: true
+    matcher:
+      files:
+        any: ['test/unit/*', 'test/unit/**/*']
+
+  - label: 'area/end-to-end-tests'
+    sync: true
+    matcher:
+      files:
+        any: ['test/e2e/*', 'test/e2e/**/*']
+
+  - label: 'area/test-apps'
+    sync: true
+    matcher:
+      files:
+        any: ['test/test_apps/*', 'test/test_apps/**/*']
+
+  - label: 'area/ci'
+    sync: true
+    matcher:
+      files:
+        any: ['.github/**/*', 'codecov.yml', 'pre-commit-config.yaml', 'sonar-project.properties']
+
+  - label: 'area/dependencies'
+    sync: true
+    matcher:
+      files:
+        any: ['pyproject.toml', '*.lock']
+
+  - label: 'area/enums'
+    sync: true
+    matcher:
+      files: ['litestar/enums.py']
+
+  - label: 'area/background-tasks'
+    sync: true
+    matcher:
+      files: ['litestar/background_tasks.py']
+
+  - label: 'area/constants'
+    sync: true
+    matcher:
+      files: ['litestar/constants.py']
+
+  - label: 'area/concurrency'
+    sync: true
+    matcher:
+      files: ['litestar/concurrency.py']
+
+  - label: 'area/parsers'
+    sync: true
+    matcher:
+      files: ['litestar/_parsers.py']
+
+  - label: 'area/layers'
+    sync: true
+    matcher:
+      files: ['litestar/_layers/*']
+
+  - label: 'area/multipart'
+    sync: true
+    matcher:
+      files: ['litestar/_multipart.py']
+
+  - label: 'area/di'
+    sync: true
+    matcher:
+      files: ['litestar/di.py']
+
+  - label: 'area/file-system'
+    sync: true
+    matcher:
+      files: ['litestar/file_system.py']
+
+  - label: 'area/controller'
+    sync: true
+    matcher:
+      files: ['litestar/controller.py']
+
+  - label: 'area/serialization'
+    sync: true
+    matcher:
+      files: ['litestar/serialization/*']
+
+  - label: 'area/params'
+    sync: true
+    matcher:
+      files: ['litestar/params.py']
+
+  - label: 'area/template'
+    sync: true
+    matcher:
+      files: ['litestar/template/*']
+
+  - label: 'area/events'
+    sync: true
+    matcher:
+      files: ['litestar/events/*']
+
+  - label: 'area/router'
+    sync: true
+    matcher:
+      files: ['litestar/router.py']
+
+  - label: 'area/exceptions'
+    sync: true
+    matcher:
+      files: ['litestar/exceptions/*']
+
+  - label: 'area/static-files'
+    sync: true
+    matcher:
+      files: ['litestar/static_files/*']
+
+  - label: 'area/signature'
+    sync: true
+    matcher:
+      files: ['litestar/_signature/*']
+
+  - label: 'area/plugins'
+    sync: true
+    matcher:
+      files: ['litestar/plugins/*']
+
+  - label: 'area/stores'
+    sync: true
+    matcher:
+      files: ['litestar/stores/*']
+
+  - label: 'area/logging'
+    sync: true
+    matcher:
+      files: ['litestar/logging/*']
+
+  - label: 'area/connection'
+    sync: true
+    matcher:
+      files: ['litestar/connection/*']
+
+  - label: 'area/asgi'
+    sync: true
+    matcher:
+      files: ['litestar/_asgi/*']
+
+  - label: 'area/types'
+    sync: true
+    matcher:
+      files: ['litestar/types/*']
+
+  - label: 'area/kwargs'
+    sync: true
+    matcher:
+      files: ['litestar/_kwargs/*']
+
+  - label: 'area/datastructures'
+    sync: true
+    matcher:
+      files: ['litestar/datastructures/*']
+
+  - label: 'area/channels'
+    sync: true
+    matcher:
+      files: ['litestar/channels/*']
+
+  - label: 'area/response'
+    sync: true
+    matcher:
+      files: ['litestar/response/*']
+
+  - label: 'area/repository'
+    sync: true
+    matcher:
+      files: ['litestar/repository/*']
+
+  - label: 'area/security'
+    sync: true
+    matcher:
+      files: ['litestar/security/*']
+
+  - label: 'area/dto'
+    sync: true
+    matcher:
+      files: ['litestar/dto/*']
+
+  - label: 'area/testing'
+    sync: true
+    matcher:
+      files: ['litestar/testing/*']
+
+  - label: 'area/openapi'
+    sync: true
+    matcher:
+      files: ['litestar/_openapi/*']
+
+  - label: 'area/middleware'
+    sync: true
+    matcher:
+      files: ['litestar/middleware/*']
+
+  - label: 'area/handlers'
+    sync: true
+    matcher:
+      files: ['litestar/handlers/*']
+
+  - label: 'area/contrib'
+    sync: true
+    matcher:
+      files: ['litestar/contrib/*']
+
+  - label: 'area/private-api'
+    sync: true
+    matcher:
+      files:
+        any: ['litestar/_*.py', 'litestar/*/_*.py', 'litestar/_*/**/*.py']
+
+  # -- Size Based Labels -------------------------------------------------------
+  - label: 'size: small'
+    sync: true
+    matcher:
+      files:
+        count:
+          gte: 1
+          lte: 10
+
+  - label: 'size: medium'
+    sync: true
+    matcher:
+      files:
+        count:
+          gte: 10
+          lte: 25
+
+  - label: 'size: large'
+    sync: true
+    matcher:
+      files:
+        count:
+          gte: 26
+
+# -- Merge Checks --------------------------------------------------------------
+checks:
+  - context: 'No Merge check'
+    description: "Disable merging when 'do not merge' label is set"
+    labels:
+      none: ['do not merge']
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 766279dcb..c3fe9a797 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -152,35 +152,6 @@ jobs:
       - name: Test
         run: pdm run pytest tests -m server_integration
 
-  upload-test-coverage:
-    runs-on: ubuntu-latest
-    needs: test
-    steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v5
-        with:
-          python-version: "3.12"
-
-      - name: Download Artifacts
-        uses: actions/download-artifact@v3
-        with:
-          name: coverage-data
-
-      - name: Combine coverage files
-        run: |
-          python -Im pip install coverage covdefaults
-          python -Im coverage combine
-          python -Im coverage xml -i
-
-      - name: Fix coverage file name
-        run: sed -i "s/home\/runner\/work\/litestar\/litestar/github\/workspace/g" coverage.xml
-
-      - name: Upload coverage reports to Codecov
-        uses: codecov/codecov-action@v4
-        with:
-          files: coverage.xml
-          token: ${{ secrets.CODECOV_TOKEN }}
-
   test-platform-compat:
     if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'test platform compat')
     strategy:
@@ -275,3 +246,109 @@ jobs:
           path: |
             docs/_build/html
             .pr_number
+
+  test_minimal_app:
+    name: Test Minimal Application with Base Dependencies
+    runs-on: ubuntu-latest
+    env:
+      python_version: "3.12"
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - uses: pdm-project/setup-pdm@v4
+        name: Set up PDM
+        with:
+          python-version: "3.12"
+          allow-python-prereleases: false
+          cache: true
+          cache-dependency-path: |
+            ./pdm.lock
+
+      - name: Install dependencies
+        run: pdm install
+
+      - name: Set pythonpath
+        run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
+
+      - name: Test
+        run: mv tests/examples/test_hello_world.py test_hello_world.py && pdm run pytest test_hello_world.py
+
+  test_pydantic_1_app:
+    name: Test Minimal Pydantic 1 application
+    runs-on: ubuntu-latest
+    env:
+      python_version: "3.12"
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - uses: pdm-project/setup-pdm@v4
+        name: Set up PDM
+        with:
+          python-version: "3.12"
+          allow-python-prereleases: false
+          cache: true
+          cache-dependency-path: |
+            ./pdm.lock
+
+      - name: Install dependencies
+        run: |
+          pdm install
+          pdm run python -m ensurepip
+          pdm run python -m pip install "pydantic==1.*"
+
+      - name: Set pythonpath
+        run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
+
+      - name: Test
+        run: pdm run coverage run --branch -m unittest test_apps/pydantic_1_app.py
+
+      - name: Rename coverage file
+        run: mv .coverage* .coverage.pydantic_v1
+
+      - uses: actions/upload-artifact@v3
+        with:
+          name: coverage-data
+          path: .coverage.pydantic_v1
+
+  upload-test-coverage:
+    runs-on: ubuntu-latest
+    needs:
+      - test
+      - test_pydantic_1_app
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Download Artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: coverage-data
+
+      - name: Combine coverage files
+        run: |
+          python -Im pip install coverage covdefaults
+          python -Im coverage combine
+          python -Im coverage xml -i
+
+      - name: Fix coverage file name
+        run: sed -i "s/home\/runner\/work\/litestar\/litestar/github\/workspace/g" coverage.xml
+
+      - name: Upload coverage reports to Codecov
+        uses: codecov/codecov-action@v4
+        with:
+          files: coverage.xml
+          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
new file mode 100644
index 000000000..7a01dc512
--- /dev/null
+++ b/.github/workflows/pr-labeler.yml
@@ -0,0 +1,50 @@
+name: "Pull Request Labeler"
+
+on:
+  pull_request_target:
+  pull_request:
+
+jobs:
+  apply-labels:
+    permissions:
+      contents: read
+      pull-requests: write
+      checks: write
+      statuses: write
+    runs-on: ubuntu-latest
+    steps:
+    - uses: fuxingloh/multi-labeler@v4
+      with:
+        github-token: "${{ secrets.GITHUB_TOKEN }}"
+
+  distinguish-pr-origin:
+    needs: apply-labels
+    if: ${{ always() }}
+    permissions:
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v7
+        with:
+          github-token: ${{secrets.GITHUB_TOKEN}}
+          script: |
+            const maintainers = [
+              'JacobCoffee', 'provinzkraut', 'cofin',
+              'peterschutt', 'Alc-Alc', 'guacs',
+              'dependabot[bot]', 'all-contributors[bot]'
+            ]
+            if (maintainers.includes(context.payload.sender.login)) {
+              github.rest.issues.addLabels({
+                issue_number: context.issue.number,
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                labels: ['pr/internal']
+              })
+            } else {
+              github.rest.issues.addLabels({
+                issue_number: context.issue.number,
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                labels: ['pr/external', 'Triage Required :hospital:']
+              })
+            }
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index ddc3bb022..e713debae 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,71 +6,6 @@ on:
   workflow_dispatch:
 
 jobs:
-  test_minimal_app:
-    name: Test Minimal Application with Base Dependencies
-    runs-on: ubuntu-latest
-    env:
-      python_version: "3.12"
-    steps:
-      - name: Check out repository
-        uses: actions/checkout@v4
-
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.12"
-
-      - uses: pdm-project/setup-pdm@v4
-        name: Set up PDM
-        with:
-          python-version: "3.12"
-          allow-python-prereleases: false
-          cache: true
-          cache-dependency-path: |
-            ./pdm.lock
-
-      - name: Install dependencies
-        run: pdm install
-
-      - name: Set pythonpath
-        run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
-
-      - name: Test
-        run: mv tests/examples/test_hello_world.py test_hello_world.py && pdm run pytest test_hello_world.py
-
-  test_pydantic_1_app:
-    name: Test Minimal Pydantic 1 application
-    runs-on: ubuntu-latest
-    env:
-      python_version: "3.12"
-    steps:
-      - name: Check out repository
-        uses: actions/checkout@v4
-
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.12"
-
-      - uses: pdm-project/setup-pdm@v4
-        name: Set up PDM
-        with:
-          python-version: "3.12"
-          allow-python-prereleases: false
-          cache: true
-          cache-dependency-path: |
-            ./pdm.lock
-
-      - name: Install dependencies
-        run: |
-          pdm install
-          pip install "pydantic==1.*"
-
-      - name: Set pythonpath
-        run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
-
-      - name: Test
-        run: pdm run python -m unittest test_apps/pydantic_1_app.py
 
   publish-release:
     name: upload release to PyPI
diff --git a/.gitignore b/.gitignore
index 0c3c190c5..72e2ed9cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,10 @@
 .venv*
 .vscode/
 __pycache__/
+assets/
 build/
 dist/
+html/
 node_modules/
 results/
 site/
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 000000000..4bb104510
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,41 @@
+cff-version: 1.2.0
+title: Litestar
+message: 'If you use this software, please cite it as below.'
+type: software
+authors:
+  - Janek Nouvertné
+  - Peter Schutt
+  - Cody Fincher
+  - Visakh Unnikrishnan
+  - Jacob Coffee
+  - Na'aman Hirschfeld
+repository-code: 'https://github.com/litestar-org/litestar'
+url: 'https://docs.litestar.dev/latest/'
+abstract: >-
+  Litestar is a powerful, flexible, and highly performant Python web framework
+  for building modern APIs and applications. With an emphasis on developer
+  experience and performance, Litestar provides a rich set of features out of the
+  box, including automatic API documentation, data validation and serialization,
+  ORM integration, dependency injection, caching, websockets, and more.
+
+
+  Litestar's layered architecture and open ecosystem enable seamless integration
+  with popular libraries like Pydantic, SQLAlchemy, and msgspec. It offers both
+  asynchronous and synchronous execution models without performance penalties.
+
+
+  With Litestar, you can effortlessly build and deploy production-ready APIs and
+  web applications, leveraging features like interactive API documentation,
+  middlewares for common tasks, session and JWT-based authentication, and strict
+  runtime validation for enhanced safety. Experience the perfect blend of ease
+  of use, flexibility and performance with Litestar.
+keywords:
+  - python
+  - web
+  - framework
+  - typing
+  - dependency injection
+  - api
+license: MIT
+version: v2.8.0
+date-released: '2024-04-05'
diff --git a/Makefile b/Makefile
index b17df2354..b43b1ae0c 100644
--- a/Makefile
+++ b/Makefile
@@ -152,7 +152,7 @@ docs-clean: 										## Dump the existing built docs
 
 docs-serve: docs-clean 								## Serve the docs locally
 	@echo "=> Serving documentation"
-	$(PDM) run sphinx-autobuild docs docs/_build/ -j auto --watch polyfactory --watch docs --watch tests --watch CONTRIBUTING.rst --port 8002
+	$(PDM) run sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --watch CONTRIBUTING.rst --port 8002
 
 docs: docs-clean 									## Dump the existing built docs and rebuild them
 	@echo "=> Building documentation"
diff --git a/docs/examples/datastructures/secrets/__init__.py b/docs/examples/datastructures/secrets/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/docs/examples/datastructures/secrets/secret_body.py b/docs/examples/datastructures/secrets/secret_body.py
new file mode 100644
index 000000000..fd9bdb703
--- /dev/null
+++ b/docs/examples/datastructures/secrets/secret_body.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+
+from litestar import post
+from litestar.datastructures.secret_values import SecretString
+
+
+@dataclass
+class Sensitive:
+    value: SecretString
+
+
+@post(sync_to_thread=False)
+def post_handler(data: Sensitive) -> Sensitive:
+    return data
diff --git a/docs/examples/datastructures/secrets/secret_header.py b/docs/examples/datastructures/secrets/secret_header.py
new file mode 100644
index 000000000..dec3165d0
--- /dev/null
+++ b/docs/examples/datastructures/secrets/secret_header.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from secrets import compare_digest
+
+from typing_extensions import Annotated
+
+from litestar import get
+from litestar.datastructures.secret_values import SecretString
+from litestar.exceptions import NotAuthorizedException
+from litestar.params import Parameter
+
+SECRET_VALUE = "super-secret"  # An example secret value - this should be stored securely in production.
+
+
+@dataclass
+class Sensitive:
+    value: str
+
+
+@get(sync_to_thread=False)
+def get_handler(secret: Annotated[SecretString, Parameter(header="x-secret")]) -> Sensitive:
+    if not compare_digest(secret.get_secret(), SECRET_VALUE):
+        raise NotAuthorizedException
+    return Sensitive(value="sensitive data")
diff --git a/docs/reference/datastructures.rst b/docs/reference/datastructures/index.rst
similarity index 62%
rename from docs/reference/datastructures.rst
rename to docs/reference/datastructures/index.rst
index 1def654ef..fe65e6e86 100644
--- a/docs/reference/datastructures.rst
+++ b/docs/reference/datastructures/index.rst
@@ -3,3 +3,8 @@ datastructures
 
 .. automodule:: litestar.datastructures
     :members:
+
+.. toctree::
+    :maxdepth: 1
+
+    secret_values
diff --git a/docs/reference/datastructures/secret_values.rst b/docs/reference/datastructures/secret_values.rst
new file mode 100644
index 000000000..7db644817
--- /dev/null
+++ b/docs/reference/datastructures/secret_values.rst
@@ -0,0 +1,5 @@
+secret_values
+=============
+
+.. automodule:: litestar.datastructures.secret_values
+    :members:
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 1929bf6b5..4ba030cb5 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -16,7 +16,7 @@ API reference
     controller
     concurrency
     data_extractors
-    datastructures
+    datastructures/index
     di
     dto/index
     enums
diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst
index 75b0798be..7f90149e4 100644
--- a/docs/release-notes/changelog.rst
+++ b/docs/release-notes/changelog.rst
@@ -3,6 +3,50 @@
 2.x Changelog
 =============
 
+.. changelog:: 2.8.2
+    :date: 2024-04-09
+
+    .. change:: pydantic v2 import tests for pydantic v1.10.15
+        :type: bugfix
+        :pr: 3347
+        :issue: 3348
+
+        Fixes bug with Pydantic v1 environment test causing the test to run against v2. Adds assertion for version to
+        the test.
+
+        Fixes a bug exposed by above that relied on Pydantic not having `v1` in the package namespace if `v1` is
+        installed. This doesn't hold true after Pydantic's `1.10.15` release.
+
+        Moves application environment tests from the release job into the normal CI run.
+
+.. changelog:: 2.8.1
+    :date: 2024-04-08
+
+    .. change:: ASGI lifespan msg after lifespan context exception
+        :type: bugfix
+        :pr: 3315
+
+        An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message
+
+        This PR modifies `ASGIRouter.lifespan()` so that it sends a shutdown failure message if we've already confirmed
+        startup.
+
+    .. change:: Fix when pydantic==1.10 is installed
+        :type: bugfix
+        :pr: 3335
+        :issue: 3334
+
+        This PR fixes a bug introduced in #3296 where it failed to take into account that the `pydantic_v2` variable could be `Empty`.
+
+    .. change:: OpenAPI router and controller on same app.
+        :type: bugfix
+        :pr: 3338
+        :issue: 3337
+
+        Fixes an `ImproperlyConfiguredException` where an app that explicitly registers an `OpenAPIController` on the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the two different handlers being given the same name as defined in `litestar.constants`.
+
+        PR adds a distinct name for use by the handler that serves `openapi.json` on the controller.
+
 .. changelog:: 2.8.0
     :date: 2024-04-05
 
diff --git a/docs/usage/applications.rst b/docs/usage/applications.rst
index 69f5e92f0..c0ff1e367 100644
--- a/docs/usage/applications.rst
+++ b/docs/usage/applications.rst
@@ -4,33 +4,36 @@ Applications
 Application objects
 -------------------
 
-At the root of every Litestar application is an instance of the :class:`Litestar <litestar.app.Litestar>`
-class. Typically, this code will be placed in a file called ``main.py`` at the project's root directory.
+At the root of every Litestar application is an instance of the :class:`~litestar.app.Litestar`
+class. Typically, this code will be placed in a file called ``main.py``, ``app.py``, ``asgi.py`` or similar
+at the project's root directory.
 
-Creating an app is straightforward – the only required arg is a list
+These entry points are also used during :ref:`CLI autodiscovery <usage/cli:autodiscovery>`
+
+Creating an app is straightforward – the only required :term:`args <argument>` is a :class:`list`
 of :class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Router>`,
 or :class:`Route handlers <.handlers.BaseRouteHandler>`:
 
 .. literalinclude:: /examples/hello_world.py
-    :caption: Hello World
-    :language: python
-
-
-The app instance is the root level of the app - it has the base path of ``/`` and all root level Controllers, Routers,
-and Route Handlers should be registered on it.
+    :caption: A simple Hello World Litestar app
 
-.. seealso::
+The app instance is the root level of the app - it has the base path of ``/`` and all root level
+:class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Router>`,
+and :class:`Route handlers <.handlers.BaseRouteHandler>` should be registered on it.
 
-    To learn more about registering routes, check out this chapter in the documentation:
+.. seealso:: To learn more about registering routes, check out this chapter in the documentation:
 
     * :ref:`Routing - Registering Routes <usage/routing/overview:Registering Routes>`
 
 Startup and Shutdown
 --------------------
 
-You can pass a list of callables - either sync or async functions, methods, or class instances - to the ``on_startup``
-/ ``on_shutdown`` kwargs of the :class:`Litestar <litestar.app.Litestar>` instance. Those will be called in
-order, once the ASGI server (uvicorn, hypercorn, etc.) emits the respective event.
+You can pass a list of :term:`callables <python:callable>` - either sync or async functions, methods, or class instances
+- to the :paramref:`~litestar.app.Litestar.on_startup` / :paramref:`~litestar.app.Litestar.on_shutdown`
+:term:`kwargs <argument>` of the :class:`app <litestar.app.Litestar>` instance. Those will be called in
+order, once the ASGI server such as `uvicorn <https://www.uvicorn.org/>`_,
+`Hypercorn <https://hypercorn.readthedocs.io/en/latest/#/>`_, `Granian <https://github.com/emmett-framework/granian/>`_,
+`Daphne <https://github.com/django/daphne/>`_, etc. emits the respective event.
 
 .. mermaid::
 
@@ -41,39 +44,37 @@ order, once the ASGI server (uvicorn, hypercorn, etc.) emits the respective even
 A classic use case for this is database connectivity. Often, we want to establish a database connection on application
 startup, and then close it gracefully upon shutdown.
 
-For example, lets create a database connection using the async engine from
+For example, let us create a database connection using the async engine from
 `SQLAlchemy <https://docs.sqlalchemy.org/en/latest/orm/extensions/asyncio.html>`_. We create two functions, one to get or
-establish the connection, and another to close it, and then pass them to the Litestar constructor:
+establish the connection, and another to close it, and then pass them to the :class:`~litestar.app.Litestar` constructor:
 
 .. literalinclude:: /examples/startup_and_shutdown.py
     :caption: Startup and Shutdown
-    :language: python
 
 .. _lifespan-context-managers:
 
 Lifespan context managers
 -------------------------
 
-In addition to the lifespan hooks, Litestar also supports managing the lifespan of an
-application using asynchronous context managers. This can be useful when dealing with
-long running tasks, or those that need to keep a certain context object, such as a
-connection, around.
-
+In addition to the lifespan hooks, Litestar also supports managing the lifespan of an application using an
+:term:`asynchronous context manager`. This can be useful when dealing with long running tasks, or those that need to
+keep a certain context object, such as a connection, around.
 
 .. literalinclude:: /examples/application_hooks/lifespan_manager.py
-    :language: python
     :caption: Handling a database connection
 
 Order of execution
------------------------------------------------------
+------------------
 
-When multiple lifespan context managers and ``on_shutdown`` hooks are specified, Litestar will invoke the context
-managers in inverse order before the shutdown hooks are invoked.
+When multiple lifespan context managers and :paramref:`~litestar.app.Litestar.on_shutdown`  hooks are specified,
+Litestar will invoke the :term:`context managers <asynchronous context manager>` in inverse order before the
+shutdown hooks are invoked.
 
 Consider the case where there are two lifespan context managers ``ctx_a`` and ``ctx_b`` as well as two shutdown hooks
 ``hook_a`` and ``hook_b`` as shown in the following code:
 
 .. code-block:: python
+    :caption: Example of multiple :term:`context managers <asynchronous context manager>` and shutdown hooks
 
     app = Litestar(lifespan=[ctx_a, ctx_b], on_shutdown=[hook_a, hook_b])
 
@@ -84,7 +85,7 @@ During shutdown, they are executed in the following order:
     flowchart LR
         ctx_b --> ctx_a --> hook_a --> hook_b
 
-As seen, the context managers are invoked in inverse order.
+As seen, the :term:`context managers <asynchronous context manager>` are invoked in inverse order.
 On the other hand, the shutdown hooks are invoked in their specified order.
 
 .. _application-state:
@@ -92,166 +93,152 @@ On the other hand, the shutdown hooks are invoked in their specified order.
 Using Application State
 -----------------------
 
-As seen in the examples for the `on_startup / on_shutdown <Startup and Shutdown>`,
-callables passed to these hooks can receive an optional kwarg called ``app``, through which the application's state object
-and other properties can be accessed.
-The advantage of using application ``state``, is that it can be accessed during multiple stages of the connection, and
-it can be injected into dependencies and route handlers.
+As seen in the examples for the `on_startup / on_shutdown <#startup-and-shutdown>`_, :term:`callables <python:callable>`
+passed to these hooks can receive an optional :term:`kwarg <argument>` called ``app``, through which the application's
+state object and other properties can be accessed. The advantage of using application :paramref:`~.app.Litestar.state`,
+is that it can be accessed during multiple stages of the connection, and it can be injected into dependencies and
+route handlers.
+
+The Application State is an instance of the :class:`.datastructures.state.State` datastructure, and it is
+accessible via the :paramref:`~.app.Litestar.state` attribute. As such it can be accessed wherever the app instance
+is accessible.
 
-The Application State is an instance of the :class:`State <.datastructures.state.State>` datastructure, and it is accessible
-via the :class:`app.state <.app.Litestar>` attribute. As such it can be accessed wherever the app instance is accessible.
+:paramref:`~.app.Litestar.state` is one of the
+:ref:`reserved keyword arguments <usage/routing/handlers:"reserved" keyword arguments>`.
 
-It's important to understand in this context that the application instance is injected into the ASGI ``scope`` mapping for
-each connection (i.e. request or websocket connection) as ``scope["app"].state``. This makes the application accessible
-wherever the scope mapping is available, e.g. in middleware, on :class:`Request <.connection.request.Request>` and
-:class:`Websocket <.connection.websocket.WebSocket>` instances (accessible as ``request.app`` / ``socket.app``), and many
+It is important to understand in this context that the application instance is injected into the ASGI ``scope`` mapping
+for each connection (i.e. request or websocket connection) as ``scope["app"]``. This makes the application
+accessible wherever the scope mapping is available, e.g. in middleware, on :class:`~.connection.request.Request` and
+:class:`~.connection.websocket.WebSocket` instances (accessible as ``request.app`` / ``socket.app``), and many
 other places.
 
-Therefore, state offers an easy way to share contextual data between disparate parts of the application, as seen below:
+Therefore, :paramref:`~.app.Litestar.state` offers an easy way to share contextual data between disparate parts
+of the application, as seen below:
 
 .. literalinclude:: /examples/application_state/using_application_state.py
     :caption: Using Application State
-    :language: python
 
 .. _Initializing Application State:
 
 Initializing Application State
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-To seed application state, you can pass a :class:`State <.datastructures.state.State>` object to the ``state`` kwarg of
-the Litestar constructor:
+To seed application state, you can pass a :class:`~.datastructures.state.State` object to the
+:paramref:`~.app.Litestar.state` parameter of the Litestar constructor:
 
 .. literalinclude:: /examples/application_state/passing_initial_state.py
     :caption: Using Application State
-    :language: python
 
+.. note:: :class:`~.datastructures.state.State` can be initialized with a :class:`dictionary <dict>`, an instance of
+    :class:`~.datastructures.state.ImmutableState` or :class:`~.datastructures.state.State`,
+    or a :class:`list` of :class:`tuples <tuple>` containing key/value pairs.
 
-.. note::
+You may instruct :class:`~.datastructures.state.State` to deep copy initialized data to prevent mutation from outside
+the application context.
 
-    :class:`State <.datastructures.state.State>` can be initialized with a dictionary, an instance of
-    :class:`ImmutableState <.datastructures.state.ImmutableState>` or :class:`State <.datastructures.state.State>`,
-    or a list of tuples containing key/value pairs.
-
-.. attention::
-
-    You may instruct :class:`State <.datastructures.state.State>` to deep copy initialized data to prevent mutation from
-    outside the application context. To do this, pass ``deep_copy=True`` to the :class:`State <.datastructures.state.State>`
-    constructor.
+To do this, set :paramref:`~.datastructures.state.State.deep_copy` to ``True`` in the
+:class:`~.datastructures.state.State` constructor.
 
 Injecting Application State into Route Handlers and Dependencies
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 As seen in the above example, Litestar offers an easy way to inject state into route handlers and dependencies - simply
-by specifying ``state`` as a kwarg to the handler function. I.e., you can simply do this in handler function or dependency
-to access the application state:
+by specifying ``state`` as a kwarg to the handler or dependency function. For example:
 
 .. code-block:: python
+    :caption: Accessing application :class:`~.datastructures.state.State` in a handler function
 
-   from litestar import get
-   from litestar.datastructures import State
+    from litestar import get
+    from litestar.datastructures import State
 
 
-   @get("/")
-   def handler(state: State) -> None: ...
+    @get("/")
+    def handler(state: State) -> None: ...
 
 When using this pattern you can specify the class to use for the state object. This type is not merely for type
-checkers, rather Litestar will instantiate a new state instance based on the type you set there. This allows users to
-use custom classes for State, e.g.:
+checkers, rather Litestar will instantiate a new ``state`` instance based on the type you set there.
+This allows users to use custom classes for :class:`~.datastructures.state.State`.
 
-While this is very powerful, it might encourage users to follow anti-patterns: it's important to emphasize that using
-state can lead to code that's hard to reason about and bugs that are difficult to understand, due to changes in
+While this is very powerful, it might encourage users to follow anti-patterns: it is important to emphasize that using
+state can lead to code that is hard to reason about and bugs that are difficult to understand, due to changes in
 different ASGI contexts. As such, this pattern should be used only when it is the best choice and in a limited fashion.
-To discourage its use, Litestar also offers a builtin ``ImmutableState`` class. You can use this class to type state and
-ensure that no mutation of state is allowed:
+To discourage its use, Litestar also offers a builtin :class:`~.datastructures.state.ImmutableState` class.
+You can use this class to type state and ensure that no mutation of state is allowed:
 
 .. literalinclude:: /examples/application_state/using_immutable_state.py
-    :caption: Using Custom State
-    :language: python
-
+    :caption: Using Custom State to ensure immutability
 
 Application Hooks
 -----------------
 
-Litestar includes several application level hooks that allow users to run their own sync or async callables. While you
-are free to use these hooks as you see fit, the design intention behind them is to allow for easy instrumentation for
-observability (monitoring, tracing, logging, etc.).
-
-.. note::
-
-    All application hook kwargs detailed below receive either a single callable or a list of callables.
-    If a list is provided, it is called in the order it is given.
+Litestar includes several application level hooks that allow users to run their own sync or async
+:term:`callables <python:callable>`. While you are free to use these hooks as you see fit, the design intention
+behind them is to allow for easy instrumentation for observability (monitoring, tracing, logging, etc.).
 
+.. note:: All application hook kwargs detailed below receive either a single :term:`python:callable` or a :class:`list`
+    of :term:`callables <python:callable>`.
+    If a :class:`list` is provided, it is called in the order it is given.
 
 After Exception
 ^^^^^^^^^^^^^^^
 
-The ``after_exception`` hook takes a :class:`sync or async callable <litestar.types.AfterExceptionHookHandler>` that is called with
-two arguments: the ``exception`` that occurred and the ASGI ``scope`` of the request or websocket connection.
+The :paramref:`~litestar.app.Litestar.after_exception` hook takes a
+:class:`sync or async callable <litestar.types.AfterExceptionHookHandler>` that is called with two arguments:
+the ``exception`` that occurred and the ASGI ``scope`` of the request or websocket connection.
 
 .. literalinclude:: /examples/application_hooks/after_exception_hook.py
     :caption: After Exception Hook
-    :language: python
-
-
-.. attention::
 
-    This hook is not meant to handle exceptions - it just receives them to allow for side effects.
+.. attention:: This hook is not meant to handle exceptions - it just receives them to allow for side effects.
     To handle exceptions you should define :ref:`exception handlers <usage/exceptions:exception handling>`.
 
 Before Send
 ^^^^^^^^^^^
 
-The ``before_send`` hook takes a :class:`sync or async callable <litestar.types.BeforeMessageSendHookHandler>` that is called when
-an ASGI message is sent. The hook receives the message instance and the ASGI ``scope``.
+The :paramref:`~litestar.app.Litestar.before_send` hook takes a
+:class:`sync or async callable <litestar.types.BeforeMessageSendHookHandler>` that is called when an ASGI message is
+sent. The hook receives the message instance and the ASGI ``scope``.
 
 .. literalinclude:: /examples/application_hooks/before_send_hook.py
     :caption: Before Send Hook
-    :language: python
-
 
 Initialization
 ^^^^^^^^^^^^^^
 
-Litestar includes a hook for intercepting the arguments passed to the :class:`Litestar constructor <litestar.app.Litestar>`,
-before they are used to instantiate the application.
+Litestar includes a hook for intercepting the arguments passed to the
+:class:`Litestar constructor <litestar.app.Litestar>`, before they are used to instantiate the application.
 
-Handlers can be passed to the ``on_app_init`` parameter on construction of the application, and in turn, each will receive
-an instance of :class:`AppConfig <litestar.config.app.AppConfig>` and must return an instance of same.
+Handlers can be passed to the :paramref:`~.app.Litestar.on_app_init` parameter on construction of the application,
+and in turn, each will receive an instance of :class:`~.config.app.AppConfig` and must return an instance of same.
 
 This hook is useful for applying common configuration between applications, and for use by developers who may wish to
 develop third-party application configuration systems.
 
-.. note::
-
-    ``on_app_init`` handlers cannot be ``async def`` functions, as they are called within ``Litestar.__init__()``,
-    outside of an async context.
+.. note:: :paramref:`~.app.Litestar.on_app_init` handlers cannot be :ref:`python:async def` functions, as they are
+    called within :paramref:`~litestar.app.Litestar.__init__`, outside of an async context.
 
 .. literalinclude:: /examples/application_hooks/on_app_init.py
-    :caption: After Exception Hook
-    :language: python
-
+    :caption: Example usage of the ``on_app_init`` hook to modify the application configuration.
 
 .. _layered-architecture:
 
 Layered architecture
 --------------------
 
-Litestar has a layered architecture compromising of (generally speaking) 4 layers:
+Litestar has a layered architecture compromising of 4 layers:
 
+#. :class:`The application object <litestar.app.Litestar>`
+#. :class:`Routers <.router.Router>`
+#. :class:`Controllers <.controller.Controller>`
+#. :class:`Handlers <.handlers.BaseRouteHandler>`
 
-#. The application object
-#. Routers
-#. Controllers
-#. Handlers
-
-There are many parameters that can be defined on every layer, in which case the parameter
+There are many :term:`parameters <parameter>` that can be defined on every layer, in which case the :term:`parameter`
 defined on the layer **closest to the handler** takes precedence. This allows for maximum
 flexibility and simplicity when configuring complex applications and enables transparent
 overriding of parameters.
 
 Parameters that support layering are:
 
-
 * :ref:`after_request <after_request>`
 * :ref:`after_response <after_response>`
 * :ref:`before_request <before_request>`
diff --git a/docs/usage/security/index.rst b/docs/usage/security/index.rst
index e2daa1aa0..f3a6f7a09 100644
--- a/docs/usage/security/index.rst
+++ b/docs/usage/security/index.rst
@@ -14,3 +14,4 @@ authentication and authorization.
     guards
     excluding-and-including-endpoints
     jwt
+    secret-datastructures
diff --git a/docs/usage/security/secret-datastructures.rst b/docs/usage/security/secret-datastructures.rst
new file mode 100644
index 000000000..4b0009556
--- /dev/null
+++ b/docs/usage/security/secret-datastructures.rst
@@ -0,0 +1,50 @@
+Handling Secrets
+================
+
+Overview
+--------
+
+Two data structures are available to assist in handling secrets in web services:
+:class:`SecretString <datastructures.SecretString>` and :class:`SecretBytes <datastructures.SecretBytes>`. These are
+containers for holding sensitive data within your application.
+
+Secret Parameters
+-----------------
+
+The following example demonstrates how to use :class:`~datastructures.SecretString` to accept a secret value as a parameter in a GET request:
+
+.. literalinclude:: /examples/datastructures/secrets/secret_header.py
+    :caption: Example of using SecretString for a Header Parameter
+
+.. note::
+
+    When storing and comparing secrets, use secure practices to prevent unauthorized access. For example, use
+    environment variables, secret management services, or encrypted databases to store secrets securely. When comparing
+    secrets, use :func:`secrets.compare_digest` or similar to mitigate the risk of timing attacks.
+
+.. note::
+
+    The :func:`headers <connection.ASGIConnection.headers>` attribute of the :class:`~connection.ASGIConnection`
+    object stores the headers exactly as they are parsed from the ASGI message. Care should be taken to ensure that
+    these headers are not logged or otherwise exposed in a way that could compromise the security of the application.
+
+Secret Body
+-----------
+
+This example demonstrates use of a data structure with a :class:`~datastructures.SecretString` field to accept a secret
+within the HTTP body of a request:
+
+.. literalinclude:: /examples/datastructures/secrets/secret_body.py
+    :caption: Example of using SecretString for a Request Body
+
+Security Considerations
+-----------------------
+
+While :class:`SecretString` and :class:`SecretBytes` can help in securely transferring secret data through the framework,
+it's vital to adopt secure practices for storing and comparing secrets within your application. Here are a few
+guidelines:
+
+- Store secrets securely, using environment variables, secret management services, or encrypted databases.
+- Always use constant time comparison functions such as :func:`secrets.compare_digest` for comparing secret values to
+  mitigate the risk of timing attacks.
+- Implement access controls and logging to monitor and restrict who can access sensitive information.
diff --git a/docs/usage/static-files.rst b/docs/usage/static-files.rst
index 8216a3781..e1d474cdc 100644
--- a/docs/usage/static-files.rst
+++ b/docs/usage/static-files.rst
@@ -1,44 +1,47 @@
 Static files
 ============
 
-To serve static files (i.e. serve arbitrary files from a given directory), the
+To serve static files (i.e., serve arbitrary files from a given directory), the
 :func:`~litestar.static_files.create_static_files_router` can be used to create a
 :class:`Router <litestar.router.Router>` to handle this task.
 
 .. literalinclude:: /examples/static_files/full_example.py
-    :language: python
+    :caption: Serving static files using :func:`create_static_files_router <litestar.static_files.create_static_files_router>`
 
 In this example, files from the directory ``assets`` will be served on the path
 ``/static``. A file ``assets/hello.txt`` would now be available on ``/static/hello.txt``
 
-.. attention::
-    Directories are interpreted as relative to the working directory from which the
+.. attention:: Directories are interpreted as relative to the working directory from which the
     application is started
 
-
 Sending files as attachments
 ----------------------------
 
 By default, files are sent "inline", meaning they will have a
-``Content-Disposition: inline`` header. Setting ``send_as_attachment=True`` flag will
-send them with a ``Content-Disposition: attachment`` instead:
+``Content-Disposition: inline`` header.
 
-.. literalinclude:: /examples/static_files/send_as_attachment.py
-    :language: python
+Setting :paramref:`~litestar.static_files.create_static_files_router.params.send_as_attachment` to ``True`` will send
+them with a ``Content-Disposition: attachment`` instead:
 
+.. literalinclude:: /examples/static_files/send_as_attachment.py
+    :caption: Sending files as attachments using the the
+      :paramref:`~litestar.static_files.create_static_files_router.params.send_as_attachment` parameter
+      of :func:`create_static_files_router`
 
 HTML mode
 ---------
 
-"HTML mode" can be enabled by setting ``html_mode=True``. This will:
+"HTML mode" can be enabled by setting :paramref:`~litestar.static_files.create_static_files_router.params.html_mode`
+to ``True``.
+
+This will:
 
 - Serve and ``/index.html`` when the path ``/`` is requested
 - Attempt to serve ``/404.html`` when a requested file is not found
 
-
 .. literalinclude:: /examples/static_files/html_mode.py
-    :language: python
-
+    :caption: Serving HTML files using the :paramref:`~litestar.static_files.create_static_files_router.params.html_mode`
+      parameter of :func:`create_static_files_router`
 
 Passing options to the generated router
 ---------------------------------------
@@ -47,18 +50,18 @@ Options available on :class:`~litestar.router.Router` can be passed to directly
 :func:`~litestar.static_files.create_static_files_router`:
 
 .. literalinclude:: /examples/static_files/passing_options.py
-    :language: python
-
+    :caption: Passing options to the router generated by
+      :func:`create_static_files_router`
 
 Using a custom router class
 ---------------------------
 
-The router class used can be customized with the ``router_class`` parameter:
+The router class used can be customized with the
+:paramref:`~.static_files.create_static_files_router.params.router_class` parameter:
 
 .. literalinclude:: /examples/static_files/custom_router.py
-    :language: python
-
-
+    :caption: Using a custom router class with
+      :func:`create_static_files_router`
 
 Retrieving paths to static files
 --------------------------------
@@ -68,14 +71,11 @@ Retrieving paths to static files
 under which a specific file will be available:
 
 .. literalinclude:: /examples/static_files/route_reverse.py
-    :language: python
-
-.. tip::
+    :caption: Retrieving paths to static files using :meth:`~.app.Litestar.route_reverse`
 
-    The ``name`` parameter has to match the ``name`` parameter passed to
+.. tip:: The ``name`` parameter has to match the ``name`` parameter passed to
     :func:`create_static_files_router`, which defaults to ``static``.
 
-
 (Remote) file systems
 ---------------------
 
@@ -91,26 +91,23 @@ with support for popular cloud providers available via 3rd party implementations
 - Google Cloud Storage via `GCFS <https://gcsfs.readthedocs.io/en/latest/>`_
 - Azure Blob Storage via `adlfs <https://github.com/fsspec/adlfs>`_
 
-
 .. literalinclude:: /examples/static_files/file_system.py
-    :language: python
-
+    :caption: Using a custom file system with
+      :func:`create_static_files_router`
 
 Upgrading from legacy StaticFilesConfig
 ---------------------------------------
 
-.. important:: Info
+.. deprecated:: v3.0
     :class:`StaticFilesConfig` is deprecated and will be removed in Litestar 3.0
 
-
 Existing code can be upgraded to :func:`create_static_files_router` by replacing
 :class:`StaticFilesConfig` instances with this function call and passing the result to
 ``route_handlers`` instead of ``static_files_config``:
 
-
 .. literalinclude:: /examples/static_files/upgrade_from_static_1.py
-    :language: python
-
+    :caption: Using the deprecated :class:`~.static_files.config.StaticFilesConfig`
 
 .. literalinclude:: /examples/static_files/upgrade_from_static_2.py
-    :language: python
+    :caption: Upgrading from :class:`~.static_files.config.StaticFilesConfig` to
+      :func:`create_static_files_router`
diff --git a/pyproject.toml b/pyproject.toml
index 5e872d6d3..e709e5525 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,7 +60,7 @@ maintainers = [
 name = "litestar"
 readme = "README.md"
 requires-python = ">=3.8,<4.0"
-version = "2.8.0"
+version = "2.8.2"
 
 [project.urls]
 Blog = "https://blog.litestar.dev"
diff --git a/tools/sphinx_ext/missing_references.py b/tools/sphinx_ext/missing_references.py
index 496e38603..38f3a396c 100644
--- a/tools/sphinx_ext/missing_references.py
+++ b/tools/sphinx_ext/missing_references.py
@@ -37,7 +37,11 @@ def get_module_global_imports(module_import_path: str, reference_target_source_o
     """
     module = importlib.import_module(module_import_path)
     obj = getattr(module, reference_target_source_obj)
-    tree = _get_module_ast(inspect.getsourcefile(obj))
+
+    try:
+        tree = _get_module_ast(inspect.getsourcefile(obj))
+    except TypeError:
+        return set()
 
     import_nodes = _get_import_nodes(tree.body)
     return {path.asname or path.name for import_node in import_nodes for path in import_node.names}


Test patch:
diff --git a/litestar/_asgi/routing_trie/mapping.py b/litestar/_asgi/routing_trie/mapping.py
index 7a56b97a2..d788d2256 100644
--- a/litestar/_asgi/routing_trie/mapping.py
+++ b/litestar/_asgi/routing_trie/mapping.py
@@ -150,21 +150,21 @@ def configure_node(
                 asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=handler),
                 handler=handler,
             )
-            node.path_parameters[method] = route.path_parameters
+            node.path_parameters[method] = tuple(route.path_parameters.values())
 
     elif isinstance(route, WebSocketRoute):
         node.asgi_handlers["websocket"] = ASGIHandlerTuple(
             asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=route.route_handler),
             handler=route.route_handler,
         )
-        node.path_parameters["websocket"] = route.path_parameters
+        node.path_parameters["websocket"] = tuple(route.path_parameters.values())
 
     else:
         node.asgi_handlers["asgi"] = ASGIHandlerTuple(
             asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=route.route_handler),
             handler=route.route_handler,
         )
-        node.path_parameters["asgi"] = route.path_parameters
+        node.path_parameters["asgi"] = tuple(route.path_parameters.values())
         node.is_asgi = True
 
 
diff --git a/litestar/_openapi/parameters.py b/litestar/_openapi/parameters.py
index 5dbe43f5a..b0da70c52 100644
--- a/litestar/_openapi/parameters.py
+++ b/litestar/_openapi/parameters.py
@@ -81,7 +81,7 @@ class ParameterFactory:
         self,
         context: OpenAPIContext,
         route_handler: BaseRouteHandler,
-        path_parameters: tuple[PathParameterDefinition, ...],
+        path_parameters: dict[str, PathParameterDefinition],
     ) -> None:
         """Initialize ParameterFactory.
 
@@ -96,7 +96,7 @@ class ParameterFactory:
         self.parameters = ParameterCollection(route_handler)
         self.dependency_providers = route_handler.resolve_dependencies()
         self.layered_parameters = route_handler.resolve_layered_parameters()
-        self.path_parameters: dict[str, PathParameterDefinition] = {p.name: p for p in path_parameters}
+        self.path_parameters = path_parameters
 
     def create_parameter(self, field_definition: FieldDefinition, parameter_name: str) -> Parameter:
         """Create an OpenAPI Parameter instance for a field definition.
@@ -233,7 +233,7 @@ class ParameterFactory:
 def create_parameters_for_handler(
     context: OpenAPIContext,
     route_handler: BaseRouteHandler,
-    path_parameters: tuple[PathParameterDefinition, ...],
+    path_parameters: dict[str, PathParameterDefinition],
 ) -> list[Parameter]:
     """Create a list of path/query/header Parameter models for the given PathHandler."""
     factory = ParameterFactory(
diff --git a/litestar/_openapi/schema_generation/schema.py b/litestar/_openapi/schema_generation/schema.py
index 6229bf74a..49cfa9acf 100644
--- a/litestar/_openapi/schema_generation/schema.py
+++ b/litestar/_openapi/schema_generation/schema.py
@@ -46,7 +46,7 @@ from litestar._openapi.schema_generation.utils import (
     _type_or_first_not_none_inner_type,
     get_json_schema_formatted_examples,
 )
-from litestar.datastructures import UploadFile
+from litestar.datastructures import SecretBytes, SecretString, UploadFile
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
 from litestar.openapi.spec.schema import Schema, SchemaDataContainer
@@ -114,6 +114,8 @@ TYPE_MAP: dict[type[Any] | None | Any, Schema] = {
     OrderedDict: Schema(type=OpenAPIType.OBJECT),
     Path: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI),
     Pattern: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.REGEX),
+    SecretBytes: Schema(type=OpenAPIType.STRING),
+    SecretString: Schema(type=OpenAPIType.STRING),
     Sequence: Schema(type=OpenAPIType.ARRAY),
     Set: Schema(type=OpenAPIType.ARRAY),
     Tuple: Schema(type=OpenAPIType.ARRAY),
diff --git a/litestar/app.py b/litestar/app.py
index 6acc4cb9f..7a6619f12 100644
--- a/litestar/app.py
+++ b/litestar/app.py
@@ -670,7 +670,8 @@ class Litestar(Router):
                 route.create_handler_map()
 
             elif isinstance(route, WebSocketRoute):
-                route.handler_parameter_model = route.create_handler_kwargs_model(route.route_handler)
+                handler = route.route_handler
+                route.handler_parameter_model = handler.create_kwargs_model(path_parameters=route.path_parameters)
 
             for plugin in self.plugins.receive_route:
                 plugin.receive_route(route)
@@ -759,11 +760,7 @@ class Litestar(Router):
         passed_parameters = set(path_parameters.keys())
 
         selected_route = next(
-            (
-                route
-                for route in routes
-                if passed_parameters.issuperset({param.name for param in route.path_parameters})
-            ),
+            (route for route in routes if passed_parameters.issuperset(route.path_parameters)),
             routes[-1],
         )
         output: list[str] = []
diff --git a/litestar/contrib/pydantic/pydantic_dto_factory.py b/litestar/contrib/pydantic/pydantic_dto_factory.py
index 49704b9ad..6942b0b5a 100644
--- a/litestar/contrib/pydantic/pydantic_dto_factory.py
+++ b/litestar/contrib/pydantic/pydantic_dto_factory.py
@@ -6,7 +6,7 @@ from warnings import warn
 
 from typing_extensions import Annotated, TypeAlias, override
 
-from litestar.contrib.pydantic.utils import is_pydantic_undefined
+from litestar.contrib.pydantic.utils import is_pydantic_undefined, is_pydantic_v2
 from litestar.dto.base_dto import AbstractDTO
 from litestar.dto.data_structures import DTOFieldDefinition
 from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field
@@ -25,6 +25,10 @@ except ImportError as e:
 
 try:
     import pydantic as pydantic_v2
+
+    if not is_pydantic_v2(pydantic_v2):
+        raise ImportError
+
     from pydantic import ValidationError as ValidationErrorV2
     from pydantic import v1 as pydantic_v1
     from pydantic.v1 import ValidationError as ValidationErrorV1
@@ -46,18 +50,24 @@ T = TypeVar("T", bound="ModelType | Collection[ModelType]")
 
 __all__ = ("PydanticDTO",)
 
-_down_types = {
-    pydantic_v2.JsonValue: Any,
+_down_types: dict[Any, Any] = {
     pydantic_v1.EmailStr: str,
-    pydantic_v2.EmailStr: str,
     pydantic_v1.IPvAnyAddress: str,
-    pydantic_v2.IPvAnyAddress: str,
     pydantic_v1.IPvAnyInterface: str,
-    pydantic_v2.IPvAnyInterface: str,
     pydantic_v1.IPvAnyNetwork: str,
-    pydantic_v2.IPvAnyNetwork: str,
 }
 
+if pydantic_v2 is not Empty:  # type: ignore[comparison-overlap]  # pragma: no cover
+    _down_types.update(
+        {
+            pydantic_v2.JsonValue: Any,
+            pydantic_v2.EmailStr: str,
+            pydantic_v2.IPvAnyAddress: str,
+            pydantic_v2.IPvAnyInterface: str,
+            pydantic_v2.IPvAnyNetwork: str,
+        }
+    )
+
 
 def convert_validation_error(validation_error: ValidationErrorV1 | ValidationErrorV2) -> list[dict[str, Any]]:
     error_list = validation_error.errors()
diff --git a/litestar/contrib/pydantic/pydantic_init_plugin.py b/litestar/contrib/pydantic/pydantic_init_plugin.py
index 1261cd8eb..95bf53f17 100644
--- a/litestar/contrib/pydantic/pydantic_init_plugin.py
+++ b/litestar/contrib/pydantic/pydantic_init_plugin.py
@@ -8,24 +8,28 @@ from msgspec import ValidationError
 from typing_extensions import Buffer, TypeGuard
 
 from litestar._signature.types import ExtendedMsgSpecValidationError
-from litestar.contrib.pydantic.utils import is_pydantic_constrained_field
+from litestar.contrib.pydantic.utils import is_pydantic_constrained_field, is_pydantic_v2
 from litestar.exceptions import MissingDependencyException
 from litestar.plugins import InitPluginProtocol
 from litestar.typing import _KWARG_META_EXTRACTORS
 from litestar.utils import is_class_and_subclass
 
 try:
-    # check if we have pydantic v2 installed, and try to import both versions
+    import pydantic as _  # noqa: F401
+except ImportError as e:
+    raise MissingDependencyException("pydantic") from e
+
+try:
     import pydantic as pydantic_v2
+
+    if not is_pydantic_v2(pydantic_v2):
+        raise ImportError
+
     from pydantic import v1 as pydantic_v1
 except ImportError:
-    # check if pydantic 1 is installed and import it
-    try:
-        import pydantic as pydantic_v1  # type: ignore[no-redef]
+    import pydantic as pydantic_v1  # type: ignore[no-redef]
 
-        pydantic_v2 = None  # type: ignore[assignment]
-    except ImportError as e:
-        raise MissingDependencyException("pydantic") from e
+    pydantic_v2 = None  # type: ignore[assignment]
 
 
 if TYPE_CHECKING:
diff --git a/litestar/contrib/pydantic/pydantic_schema_plugin.py b/litestar/contrib/pydantic/pydantic_schema_plugin.py
index 2c189e441..5d7045e4f 100644
--- a/litestar/contrib/pydantic/pydantic_schema_plugin.py
+++ b/litestar/contrib/pydantic/pydantic_schema_plugin.py
@@ -10,6 +10,7 @@ from litestar.contrib.pydantic.utils import (
     is_pydantic_constrained_field,
     is_pydantic_model_class,
     is_pydantic_undefined,
+    is_pydantic_v2,
     pydantic_get_type_hints_with_generics_resolved,
     pydantic_unwrap_and_get_origin,
 )
@@ -21,17 +22,21 @@ from litestar.typing import FieldDefinition
 from litestar.utils import is_class_and_subclass, is_generic
 
 try:
-    # check if we have pydantic v2 installed, and try to import both versions
+    import pydantic as _  # noqa: F401
+except ImportError as e:
+    raise MissingDependencyException("pydantic") from e
+
+try:
     import pydantic as pydantic_v2
+
+    if not is_pydantic_v2(pydantic_v2):
+        raise ImportError
+
     from pydantic import v1 as pydantic_v1
 except ImportError:
-    # check if pydantic 1 is installed and import it
-    try:
-        import pydantic as pydantic_v1  # type: ignore[no-redef]
+    import pydantic as pydantic_v1  # type: ignore[no-redef]
 
-        pydantic_v2 = None  # type: ignore[assignment]
-    except ImportError as e:
-        raise MissingDependencyException("pydantic") from e
+    pydantic_v2 = None  # type: ignore[assignment]
 
 if TYPE_CHECKING:
     from litestar._openapi.schema_generation.schema import SchemaCreator
diff --git a/litestar/contrib/pydantic/utils.py b/litestar/contrib/pydantic/utils.py
index 6aee3225d..45362c150 100644
--- a/litestar/contrib/pydantic/utils.py
+++ b/litestar/contrib/pydantic/utils.py
@@ -41,6 +41,8 @@ except ImportError:
 
 
 if TYPE_CHECKING:
+    from types import ModuleType
+
     from typing_extensions import TypeGuard
 
 
@@ -212,3 +214,17 @@ def create_field_definitions_for_computed_fields(
         )
         for k, dec in pydantic_decorators.computed_fields.items()
     }
+
+
+def is_pydantic_v2(module: ModuleType) -> bool:
+    """Determine if the given module is pydantic v2.
+
+    Given a module we expect to be a pydantic version, determine if it is pydantic v2.
+
+    Args:
+        module: A module.
+
+    Returns:
+        True if the module is pydantic v2, otherwise False.
+    """
+    return bool(module.__version__.startswith("2."))
diff --git a/litestar/datastructures/__init__.py b/litestar/datastructures/__init__.py
index 74fc25b5b..cddf0ae3e 100644
--- a/litestar/datastructures/__init__.py
+++ b/litestar/datastructures/__init__.py
@@ -14,6 +14,7 @@ from litestar.datastructures.multi_dicts import (
     MultiMixin,
 )
 from litestar.datastructures.response_header import ResponseHeader
+from litestar.datastructures.secret_values import SecretBytes, SecretString
 from litestar.datastructures.state import ImmutableState, State
 from litestar.datastructures.upload_file import UploadFile
 from litestar.datastructures.url import URL, Address
@@ -33,6 +34,8 @@ __all__ = (
     "MultiMixin",
     "MutableScopeHeaders",
     "ResponseHeader",
+    "SecretBytes",
+    "SecretString",
     "State",
     "UploadFile",
     "URL",
diff --git a/litestar/datastructures/secret_values.py b/litestar/datastructures/secret_values.py
new file mode 100644
index 000000000..de7b53212
--- /dev/null
+++ b/litestar/datastructures/secret_values.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Generic, TypeVar, Union
+
+__all__ = (
+    "SecretBytes",
+    "SecretString",
+    "SecretT",
+    "SecretValue",
+)
+
+# NOTE: Union instead of ` | ` is used for compatibility with Sphinx autodoc.
+#  - Sphinx autodoc doesn't handle string annotations for "bound" parameter in TypeVar.
+#  - We cannot use `|` without string annotations due to supporting < python 3.10.
+SecretT = TypeVar("SecretT", bound=Union[str, bytes])
+"""Type that represents a secret value of type ``str`` or ``bytes``."""
+
+
+class SecretValue(ABC, Generic[SecretT]):
+    """Represents a secret value that can be of type `str` or `bytes`."""
+
+    def __init__(self, secret_value: SecretT) -> None:
+        """Initializes a :class:`SecretValue` object with a secret value of type ``str`` or ``bytes``.
+
+        Args:
+            secret_value (str | bytes): The secret value to be encapsulated.
+        """
+
+        self._secret_value = secret_value
+
+    def get_secret(self) -> SecretT:
+        """Returns the actual secret value.
+
+        Returns:
+            str | bytes: The secret value.
+        """
+
+        return self._secret_value
+
+    @abstractmethod
+    def get_obscured(self) -> SecretT:
+        """Return the hidden representation of the secret value.
+
+        Raises:
+            NotImplementedError: Always raised to enforce implementation in subclasses.
+        """
+
+        raise NotImplementedError("Subclasses must implement get_obscured")
+
+    def __str__(self) -> str:
+        """Returns a string representation of the hidden secret value.
+
+        Returns:
+            str: String representation of the hidden secret value.
+        """
+
+        return str(self.get_obscured())
+
+    def __repr__(self) -> str:
+        """Returns a string representation of the object for debugging purposes.
+
+        Returns:
+            str: String representation of the object.
+        """
+
+        class_name = self.__class__.__name__
+        return f"{class_name}({self.get_obscured()!r})"
+
+
+class SecretString(SecretValue[str]):
+    """Represents a secret string value."""
+
+    def get_obscured(self) -> str:
+        """Overrides the base class method to return the hidden string value.
+
+        Returns:
+            str: The hidden string representation of the secret value.
+        """
+
+        return "******"
+
+
+class SecretBytes(SecretValue[bytes]):
+    """Represents a secret bytes value."""
+
+    def get_obscured(self) -> bytes:
+        """Overrides the base class method to return the hidden bytes value.
+
+        Returns:
+            bytes: The hidden bytes representation of the secret value.
+        """
+
+        return b"******"
diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py
index 1803af39a..b6d58b398 100644
--- a/litestar/handlers/base.py
+++ b/litestar/handlers/base.py
@@ -27,6 +27,7 @@ from litestar.utils.signature import ParsedSignature, add_types_to_signature_nam
 if TYPE_CHECKING:
     from typing_extensions import Self
 
+    from litestar._kwargs import KwargsModel
     from litestar.app import Litestar
     from litestar.connection import ASGIConnection
     from litestar.controller import Controller
@@ -35,6 +36,7 @@ if TYPE_CHECKING:
     from litestar.router import Router
     from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler
     from litestar.types.empty import EmptyType
+    from litestar.types.internal_types import PathParameterDefinition
 
 __all__ = ("BaseRouteHandler",)
 
@@ -564,3 +566,18 @@ class BaseRouteHandler:
         if not hasattr(target, "__qualname__"):
             target = type(target)
         return f"{target.__module__}.{target.__qualname__}"
+
+    def create_kwargs_model(
+        self,
+        path_parameters: dict[str, PathParameterDefinition],
+    ) -> KwargsModel:
+        """Create a `KwargsModel` for a given route handler."""
+        from litestar._kwargs import KwargsModel
+
+        return KwargsModel.create_for_signature_model(
+            signature_model=self.signature_model,
+            parsed_signature=self.parsed_fn_signature,
+            dependencies=self.resolve_dependencies(),
+            path_parameters=set(path_parameters.keys()),
+            layered_parameters=self.resolve_layered_parameters(),
+        )
diff --git a/litestar/openapi/controller.py b/litestar/openapi/controller.py
index d99f3c89c..effc2ea3d 100644
--- a/litestar/openapi/controller.py
+++ b/litestar/openapi/controller.py
@@ -1,9 +1,10 @@
 from __future__ import annotations
 
 from functools import cached_property
-from typing import TYPE_CHECKING, Any, Callable, Literal
+from typing import TYPE_CHECKING, Any, Callable, Final, Literal
+from uuid import uuid4
 
-from litestar.constants import OPENAPI_JSON_HANDLER_NAME, OPENAPI_NOT_INITIALIZED
+from litestar.constants import OPENAPI_NOT_INITIALIZED
 from litestar.controller import Controller
 from litestar.enums import MediaType, OpenAPIMediaType
 from litestar.exceptions import ImproperlyConfiguredException
@@ -14,13 +15,17 @@ from litestar.serialization import encode_json
 from litestar.serialization.msgspec_hooks import decode_json
 from litestar.status_codes import HTTP_404_NOT_FOUND
 
-__all__ = ("OpenAPIController",)
-
-
 if TYPE_CHECKING:
     from litestar.connection.request import Request
     from litestar.openapi.spec.open_api import OpenAPI
 
+__all__ = ("OpenAPIController",)
+
+# NOTE: We are explicitly using a different name to the one defined in litestar.constants so that an openapi
+#   controller can be added to a router on the same application as the openapi router.
+#   See: https://github.com/litestar-org/litestar/issues/3337
+OPENAPI_JSON_HANDLER_NAME: Final = f"{uuid4().hex}_litestar_openapi_json"
+
 
 class OpenAPIController(Controller):
     """Controller for OpenAPI endpoints."""
diff --git a/litestar/routes/base.py b/litestar/routes/base.py
index b9baab486..6130c41c0 100644
--- a/litestar/routes/base.py
+++ b/litestar/routes/base.py
@@ -10,14 +10,12 @@ from uuid import UUID
 
 import msgspec
 
-from litestar._kwargs import KwargsModel
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.types.internal_types import PathParameterDefinition
 from litestar.utils import join_paths, normalize_path
 
 if TYPE_CHECKING:
     from litestar.enums import ScopeType
-    from litestar.handlers.base import BaseRouteHandler
     from litestar.types import Method, Receive, Scope, Send
 
 
@@ -101,10 +99,7 @@ class BaseRoute(ABC):
             scope_type: Type of the ASGI scope
             methods: Supported methods
         """
-        self.path, self.path_format, self.path_components = self._parse_path(path)
-        self.path_parameters: tuple[PathParameterDefinition, ...] = tuple(
-            component for component in self.path_components if isinstance(component, PathParameterDefinition)
-        )
+        self.path, self.path_format, self.path_components, self.path_parameters = self._parse_path(path)
         self.handler_names = handler_names
         self.scope_type = scope_type
         self.methods = set(methods or [])
@@ -123,23 +118,6 @@ class BaseRoute(ABC):
         """
         raise NotImplementedError("Route subclasses must implement handle which serves as the ASGI app entry point")
 
-    def create_handler_kwargs_model(self, route_handler: BaseRouteHandler) -> KwargsModel:
-        """Create a `KwargsModel` for a given route handler."""
-
-        path_parameters = set()
-        for param in self.path_parameters:
-            if param.name in path_parameters:
-                raise ImproperlyConfiguredException(f"Duplicate parameter '{param.name}' detected in '{self.path}'.")
-            path_parameters.add(param.name)
-
-        return KwargsModel.create_for_signature_model(
-            signature_model=route_handler.signature_model,
-            parsed_signature=route_handler.parsed_fn_signature,
-            dependencies=route_handler.resolve_dependencies(),
-            path_parameters=path_parameters,
-            layered_parameters=route_handler.resolve_layered_parameters(),
-        )
-
     @staticmethod
     def _validate_path_parameter(param: str, path: str) -> None:
         """Validate that a path parameter adheres to the required format and datatypes.
@@ -160,7 +138,9 @@ class BaseRoute(ABC):
             )
 
     @classmethod
-    def _parse_path(cls, path: str) -> tuple[str, str, list[str | PathParameterDefinition]]:
+    def _parse_path(
+        cls, path: str
+    ) -> tuple[str, str, list[str | PathParameterDefinition], dict[str, PathParameterDefinition]]:
         """Normalize and parse a path.
 
         Splits the path into a list of components, parsing any that are path parameters. Also builds the OpenAPI
@@ -173,6 +153,7 @@ class BaseRoute(ABC):
 
         parsed_components: list[str | PathParameterDefinition] = []
         path_format_components = []
+        path_parameters: dict[str, PathParameterDefinition] = {}
 
         components = [component for component in path.split("/") if component]
         for component in components:
@@ -182,9 +163,11 @@ class BaseRoute(ABC):
                 param_name, param_type = (p.strip() for p in param.split(":"))
                 type_class = param_type_map[param_type]
                 parser = parsers_map[type_class] if type_class not in {str, Path} else None
-                parsed_components.append(
-                    PathParameterDefinition(name=param_name, type=type_class, full=param, parser=parser)
-                )
+                if param_name in path_parameters:
+                    raise ImproperlyConfiguredException(f"Duplicate parameter '{param_name}' detected in '{path}'.")
+                param_definition = PathParameterDefinition(name=param_name, type=type_class, full=param, parser=parser)
+                parsed_components.append(param_definition)
+                path_parameters[param_name] = param_definition
                 path_format_components.append("{" + param_name + "}")
             else:
                 parsed_components.append(component)
@@ -192,4 +175,4 @@ class BaseRoute(ABC):
 
         path_format = join_paths(path_format_components)
 
-        return path, path_format, parsed_components
+        return path, path_format, parsed_components, path_parameters
diff --git a/litestar/routes/http.py b/litestar/routes/http.py
index b1f70cb36..1b58b10f4 100644
--- a/litestar/routes/http.py
+++ b/litestar/routes/http.py
@@ -96,7 +96,7 @@ class HTTPRoute(BaseRoute):
         http- methods and route handlers.
         """
         for route_handler in self.route_handlers:
-            kwargs_model = self.create_handler_kwargs_model(route_handler=route_handler)
+            kwargs_model = route_handler.create_kwargs_model(path_parameters=self.path_parameters)
             for http_method in route_handler.http_methods:
                 if self.route_handler_map.get(http_method):
                     raise ImproperlyConfiguredException(
diff --git a/litestar/serialization/msgspec_hooks.py b/litestar/serialization/msgspec_hooks.py
index 3d7fdb8da..ba98ee4a4 100644
--- a/litestar/serialization/msgspec_hooks.py
+++ b/litestar/serialization/msgspec_hooks.py
@@ -19,6 +19,7 @@ from uuid import UUID
 
 import msgspec
 
+from litestar.datastructures.secret_values import SecretBytes, SecretString
 from litestar.exceptions import SerializationException
 from litestar.types import Empty, EmptyType, Serializer, TypeDecodersSequence
 
@@ -52,6 +53,8 @@ DEFAULT_TYPE_ENCODERS: TypeEncodersMap = {
     deque: list,
     Decimal: lambda val: int(val) if val.as_tuple().exponent >= 0 else float(val),
     Pattern: lambda val: val.pattern,
+    SecretBytes: lambda val: val.get_obscured().decode("utf-8"),
+    SecretString: lambda val: val.get_obscured(),
     # support subclasses of stdlib types, If no previous type matched, these will be
     # the last type in the mro, so we use this to (attempt to) convert a subclass into
     # its base class. # see https://github.com/jcrist/msgspec/issues/248
@@ -115,6 +118,12 @@ def default_deserializer(
     if issubclass(target_type, (Path, PurePath, ImmutableState, UUID)):
         return target_type(value)
 
+    if issubclass(target_type, SecretBytes) and isinstance(value, (bytes, str)):
+        return SecretBytes(value.encode("utf-8") if isinstance(value, str) else value)
+
+    if issubclass(target_type, SecretString) and isinstance(value, str):
+        return SecretString(value)
+
     raise TypeError(f"Unsupported type: {type(value)!r}")
 
 
diff --git a/test_apps/pydantic_1_app.py b/test_apps/pydantic_1_app.py
index 69772d380..41915d14d 100644
--- a/test_apps/pydantic_1_app.py
+++ b/test_apps/pydantic_1_app.py
@@ -19,6 +19,8 @@ async def handler(data: Foo) -> Foo:
 
 class TestApp(unittest.TestCase):
     def test_app(self) -> None:
+        assert pydantic.__version__.startswith("1."), pydantic.__version__
+
         with create_test_client([handler]) as client:
             data = {"bar": "baz", "baz": ["a", "b", "c"]}
             res = client.post("/", json=data)
diff --git a/tests/examples/test_datastructures/__init__.py b/tests/examples/test_datastructures/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/examples/test_datastructures/test_secrets.py b/tests/examples/test_datastructures/test_secrets.py
new file mode 100644
index 000000000..827599778
--- /dev/null
+++ b/tests/examples/test_datastructures/test_secrets.py
@@ -0,0 +1,21 @@
+from docs.examples.datastructures.secrets.secret_body import post_handler
+from docs.examples.datastructures.secrets.secret_header import get_handler
+
+from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED
+from litestar.testing import create_test_client
+
+
+def test_secret_header() -> None:
+    with create_test_client(get_handler) as client:
+        r = client.get("/", headers={"x-secret": "super-secret"})
+
+    assert r.status_code == HTTP_200_OK
+    assert r.json() == {"value": "sensitive data"}
+
+
+def test_secret_body() -> None:
+    with create_test_client(post_handler) as client:
+        r = client.post("/", json={"value": "super-secret"})
+
+    assert r.status_code == HTTP_201_CREATED
+    assert r.json() == {"value": "******"}
diff --git a/tests/unit/test_datastructures/test_secret_values.py b/tests/unit/test_datastructures/test_secret_values.py
new file mode 100644
index 000000000..a23e3fa75
--- /dev/null
+++ b/tests/unit/test_datastructures/test_secret_values.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+import msgspec
+import pytest
+from typing_extensions import Annotated
+
+from litestar import Litestar, get, post
+from litestar.datastructures.secret_values import SecretBytes, SecretString
+from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter
+from litestar.openapi.spec.schema import Schema
+from litestar.params import Parameter
+from litestar.serialization import default_deserializer, default_serializer
+from litestar.testing import create_test_client
+
+
+def test_secret_string_get_secret() -> None:
+    secret_string = SecretString("some_secret_value")
+    assert secret_string.get_secret() == "some_secret_value"
+
+
+def test_secret_string_get_obscured_representation() -> None:
+    secret_string = SecretString("some_secret_value")
+    assert secret_string.get_obscured() == "******"
+
+
+def test_secret_string_str() -> None:
+    secret_string = SecretString("some_secret_value")
+    assert str(secret_string) == "******"
+
+
+def test_secret_string_repr() -> None:
+    secret_string = SecretString("some_secret_value")
+    assert repr(secret_string) == "SecretString('******')"
+
+
+def test_secret_bytes_get_secret() -> None:
+    secret_bytes = SecretBytes(b"some_secret_value")
+    assert secret_bytes.get_secret() == b"some_secret_value"
+
+
+def test_secret_bytes_get_obscured_representation() -> None:
+    secret_bytes = SecretBytes(b"some_secret_value")
+    assert secret_bytes.get_obscured() == b"******"
+
+
+def test_secret_bytes_str() -> None:
+    secret_bytes = SecretBytes(b"some_secret_value")
+    assert str(secret_bytes) == str(b"******")
+
+
+def test_secret_bytes_repr() -> None:
+    secret_bytes = SecretBytes(b"some_secret_value")
+    assert repr(secret_bytes) == "SecretBytes(b'******')"
+
+
+def test_secret_string_encode() -> None:
+    secret_string = SecretString("some_secret")
+    assert default_serializer(secret_string) == "******"
+
+
+def test_secret_bytes_encode() -> None:
+    secret_bytes = SecretBytes(b"some_secret")
+    assert default_serializer(secret_bytes) == "******"
+
+
+def test_secret_string_decode() -> None:
+    secret = default_deserializer(SecretString, "super-secret")
+    assert isinstance(secret, SecretString)
+    assert secret.get_secret() == "super-secret"
+
+
+def test_secret_bytes_decode() -> None:
+    secret = default_deserializer(SecretBytes, b"super-secret")
+    assert isinstance(secret, SecretBytes)
+    assert secret.get_secret() == b"super-secret"
+
+
+def test_secret_string_parameter() -> None:
+    @get()
+    def get_secret(secret: SecretString) -> SecretBytes:
+        assert secret.get_secret() == "super-secret"
+        return SecretBytes(b"super-secret")
+
+    with create_test_client([get_secret]) as client:
+        response = client.get("/?secret=super-secret")
+        assert response.status_code == 200
+        assert response.json() == "******"
+
+
+def test_decode_secret_string_on_model() -> None:
+    class Model(msgspec.Struct):
+        secret: SecretString
+
+    @post(signature_types=[Model])
+    async def post_secret(data: Model) -> None:
+        assert data.secret.get_secret() == "super"
+
+    with create_test_client([post_secret]) as client:
+        response = client.post("/", json={"secret": "super"})
+        assert response.status_code == 201
+
+
+def test_decode_secret_bytes_on_model() -> None:
+    class Model(msgspec.Struct):
+        secret: SecretBytes
+
+    @post(signature_types=[Model])
+    async def post_secret(data: Model) -> None:
+        assert data.secret.get_secret() == b"super"
+
+    with create_test_client([post_secret]) as client:
+        response = client.post("/", json={"secret": "super"})
+        assert response.status_code == 201
+
+
+@pytest.mark.parametrize(("secret_type",), [(SecretString,), (SecretBytes,)])
+def test_decode_secret_string_on_model_client_error(secret_type: type[SecretString | SecretBytes]) -> None:
+    model = msgspec.defstruct(name="Model", fields=[("secret", secret_type)])
+
+    @post(signature_namespace={"model": model})
+    async def post_secret(data: model) -> None:  # type: ignore[valid-type]
+        return None
+
+    with create_test_client([post_secret]) as client:
+        response = client.post("/", json={"secret": 123})
+        assert response.status_code == 400
+        assert response.json() == {
+            "status_code": 400,
+            "detail": "Validation failed for POST /",
+            "extra": [{"message": "Unsupported type: <class 'int'>", "key": "secret", "source": "body"}],
+        }
+
+
+def test_secret_openapi() -> None:
+    @get(sync_to_thread=False)
+    def get_secret(secret: Annotated[SecretString, Parameter(header="x-secret")]) -> str:
+        return secret.get_obscured()
+
+    app = Litestar(route_handlers=[get_secret])
+    paths = app.openapi_schema.paths
+    assert paths is not None
+    op = paths["/"].get
+    assert op is not None
+    assert op.parameters is not None
+    param = op.parameters[0]
+    assert isinstance(param, OpenAPIParameter)
+    assert param.name == "x-secret"
+    assert param.param_in == "header"
+    assert isinstance(param.schema, Schema)
+    assert param.schema.type == "string"
+
+
+def test_secret_value_in_model_repr() -> None:
+    class Model(msgspec.Struct):
+        string: SecretString
+        bytes: SecretBytes
+
+    model = Model(string=SecretString("super-secret"), bytes=SecretBytes(b"super-secret"))
+    assert repr(model) == "Model(string=SecretString('******'), bytes=SecretBytes(b'******'))"
+    assert str(model) == "Model(string=SecretString('******'), bytes=SecretBytes(b'******'))"
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
index b0acbf8c7..2a84b32fd 100644
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -10,7 +10,7 @@ import pytest
 import yaml
 from typing_extensions import Annotated
 
-from litestar import Controller, Litestar, delete, get, patch, post
+from litestar import Controller, Litestar, Router, delete, get, patch, post
 from litestar._openapi.plugin import OpenAPIPlugin
 from litestar.enums import MediaType, OpenAPIMediaType, ParamType
 from litestar.openapi import OpenAPIConfig, OpenAPIController
@@ -503,3 +503,40 @@ def test_components_schemas_in_alphabetical_order() -> None:
         "test_components_schemas_in_alphabetical_order.C",
     ]
     assert list(openapi.components.schemas.keys()) == expected_keys
+
+
+def test_openapi_controller_and_openapi_router_on_same_app() -> None:
+    """Test that OpenAPIController and OpenAPIRouter can coexist on the same app.
+
+    As part of backward compatibility with new plugin-based OpenAPI router approach, we did not consider
+    the case where an OpenAPIController is registered on the application by means other than via the
+    OpenAPIConfig object. This is an approach that has been used to serve the openapi both under the
+    `/schema` and `/some-prefix/schema` paths. This test ensures that the OpenAPIController and OpenAPIRouter
+    can coexist on the same app.
+
+    See: https://github.com/litestar-org/litestar/issues/3337
+    """
+    router = Router(path="/abc", route_handlers=[OpenAPIController])
+    openapi_config = OpenAPIConfig("Litestar", "v0.0.1")  # no openapi_controller specified means we use the router
+    app = Litestar([router], openapi_config=openapi_config)
+    assert sorted(r.path for r in app.routes) == [
+        "/abc/schema",
+        "/abc/schema/elements",
+        "/abc/schema/oauth2-redirect.html",
+        "/abc/schema/openapi.json",
+        "/abc/schema/openapi.yaml",
+        "/abc/schema/openapi.yml",
+        "/abc/schema/rapidoc",
+        "/abc/schema/redoc",
+        "/abc/schema/swagger",
+        "/schema",
+        "/schema/elements",
+        "/schema/oauth2-redirect.html",
+        "/schema/openapi.json",
+        "/schema/openapi.yaml",
+        "/schema/openapi.yml",
+        "/schema/rapidoc",
+        "/schema/redoc",
+        "/schema/swagger",
+        "/schema/{path:str}",
+    ]