mirror of
https://github.com/PRVTPRO/Amnezia-Web-Panel.git
synced 2026-09-18 01:44:10 +02:00
cf25956a9f
"Error loading connections: JSON.parse: unexpected character at line 1 column
1 of the JSON data" is what the panel shows whenever a response is not JSON,
and it says nothing about the actual failure. Two halves:
The app has no handler for unhandled exceptions, so Starlette answers with a
plain-text "Internal Server Error" - unparseable for every caller that does
`await res.json()`. /api/* now answers `{"error": "Internal server error"}`
with the traceback logged; pages keep the plain-text body, since a browser
showing an error page is fine.
apiCall read the body as JSON unconditionally, so a proxy error page, a
restart mid-request or an empty body surfaced as a parser complaint. It now
reads text, parses defensively and throws "HTTP <status>: <first line of the
body>", which points at what happened. The existing err.payload passthrough
is kept, so the wg-easy import log still survives the throw, and data.detail
is consulted for FastAPI's own validation errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""/api/* must answer JSON even when a handler blows up, and the browser
|
|
helper must survive an answer that is not JSON at all.
|
|
|
|
Without both halves a proxy error page, a restart mid-request or an unhandled
|
|
exception surfaces in the UI as "JSON.parse: unexpected character at line 1
|
|
column 1 of the JSON data", which says nothing about what actually happened.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
|
|
import app as panel
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def call_handler(path):
|
|
request = mock.Mock()
|
|
request.method = 'GET'
|
|
request.url.path = path
|
|
import asyncio
|
|
return asyncio.run(panel.api_json_error_handler(request, RuntimeError('boom')))
|
|
|
|
|
|
class ApiErrorHandlerTests(unittest.TestCase):
|
|
def test_handler_is_registered_for_unhandled_exceptions(self):
|
|
self.assertIn(Exception, panel.app.exception_handlers)
|
|
|
|
def test_api_paths_answer_json(self):
|
|
response = call_handler('/api/servers/0/connections')
|
|
self.assertIsInstance(response, JSONResponse)
|
|
self.assertEqual(response.status_code, 500)
|
|
self.assertIn(b'"error"', response.body)
|
|
|
|
def test_pages_keep_plain_text(self):
|
|
response = call_handler('/server/0')
|
|
self.assertIsInstance(response, PlainTextResponse)
|
|
self.assertEqual(response.status_code, 500)
|
|
|
|
|
|
class ApiCallHelperTests(unittest.TestCase):
|
|
"""Text-level checks of the fetch helper every page uses."""
|
|
|
|
def setUp(self):
|
|
with open(os.path.join(ROOT, 'templates', 'base.html'), encoding='utf-8') as f:
|
|
page = f.read()
|
|
start = page.index('async function apiCall(')
|
|
self.helper = page[start:page.index('\n }', start)]
|
|
|
|
def test_reads_text_and_parses_defensively(self):
|
|
self.assertIn('await res.text()', self.helper)
|
|
self.assertIn('JSON.parse(text)', self.helper)
|
|
self.assertNotIn('await res.json()', self.helper)
|
|
self.assertRegex(self.helper, r'catch \(parseError\)')
|
|
# the thrown message carries the status, not just the parser complaint
|
|
self.assertIn('HTTP ${res.status}', self.helper)
|
|
|
|
def test_still_redirects_on_an_expired_session(self):
|
|
self.assertIn("res.status === 401", self.helper)
|
|
self.assertIn("window.location.href = '/login'", self.helper)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|