patx/wred
Return JSON 404 errors from static file helper
Commit 1f824ad · patx · 2026-10-18T00:09:01-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 95d7b3c..3a9fc5f 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,7 @@ async def asset(request, filename):
Omitting `root`, passing `''`, or passing `None` uses the `static` directory in the
current working directory. Set `root` to a string or `Path` to use another directory.
MIME types are guessed; missing, unreadable, or outside-root files return
-plain-text 404, including symlink escapes. Files are read in a worker thread, entirely into memory.
+JSON 404 (`{"error":"Not Found"}`), including symlink escapes. Files are read in a worker thread, entirely into memory.
Small files. Don't feed it a movie.
## Errors
diff --git a/tests/test_wred.py b/tests/test_wred.py
index 4643a96..434e6d9 100644
--- a/tests/test_wred.py
+++ b/tests/test_wred.py
@@ -131,12 +131,12 @@ class WredTests(unittest.IsolatedAsyncioTestCase):
cases = [
('index.html', 200, b'text/html', b'<p>Hello</p>'),
('asset.unknownextension', 200, b'application/octet-stream', b'\x00\xff'),
- ('missing.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
- ('.', 404, b'text/plain; charset=utf-8', b'Not Found'),
- ('../secret.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
- (str(base / 'secret.txt'), 404, b'text/plain; charset=utf-8', b'Not Found'),
- ('escape.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
- ('invalid\x00.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ('missing.txt', 404, b'application/json', b'{"error":"Not Found"}'),
+ ('.', 404, b'application/json', b'{"error":"Not Found"}'),
+ ('../secret.txt', 404, b'application/json', b'{"error":"Not Found"}'),
+ (str(base / 'secret.txt'), 404, b'application/json', b'{"error":"Not Found"}'),
+ ('escape.txt', 404, b'application/json', b'{"error":"Not Found"}'),
+ ('invalid\x00.txt', 404, b'application/json', b'{"error":"Not Found"}'),
]
cases = [
(options, *case)
@@ -440,8 +440,8 @@ class WredTests(unittest.IsolatedAsyncioTestCase):
messages = await call_app(app, {'type': 'http', 'path': '/', 'method': 'GET'}, receive)
self.assertEqual(messages, [
{'type': 'http.response.start', 'status': 404,
- 'headers': [(b'content-type', b'text/plain; charset=utf-8')]},
- {'type': 'http.response.body', 'body': b'Not Found'},
+ 'headers': [(b'content-type', b'application/json')]},
+ {'type': 'http.response.body', 'body': b'{"error":"Not Found"}'},
])
async def test_raw_and_custom_response_bodies(self):
diff --git a/wred.py b/wred.py
index b6e03e8..c4a01fb 100644
--- a/wred.py
+++ b/wred.py
@@ -107,9 +107,9 @@ class App:
try:
directory = Path(root or 'static').resolve()
path = (directory / filename).resolve()
- if not path.is_relative_to(directory) or not path.is_file(): return b'Not Found', 'text/plain; charset=utf-8', 404
+ if not path.is_relative_to(directory) or not path.is_file(): return b'{"error":"Not Found"}', 'application/json', 404
return path.read_bytes(), guess_type(path.name)[0] or 'application/octet-stream', 200
- except (OSError, ValueError, RuntimeError): return b'Not Found', 'text/plain; charset=utf-8', 404
+ except (OSError, ValueError, RuntimeError): return b'{"error":"Not Found"}', 'application/json', 404
return await to_thread(read)
def route(self, path: str, methods: list[str] | tuple[str, ...] = ('GET',)) -> Callable: