diff --git a/docs/staticfiles.md b/docs/staticfiles.md
index 03d9a58..f1b9d0a 100644
--- a/docs/staticfiles.md
+++ b/docs/staticfiles.md
@@ -1,20 +1,17 @@
 
-As well as the `FileResponse` class, Starlette also includes ASGI applications
-for serving a specific file or directory:
+Starlette also includes an `StaticFiles` class for serving a specific directory:
 
-* `StaticFile(path)` - Serve a single file, given by `path`.
 * `StaticFiles(directory)` - Serve any files in the given `directory`.
 
-You can combine these ASGI applications with Starlette's routing to provide
+You can combine this ASGI application with Starlette's routing to provide
 comprehensive static file serving.
 
 ```python
 from starlette.routing import Router, Path, PathPrefix
-from starlette.staticfiles import StaticFile, StaticFiles
+from starlette.staticfiles import StaticFiles
 
 
 app = Router(routes=[
-    Path('/', app=StaticFile(path='index.html')),
     PathPrefix('/static', app=StaticFiles(directory='static')),
 ])
 ```
diff --git a/starlette/formparsers.py b/starlette/formparsers.py
index 4d6580c..6d83320 100644
--- a/starlette/formparsers.py
+++ b/starlette/formparsers.py
@@ -1,9 +1,11 @@
-from enum import Enum
-from starlette.datastructures import Headers
-import asyncio
 import io
-import tempfile
 import typing
+import asyncio
+import tempfile
+from enum import Enum
+from urllib.parse import unquote
+
+from starlette.datastructures import Headers
 
 try:
     from multipart.multipart import parse_options_header
@@ -69,27 +71,22 @@ class FormParser:
         self.messages = []  # type: typing.List[typing.Tuple[FormMessage, bytes]]
 
     def on_field_start(self) -> None:
-        print("on_field_start")
         message = (FormMessage.FIELD_START, b"")
         self.messages.append(message)
 
     def on_field_name(self, data: bytes, start: int, end: int) -> None:
-        print("on_field_name")
         message = (FormMessage.FIELD_NAME, data[start:end])
         self.messages.append(message)
 
     def on_field_data(self, data: bytes, start: int, end: int) -> None:
-        print("on_field_data")
         message = (FormMessage.FIELD_DATA, data[start:end])
         self.messages.append(message)
 
     def on_field_end(self) -> None:
-        print("on_field_end")
         message = (FormMessage.FIELD_END, b"")
         self.messages.append(message)
 
     def on_end(self) -> None:
-        print("on_end")
         message = (FormMessage.END, b"")
         self.messages.append(message)
 
@@ -127,7 +124,9 @@ class FormParser:
                 elif message_type == FormMessage.FIELD_DATA:
                     field_value += message_bytes
                 elif message_type == FormMessage.FIELD_END:
-                    result[field_name.decode("latin-1")] = field_value.decode("latin-1")
+                    result[field_name.decode("latin-1")] = unquote(
+                        field_value.decode("latin-1")
+                    )
                 elif message_type == FormMessage.END:
                     pass
 
diff --git a/starlette/responses.py b/starlette/responses.py
index 3471fbf..09ac58f 100644
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -1,15 +1,16 @@
-import hashlib
 import os
-import typing
 import json
-
+import stat
+import typing
+import hashlib
+import http.cookies
 from email.utils import formatdate
 from mimetypes import guess_type
+from urllib.parse import quote_plus
+
 from starlette.background import BackgroundTask
 from starlette.datastructures import MutableHeaders, URL
 from starlette.types import Receive, Send
-from urllib.parse import quote_plus
-import http.cookies
 
 try:
     import aiofiles
@@ -227,8 +228,15 @@ class FileResponse(Response):
 
     async def __call__(self, receive: Receive, send: Send) -> None:
         if self.stat_result is None:
-            stat_result = await aio_stat(self.path)
-            self.set_stat_headers(stat_result)
+            try:
+                stat_result = await aio_stat(self.path)
+                self.set_stat_headers(stat_result)
+            except FileNotFoundError:
+                raise RuntimeError(f"File at path {self.path} does not exist.")
+            else:
+                mode = stat_result.st_mode
+                if not stat.S_ISREG(mode):
+                    raise RuntimeError(f"File at path {self.path} is not a file.")
         await send(
             {
                 "type": "http.response.start",
diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py
index eb8f748..b9b54cf 100644
--- a/starlette/staticfiles.py
+++ b/starlette/staticfiles.py
@@ -7,17 +7,6 @@ from starlette.responses import PlainTextResponse, FileResponse, Response
 from starlette.types import Send, Receive, Scope, ASGIInstance
 
 
-class StaticFile:
-    def __init__(self, *, path: str) -> None:
-        self.path = path
-
-    def __call__(self, scope: Scope) -> ASGIInstance:
-        assert scope["type"] == "http"
-        if scope["method"] not in ("GET", "HEAD"):
-            return PlainTextResponse("Method Not Allowed", status_code=405)
-        return _StaticFileResponder(scope, path=self.path)
-
-
 class StaticFiles:
     def __init__(self, *, directory: str) -> None:
         self.directory = directory
@@ -39,25 +28,6 @@ class StaticFiles:
         return _StaticFilesResponder(scope, path=path, check_directory=check_directory)
 
 
-class _StaticFileResponder:
-    def __init__(self, scope: Scope, path: str) -> None:
-        self.scope = scope
-        self.path = path
-
-    async def __call__(self, receive: Receive, send: Send) -> None:
-        try:
-            stat_result = await aio_stat(self.path)
-        except FileNotFoundError:
-            raise RuntimeError("StaticFile at path '%s' does not exist." % self.path)
-        else:
-            mode = stat_result.st_mode
-            if not stat.S_ISREG(mode):
-                raise RuntimeError("StaticFile at path '%s' is not a file." % self.path)
-
-        response = FileResponse(self.path, stat_result=stat_result)
-        await response(receive, send)
-
-
 class _StaticFilesResponder:
     def __init__(self, scope: Scope, path: str, check_directory: str = None) -> None:
         self.scope = scope

Drop `StaticFile` app.
We have `FileResponse` and `StaticFiles`.

I think that including the `StaticFile` ASGI app complicates things unnecessarily, and that we should probably remove it.

* Drop `StaticFile` app.
* Put runtime checks that file exists, and file is a regular file in `FileResponse`.
diff --git a/tests/test_responses.py b/tests/test_responses.py
index 467b360..63d5076 100644
--- a/tests/test_responses.py
+++ b/tests/test_responses.py
@@ -9,6 +9,7 @@ from starlette.requests import Request
 from starlette.testclient import TestClient
 from starlette import status
 import asyncio
+import pytest
 import os
 
 
@@ -144,6 +145,28 @@ def test_file_response(tmpdir):
     assert "etag" in response.headers
 
 
+def test_file_response_with_directory_raises_error(tmpdir):
+    def app(scope):
+        return FileResponse(path=tmpdir, filename="example.png")
+
+    client = TestClient(app)
+    with pytest.raises(RuntimeError) as exc:
+        client.get("/")
+    assert "is not a file" in str(exc)
+
+
+def test_file_response_with_missing_file_raises_error(tmpdir):
+    path = os.path.join(tmpdir, "404.txt")
+
+    def app(scope):
+        return FileResponse(path=path, filename="404.txt")
+
+    client = TestClient(app)
+    with pytest.raises(RuntimeError) as exc:
+        client.get("/")
+    assert "does not exist" in str(exc)
+
+
 def test_set_cookie():
     def app(scope):
         async def asgi(receive, send):
diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py
index e21ce60..bc7ef0f 100644
--- a/tests/test_staticfiles.py
+++ b/tests/test_staticfiles.py
@@ -2,63 +2,7 @@ import os
 import pytest
 
 from starlette.testclient import TestClient
-from starlette.staticfiles import StaticFile, StaticFiles
-
-
-def test_staticfile(tmpdir):
-    path = os.path.join(tmpdir, "example.txt")
-    with open(path, "w") as file:
-        file.write("<file content>")
-
-    app = StaticFile(path=path)
-    client = TestClient(app)
-    response = client.get("/")
-    assert response.status_code == 200
-    assert response.text == "<file content>"
-
-
-def test_large_staticfile(tmpdir):
-    path = os.path.join(tmpdir, "example.txt")
-    content = "this is a lot of content" * 200
-    print("content len = ", len(content))
-    with open(path, "w") as file:
-        file.write(content)
-
-    app = StaticFile(path=path)
-    client = TestClient(app)
-    response = client.get("/")
-    assert response.status_code == 200
-    assert len(content) == len(response.text)
-    assert content == response.text
-
-
-def test_staticfile_post(tmpdir):
-    path = os.path.join(tmpdir, "example.txt")
-    with open(path, "w") as file:
-        file.write("<file content>")
-
-    app = StaticFile(path=path)
-    client = TestClient(app)
-    response = client.post("/")
-    assert response.status_code == 405
-    assert response.text == "Method Not Allowed"
-
-
-def test_staticfile_with_directory_raises_error(tmpdir):
-    app = StaticFile(path=tmpdir)
-    client = TestClient(app)
-    with pytest.raises(RuntimeError) as exc:
-        client.get("/")
-    assert "is not a file" in str(exc)
-
-
-def test_staticfile_with_missing_file_raises_error(tmpdir):
-    path = os.path.join(tmpdir, "404.txt")
-    app = StaticFile(path=path)
-    client = TestClient(app)
-    with pytest.raises(RuntimeError) as exc:
-        client.get("/")
-    assert "does not exist" in str(exc)
+from starlette.staticfiles import StaticFiles
 
 
 def test_staticfiles(tmpdir):

pip install -e .[full]
parse_log_pytest
requirements.txt
pytest codecov mypy black
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiofiles==24.1.0
babel==2.17.0
backrefs==5.8
black==25.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
codecov==2.1.13
colorama==0.4.6
coverage==7.8.0
exceptiongroup==1.2.2
ghp-import==2.1.0
graphene==3.4.3
graphql-core==3.2.6
graphql-relay==3.2.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
Jinja2==3.1.6
Markdown==3.7
MarkupSafe==3.0.2
mergedeep==1.3.4
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.10
mkdocs-material-extensions==1.3.1
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
Pygments==2.19.1
pymdown-extensions==10.14.3
pytest==8.3.5
pytest-cov==6.0.0
python-dateutil==2.9.0.post0
python-multipart==0.0.20
PyYAML==6.0.2
pyyaml_env_tag==0.1
requests==2.32.3
six==1.17.0
-e git+https://github.com/encode/starlette.git@a32ea0a8e89567b24f3b8cd04847a1b01c9e98f0#egg=starlette
tomli==2.2.1
typing_extensions==4.13.0
ujson==5.10.0
urllib3==2.3.0
watchdog==6.0.0
zipp==3.21.0

name: starlette
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiofiles==24.1.0
      - babel==2.17.0
      - backrefs==5.8
      - black==25.1.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - click==8.1.8
      - codecov==2.1.13
      - colorama==0.4.6
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - ghp-import==2.1.0
      - graphene==3.4.3
      - graphql-core==3.2.6
      - graphql-relay==3.2.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markdown==3.7
      - markupsafe==3.0.2
      - mergedeep==1.3.4
      - mkdocs==1.6.1
      - mkdocs-get-deps==0.2.0
      - mkdocs-material==9.6.10
      - mkdocs-material-extensions==1.3.1
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - paginate==0.5.7
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pygments==2.19.1
      - pymdown-extensions==10.14.3
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - python-dateutil==2.9.0.post0
      - python-multipart==0.0.20
      - pyyaml==6.0.2
      - pyyaml-env-tag==0.1
      - requests==2.32.3
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - ujson==5.10.0
      - urllib3==2.3.0
      - watchdog==6.0.0
      - zipp==3.21.0
prefix: /opt/conda/envs/starlette

tests/test_responses.py::test_file_response_with_directory_raises_error tests/test_responses.py::test_file_response_with_missing_file_raises_error
tests/test_responses.py::test_text_response tests/test_responses.py::test_bytes_response tests/test_responses.py::test_ujson_response tests/test_responses.py::test_redirect_response tests/test_responses.py::test_streaming_response tests/test_responses.py::test_response_headers tests/test_responses.py::test_response_phrase tests/test_responses.py::test_file_response tests/test_responses.py::test_set_cookie tests/test_responses.py::test_delete_cookie tests/test_staticfiles.py::test_staticfiles tests/test_staticfiles.py::test_staticfiles_post tests/test_staticfiles.py::test_staticfiles_with_directory_returns_404 tests/test_staticfiles.py::test_staticfiles_with_missing_file_returns_404 tests/test_staticfiles.py::test_staticfiles_configured_with_missing_directory tests/test_staticfiles.py::test_staticfiles_configured_with_file_instead_of_directory tests/test_staticfiles.py::test_staticfiles_config_check_occurs_only_once tests/test_staticfiles.py::test_staticfiles_prevents_breaking_out_of_directory
starlette/responses.py starlette/staticfiles.py docs/staticfiles.md starlette/formparsers.py
starlette/responses.py starlette/staticfiles.py docs/staticfiles.md starlette/formparsers.py