[Feature]: Standardize Flask ML API errors behind a machine-readable error envelope (backward compatible)
Summary
Error responses from the Flask ML API are inconsistent in shape, which makes reliable client-side error handling impossible. Depending on which handler you hit you get:
{"error": "No text provided"} — /predict, /spam-insights, /importance
{"error": str(e)} — several except blocks (also leaks raw exception strings)
{"success": false, "error": "Forbidden: ..."} — zero-trust 403 / /api/wordcloud
{"error": "Internal server error", "request_id": ...} — the global 500 handler
{"error": "rate_limit_exceeded", "message": ..., "retry_after": 60} — the 429 handler
There is no error code vocabulary and no consistent place for request_id, so a consumer can't branch on error type or correlate a failure with server logs (even though g.request_id already exists).
This proposes a single, typed error envelope, added additively so existing clients that read error as a string keep working.
Proposed envelope
Legacy top-level error string stays, so nothing breaks (the README explicitly promises /predict backward compatibility). New/modern clients read error_detail.
Proposed solution & PR split
PR 1/2 — Error module + handlers + core endpoints (Hard)
| File |
Change |
backend/errors.py (new) |
ErrorCode(StrEnum), ApiError(Exception), error_response(code, message, status, request_id) builder |
backend/api.py |
register errorhandler(ApiError) + 400/403/404/429/500 → envelope (reusing existing 429 payload fields); migrate /predict input-validation returns, /feedback, /feedback/stats, /spam-insights, /importance, /api/wordcloud to the builder |
backend/tests/test_error_envelope.py (new) |
every migrated 4xx/5xx has error (str) and error_detail.{code,message,request_id}; codes are stable; legacy field preserved |
Standalone: the envelope is live and tested for the core endpoints.
PR 2/2 — Email / OAuth / IMAP endpoints + contract test (Hard)
| File |
Change |
backend/api.py |
migrate /analyze-email-header, /gmail/*, /outlook/*, /scan-emails, /imap/connect handlers to raise ApiError with codes (stop returning raw str(e)) |
backend/errors.py |
add provider/auth/upstream error codes |
backend/tests/test_error_contract.py (new) |
drive each error path and assert the envelope + that no handler leaks a bare str(e) without a code |
README.md |
"Error format" reference section |
Standalone: completes coverage + documents the contract; safe to merge after PR-1.
Difficulty: Hard (each sub-PR)
Touching every error path in a large module, introducing an enum-backed exception type + handlers, preserving backward compatibility, and adding contract tests that assert the schema is depth work
[Feature]: Standardize Flask ML API errors behind a machine-readable error envelope (backward compatible)
Summary
Error responses from the Flask ML API are inconsistent in shape, which makes reliable client-side error handling impossible. Depending on which handler you hit you get:
{"error": "No text provided"}—/predict,/spam-insights,/importance{"error": str(e)}— severalexceptblocks (also leaks raw exception strings){"success": false, "error": "Forbidden: ..."}— zero-trust 403 //api/wordcloud{"error": "Internal server error", "request_id": ...}— the global 500 handler{"error": "rate_limit_exceeded", "message": ..., "retry_after": 60}— the 429 handlerThere is no error code vocabulary and no consistent place for
request_id, so a consumer can't branch on error type or correlate a failure with server logs (even thoughg.request_idalready exists).This proposes a single, typed error envelope, added additively so existing clients that read
erroras a string keep working.Proposed envelope
{ "error": "Human readable message", // kept for backward compatibility "error_detail": { "code": "NO_TEXT_PROVIDED", // stable ErrorCode enum "message": "Human readable message", "request_id": "…" // always present, from g.request_id } }Legacy top-level
errorstring stays, so nothing breaks (the README explicitly promises/predictbackward compatibility). New/modern clients readerror_detail.Proposed solution & PR split
PR 1/2 — Error module + handlers + core endpoints (Hard)
backend/errors.py(new)ErrorCode(StrEnum),ApiError(Exception),error_response(code, message, status, request_id)builderbackend/api.pyerrorhandler(ApiError)+ 400/403/404/429/500 → envelope (reusing existing 429 payload fields); migrate/predictinput-validation returns,/feedback,/feedback/stats,/spam-insights,/importance,/api/wordcloudto the builderbackend/tests/test_error_envelope.py(new)error(str) anderror_detail.{code,message,request_id}; codes are stable; legacy field preservedStandalone: the envelope is live and tested for the core endpoints.
PR 2/2 — Email / OAuth / IMAP endpoints + contract test (Hard)
backend/api.py/analyze-email-header,/gmail/*,/outlook/*,/scan-emails,/imap/connecthandlers to raiseApiErrorwith codes (stop returning rawstr(e))backend/errors.pybackend/tests/test_error_contract.py(new)str(e)without a codeREADME.mdStandalone: completes coverage + documents the contract; safe to merge after PR-1.
Difficulty: Hard (each sub-PR)
Touching every error path in a large module, introducing an enum-backed exception type + handlers, preserving backward compatibility, and adding contract tests that assert the schema is depth work