Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -368,6 +368,48 @@ def register_error_handlers(app: Flask):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'The database rejected that change.', http_code=400)
|
||||
|
||||
# STRICT_TRANS_TABLES (see config.SQLALCHEMY_ENGINE_OPTIONS) turns what
|
||||
# MySQL used to silently adjust into an error: a value too long for its
|
||||
# column, a date that is not a date, a number that is not a number. That is
|
||||
# the behaviour we want - the alternative is a 201 for a row the caller did
|
||||
# not send - but the error is the CALLER's, so it must not read as a server
|
||||
# fault. Without this it surfaced as a 500 with a traceback in the log.
|
||||
from sqlalchemy.exc import DataError
|
||||
|
||||
@app.errorhandler(DataError)
|
||||
def handle_data_error(error):
|
||||
from .extensions import db
|
||||
db.session.rollback()
|
||||
message = str(getattr(error, 'orig', error))
|
||||
app.logger.warning('data error: %s', message)
|
||||
column = _column_from_data_error(message)
|
||||
if 'too long' in message.lower():
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'That value is too long for %s.' % (column or 'this field'),
|
||||
http_code=400)
|
||||
if 'incorrect date' in message.lower() or \
|
||||
'incorrect datetime' in message.lower():
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'That is not a valid date for %s.' % (column or 'this field'),
|
||||
http_code=400)
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'The database rejected that value%s.'
|
||||
% (' for %s' % column if column else ''),
|
||||
http_code=400)
|
||||
|
||||
# A wrong query argument is the caller's mistake. Registered app-wide so a
|
||||
# list route does not have to remember to catch it - forgetting was the
|
||||
# whole defect, in six routes across five plugins.
|
||||
from .utils.queryargs import QueryArgError
|
||||
|
||||
@app.errorhandler(QueryArgError)
|
||||
def handle_query_arg_error(error):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, str(error),
|
||||
http_code=400)
|
||||
|
||||
@app.errorhandler(ShopDBException)
|
||||
def handle_shopdb_exception(error):
|
||||
http_codes = {
|
||||
@@ -393,6 +435,48 @@ def register_error_handlers(app: Flask):
|
||||
http_code=404
|
||||
)
|
||||
|
||||
# Werkzeug answers these three itself, in HTML, so a JSON client asking a
|
||||
# question slightly wrong got a page of markup with no error code and no
|
||||
# envelope - and on a route the caller cannot see, the 405 also confirmed
|
||||
# that the path exists, which is how a gated kiosk route meant to look
|
||||
# absent announced itself.
|
||||
#
|
||||
# Only under /api/. Everything else is the SPA, which needs the framework's
|
||||
# own behaviour (and its own catch-all) left alone.
|
||||
from flask import request as flask_request
|
||||
|
||||
def _api_request():
|
||||
return flask_request.path.startswith('/api/')
|
||||
|
||||
@app.errorhandler(405)
|
||||
def method_not_allowed_error(error):
|
||||
if not _api_request():
|
||||
return error
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'That method is not allowed on this endpoint.',
|
||||
http_code=405)
|
||||
|
||||
@app.errorhandler(415)
|
||||
def unsupported_media_type_error(error):
|
||||
if not _api_request():
|
||||
return error
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'This endpoint expects a JSON body; send '
|
||||
'Content-Type: application/json.',
|
||||
http_code=415)
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request_error(error):
|
||||
if not _api_request():
|
||||
return error
|
||||
# Werkzeug raises this for malformed JSON before any route sees it.
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'The request body could not be read as JSON.',
|
||||
http_code=400)
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return error_response(
|
||||
@@ -410,6 +494,20 @@ def register_error_handlers(app: Flask):
|
||||
)
|
||||
|
||||
|
||||
def _column_from_data_error(message):
|
||||
"""Pull the column name out of a MySQL data error, when it names one.
|
||||
|
||||
MySQL says "Data too long for column 'username' at row 1"; SQLite says
|
||||
nothing of the sort. Best effort on purpose - a message that names the
|
||||
field is much more useful than a generic one, and a message that names
|
||||
nothing is still better than a 500.
|
||||
"""
|
||||
import re
|
||||
|
||||
found = re.search(r"column '([^']+)'", message or '')
|
||||
return found.group(1) if found else None
|
||||
|
||||
|
||||
def register_frontend_routes(app: Flask):
|
||||
"""Serve Vue frontend static files."""
|
||||
frontend_dist = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'dist')
|
||||
|
||||
Reference in New Issue
Block a user