feat: enhance error handling and configuration validation
Some checks failed
CI / lint-and-test (push) Failing after 27s
Some checks failed
CI / lint-and-test (push) Failing after 27s
- Added a global exception handler to log unhandled exceptions and return a generic 500 JSON response without exposing details to the client. - Updated the configuration to validate the `DATABASE_URL` format, ensuring it starts with `sqlite://` or `postgresql://`, and log warnings for invalid formats. - Introduced safe parsing for numeric environment variables (`HTTP_PORT`, `INIT_DATA_MAX_AGE_SECONDS`) with defaults on invalid values, including logging warnings for out-of-range values. - Enhanced the duty schedule parser to enforce limits on the number of schedule rows and the length of full names and duty strings, raising appropriate errors when exceeded. - Updated internationalization messages to include generic error responses for import failures and parsing issues, improving user experience. - Added unit tests to verify the new error handling and configuration validation behaviors.
This commit is contained in:
@@ -9,6 +9,11 @@ DUTY_MARKERS = frozenset({"б", "Б", "в", "В"})
|
||||
UNAVAILABLE_MARKER = "Н"
|
||||
VACATION_MARKER = "О"
|
||||
|
||||
# Limits to avoid abuse and unreasonable input.
|
||||
MAX_SCHEDULE_ROWS = 500
|
||||
MAX_FULL_NAME_LENGTH = 200
|
||||
MAX_DUTY_STRING_LENGTH = 10000
|
||||
|
||||
|
||||
@dataclass
|
||||
class DutyScheduleEntry:
|
||||
@@ -69,10 +74,24 @@ def parse_duty_schedule(raw_bytes: bytes) -> DutyScheduleResult:
|
||||
except ValueError as e:
|
||||
raise DutyScheduleParseError(f"Invalid meta.start_date: {start_str}") from e
|
||||
|
||||
# Reject dates outside current year ± 1.
|
||||
today = date.today()
|
||||
min_year = today.year - 1
|
||||
max_year = today.year + 1
|
||||
if not (min_year <= start_date.year <= max_year):
|
||||
raise DutyScheduleParseError(
|
||||
f"meta.start_date year must be between {min_year} and {max_year}"
|
||||
)
|
||||
|
||||
schedule = data.get("schedule")
|
||||
if not isinstance(schedule, list):
|
||||
raise DutyScheduleParseError("Missing or invalid 'schedule' (must be array)")
|
||||
|
||||
if len(schedule) > MAX_SCHEDULE_ROWS:
|
||||
raise DutyScheduleParseError(
|
||||
f"schedule has too many rows (max {MAX_SCHEDULE_ROWS})"
|
||||
)
|
||||
|
||||
max_days = 0
|
||||
entries: list[DutyScheduleEntry] = []
|
||||
|
||||
@@ -85,12 +104,20 @@ def parse_duty_schedule(raw_bytes: bytes) -> DutyScheduleResult:
|
||||
full_name = name.strip()
|
||||
if not full_name:
|
||||
raise DutyScheduleParseError("schedule item 'name' cannot be empty")
|
||||
if len(full_name) > MAX_FULL_NAME_LENGTH:
|
||||
raise DutyScheduleParseError(
|
||||
f"schedule item 'name' must not exceed {MAX_FULL_NAME_LENGTH} characters"
|
||||
)
|
||||
|
||||
duty_str = row.get("duty")
|
||||
if duty_str is None:
|
||||
duty_str = ""
|
||||
if not isinstance(duty_str, str):
|
||||
raise DutyScheduleParseError("schedule item 'duty' must be string")
|
||||
if len(duty_str) > MAX_DUTY_STRING_LENGTH:
|
||||
raise DutyScheduleParseError(
|
||||
f"schedule item 'duty' must not exceed {MAX_DUTY_STRING_LENGTH} characters"
|
||||
)
|
||||
|
||||
cells = [c.strip() for c in duty_str.split(";")]
|
||||
max_days = max(max_days, len(cells))
|
||||
@@ -120,4 +147,9 @@ def parse_duty_schedule(raw_bytes: bytes) -> DutyScheduleResult:
|
||||
else:
|
||||
end_date = start_date + timedelta(days=max_days - 1)
|
||||
|
||||
if not (min_year <= end_date.year <= max_year):
|
||||
raise DutyScheduleParseError(
|
||||
f"Computed end_date year must be between {min_year} and {max_year}"
|
||||
)
|
||||
|
||||
return DutyScheduleResult(start_date=start_date, end_date=end_date, entries=entries)
|
||||
|
||||
Reference in New Issue
Block a user