Error Responses
Standard error response formats across ZoneVast services. How to parse and handle errors from Django DRF and NestJS backends.
Error Responses
ZoneVast services return errors in different formats depending on the backend framework. This page documents both formats and how to handle them consistently.
HTTP Status Codes
All services use standard HTTP status codes:
|| Status | Meaning | Typical Cause |
||--------|---------|---------------|
|| 400 | Bad Request | Invalid input, validation failure |
|| 401 | Unauthorized | Missing or invalid/expired token |
|| 403 | Forbidden | Valid token but insufficient permissions or wrong project |
|| 404 | Not Found | Resource does not exist |
|| 409 | Conflict | Duplicate resource (e.g., already registered phone) |
|| 429 | Too Many Requests | Rate limit exceeded |
|| 500 | Internal Server Error | Backend issue |
Django DRF Error Format
Django services (zv-auth-service, zv-project-service) use DRF's built-in error handling.
Simple Error
{
"detail": "Human-readable error message",
"code": "error_code"
}
Example:
{
"detail": "Given token not valid for any token type",
"code": "token_not_valid"
}
Validation Errors (Field-Level)
When input validation fails, DRF returns errors keyed by field name:
{
"email": ["User with this email already exists."],
"phone": ["This field is required."]
}
Non-Field Errors
Errors not tied to a specific field appear under non_field_errors or detail:
{
"non_field_errors": ["Unable to log in with provided credentials."]
}
Token Validation Error
{
"detail": "Given token not valid for any token type",
"code": "token_not_valid",
"messages": [
{
"token_class": "AccessToken",
"token_type": "access",
"message": "Token is invalid or expired"
}
]
}
NestJS Error Format
NestJS services (zv-flex-auth-service) use a global exception filter that normalizes errors.
Standard Error
{
"statusCode": 400,
"message": "Error description",
"timestamp": "2026-04-27T12:00:00.000Z"
}
Validation Error
When class-validator validation fails, messages are joined:
{
"statusCode": 400,
"message": "phone must be 7-15 digits, password should not be empty",
"timestamp": "2026-04-27T12:00:00.000Z"
}
Note: The NestJS
ValidationPipeuseswhitelist: trueandforbidNonWhitelisted: true. This means unknown fields in your request body will cause a 400 error rather than being silently ignored.
Not Found
{
"statusCode": 404,
"message": "User not found",
"timestamp": "2026-04-27T12:00:00.000Z"
}
Authentication Error
{
"statusCode": 401,
"message": "Unauthorized",
"timestamp": "2026-04-27T12:00:00.000Z"
}
Comparison
|| Aspect | Django DRF | NestJS |
||--------|-----------|--------|
|| Error detail key | detail | message |
|| Error code key | code | statusCode |
|| Field errors | { "field": ["msg"] } | Joined in message |
|| Timestamp | Not included | Always included |
|| Validation | Per-field arrays | Joined string |
Universal Error Handler
interface ApiError {
// Django format
detail?: string;
code?: string;
messages?: Array<{
token_class: string;
token_type: string;
message: string;
}>;
[field: string]: string[] | unknown; // field-level errors
// NestJS format
statusCode?: number;
message?: string;
timestamp?: string;
}
function parseError(error: ApiError): {
status: number;
message: string;
fields?: Record<string, string[]>;
} {
// NestJS format
if (error.statusCode) {
return {
status: error.statusCode,
message: error.message || 'Unknown error',
};
}
// Django format — field-level errors
const fields: Record<string, string[]> = {};
let message = error.detail || 'Unknown error';
for (const [key, value] of Object.entries(error)) {
if (key === 'detail' || key === 'code') continue;
if (Array.isArray(value)) {
fields[key] = value as string[];
}
}
return {
status: 0, // set from HTTP response
message,
fields: Object.keys(fields).length > 0 ? fields : undefined,
};
}
Common Error Scenarios
401 — Token Expired
// Django
{ "detail": "Given token not valid for any token type", "code": "token_not_valid" }
// NestJS
{ "statusCode": 401, "message": "Unauthorized", "timestamp": "..." }
Fix: Use the refresh token to get a new access token. See Headers & Authentication.
403 — Wrong Project
{ "detail": "You do not have permission to perform this action." }
Fix: Ensure X-Project-ID matches the project in your JWT token.
400 — Validation Failure
// Django — per-field
{ "phone": ["This field is required."], "password": ["Ensure this field has at least 6 characters."] }
// NestJS — joined
{ "statusCode": 400, "message": "phone must not be empty, password must be longer than 6 characters", "timestamp": "..." }
Fix: Check the field requirements in the service documentation.
409 — Conflict (Duplicate)
{ "detail": "A user with this phone number already exists." }
Fix: Use a different value or try logging in instead of registering.
What's Next
- Error Codes & Troubleshooting — Common error codes and debugging
- Rate Limiting — 429 error specifics
- Headers & Authentication — Token handling