The Problem
Odoo is strict by design. A single missing field, permission issue, or constraint violation can expose raw Python stack traces or generic server exceptions in your Flutter app.
Showing errors like:
odoo.exceptions.UserError: Invalid Quantity
to end users is unacceptable in a production mobile application.
This guide explains how to correctly classify, parse, and handle Odoo server errors in Flutter, turning low-level backend exceptions into clean, user-friendly messages.
1. Understanding the Error Hierarchy
Before handling errors, you must understand what you are actually catching.
Network-Level Errors
- SocketException / ClientException
- Cause: No internet, server down, DNS failure
- User Message: "Server is unreachable. Please check your connection."
Session-Level Errors
- OdooSessionExpiredException
- HTTP 403 responses
- Redirects to /web/login (HTML instead of JSON)
Cause: Session expired or invalid cookies
User Message: "Your session has expired. Please log in again."
RPC / Server-Level Errors (Most Important)
These originate from Python code inside Odoo.
2. Robust Exception Parsing
| Exception Type | Meaning | Recommended UI Message |
| odoo.exceptions.UserError | Business logic violation | Show exact message |
| odoo.exceptions.ValidationError | Python constraint | Show exact message |
| odoo.exceptions.AccessError | Security / ACL rule | "Access denied" |
| psycopg2.IntegrityError | DB constraint (unique, FK) | "Duplicate or invalid record" |
Raw exceptions returned by Odoo are noisy and inconsistent. To display meaningful messages, you must extract the real error message using pattern matching.
Centralized Error Parser
class OdooErrorHandler {
static String parse(Object error) {
final msg = error.toString();
// 1. Network errors
if (msg.contains('SocketException') || msg.contains('Connection refused')) {
return 'Server is unreachable. Check your internet connection.';
}
// 2. Session errors
if (msg.contains('Session expired') || msg.contains('403')) {
return 'Session expired. Please log in again.';
}
// 3. Odoo Python exceptions
final strategies = [
RegExp(r'odoo\.exceptions\.[^:]+:\s*(.+?)(?:\n|$)', multiLine: true),
RegExp(r'Exception:\s*(.+?)(?:\n|$)', multiLine: true),
RegExp(r'ValueError:\s*(.+?)(?:\n|$)', multiLine: true),
];
for (final pattern in strategies) {
final match = pattern.firstMatch(msg);
if (match != null) {
return _clean(match.group(1)!);
}
}
return 'Unknown error: ${msg.split('\n').first}';
}
static String _clean(String s) {
return s.trim().replaceAll(RegExp(r'&#?\w+;'), '');
}
}Result:
- Developers still get full stack traces in logs
- Users only see clear, actionable messages
3. Handling "Database Discovery" Issues
Some Odoo deployments disable database listing using:
list_db = False
In this case, standard RPC database discovery fails, breaking login flows.
Strategy: Scrape the Database Name
If RPC discovery fails, extract the database name from cookies or HTML.
Future<String?> scrapeDatabase(String url) async {
try {
final resp = await http.get(Uri.parse('$url/web/login'));
// 1. Check cookies
final cookies = resp.headers['set-cookie'] ?? '';
if (cookies.contains('db=')) {
return RegExp(r'db=([^;]+)').firstMatch(cookies)?.group(1);
}
// 2. Fallback: parse HTML body for Odoo patterns
// See: OdooApiService._extractDbFromHtml
} catch (_) {}
return null;
}This technique is critical for hosted and security-hardened Odoo instances.
4. Advanced Technique: Dynamic Schema Healing
Odoo schemas evolve. Fields may exist in one version or module but not another.
The Problem
Your Flutter app requests a field that doesn’t exist:
ValueError: Invalid field 'x_mobile'
This crashes the request.
The Solution: Recursive Self-Healing Calls
Detect the offending field, remove it, and retry automatically.
Future<dynamic> safeCall(
String model,
String method,
List args,
Map kwargs,
) async {
try {
return await client.callKw(model, method, args, kwargs);
} catch (e) {
final err = e.toString();
final match = RegExp(r"Invalid field '([^']+)'" ).firstMatch(err);
if (match != null) {
final badField = match.group(1);
print(" Healing schema: Removing '$badField' and retrying...");
// 1. Remove from read/search fields
if (kwargs['fields'] is List) {
kwargs['fields'] = List.from(kwargs['fields'])..remove(badField);
return safeCall(model, method, args, kwargs);
}
// 2. Remove from create/write values
if (args.isNotEmpty && args.last is Map) {
final vals = Map.from(args.last);
if (vals.remove(badField) != null) {
args.last = vals;
return safeCall(model, method, args, kwargs);
}
}
// 3. Remove from the order clause
if (kwargs['order'] is String && kwargs['order'].contains(badField)) {
final orders = kwargs['order'].split(', ')
..removeWhere((o) => o.contains(badField));
kwargs['order'] = orders.join(', ');
return safeCall(model, method, args, kwargs);
}
}
rethrow; // Cannot heal further
}
}
This approach enables a single Flutter codebase to support multiple Odoo versions and module combinations.
Robustness beats visibility
- Never expose raw Odoo stack traces to users
- Parse and normalize errors centrally
- Scrape database names when discovery is disabled
- Heal schema mismatches dynamically
By handling Odoo constraints and server errors intelligently, your Mobo app becomes resilient, professional, and enterprise-ready—even in hostile or inconsistent backend environments.
To read more about How to Handle Timezones and Date Fields from Odoo for Mobo, refer to our blog How to Handle Timezones and Date Fields from Odoo for Mobo.