API V3: Implement Structured Error Handling and Validation Tests

- Updated api_get_object and api_post_object to extract rich metadata (meta.details) from 400/500 responses.
- Enables frontend to bubble up specific DB schema, validation, and constraint errors for better debugging.
- Added 'V3 Hardening' section to /testing dashboard with automated tests for Permissive Mode and Structured Error extraction.
This commit is contained in:
Scott Idem
2026-01-19 17:38:05 -05:00
parent 8566917be1
commit c40a296a77
3 changed files with 96 additions and 8 deletions

View File

@@ -184,7 +184,21 @@ export const get_object = async function get_object({
return false;
}
console.log('The response was not ok. Throwing an error!');
// Structured Error Handling (V3): Attempt to get rich error metadata
let error_json: any = null;
try {
error_json = await response.json();
} catch (e) {
// Not JSON
}
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
if (error_json?.meta?.details) {
// Return the rich error object so caller can handle specific categories (database_schema, etc)
return error_json;
}
throw new Error(`HTTP error! status: ${response.status}`);
}

View File

@@ -164,19 +164,34 @@ export const post_object = async function post_object({
if (!response.ok) {
if (response.status === 404) {
console.warn('404 Not Found. Returning null.');
if (log_lvl) {
console.log('The response was a 404 not found "error". Returning null.');
}
return null;
}
const errorBody = await response.text();
console.error(`HTTP error! status: ${response.status}`, errorBody);
// Don't retry on client errors (400, 401, 403)
if (response.status >= 400 && response.status < 404) {
// FAIL FAST (Section 2D): Do not retry on Auth/Permission failures
if (response.status === 401 || response.status === 403) {
console.error(`API Auth Failure (${response.status}). Failing fast as per Section 2D protocol.`);
return false;
}
throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
// Structured Error Handling (V3): Attempt to get rich error metadata
let error_json: any = null;
try {
error_json = await response.json();
} catch (e) {
// Not JSON
}
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
if (error_json?.meta?.details) {
// Return the rich error object so caller can handle specific categories (database_schema, etc)
return error_json;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!return_blob) {

View File

@@ -176,6 +176,53 @@
return await response.json();
});
// V3 Schema & Error Validation
const test_permissive_mode = () => run_test('Permissive Mode Test', async () => {
const endpoint = `/v3/crud/account/${$ae_loc.account_id || 'ghost'}`;
const data = {
name: $ae_loc.account_name,
_non_existent_field: 'This should be ignored by the API'
};
// Use post_object for a patch-like operation if needed, or update_ae_obj_v3 style
// For testing purposes, we'll use a standard fetch to see raw response
const url = new URL(endpoint, $ae_api.base_url);
const headers = {
...$ae_api.headers,
'x-account-id': $ae_loc.account_id || 'ghost',
'Authorization': `Bearer ${$ae_loc.jwt}`
};
const response = await fetch(url.toString(), {
method: 'PATCH',
headers,
body: JSON.stringify(data)
});
const result = await response.json();
return { status: response.status, result };
});
const test_structured_error = () => run_test('Structured Error Validation (Deliberate 400)', async () => {
const endpoint = `/v3/crud/account/${$ae_loc.account_id || 'ghost'}`;
// To trigger a 400 error despite permissive mode, we'll try to set a read-only field or invalid type if possible
// Actually, the easiest way to test structured error is to send a malformed filter to /search
const url = new URL(`${endpoint.split('/account')[0]}/account/search`, $ae_api.base_url);
const headers = {
...$ae_api.headers,
'x-account-id': $ae_loc.account_id || 'ghost',
'Authorization': `Bearer ${$ae_loc.jwt}`
};
const response = await fetch(url.toString(), {
method: 'POST',
headers,
body: JSON.stringify({ and: [{ field: "non_existent", op: "eq", value: "fail" }] })
});
const result = await response.json();
return { status: response.status, structured_details: result.meta?.details || 'MISSING' };
});
// Environment Diagnostics
let is_native = $derived(typeof window !== 'undefined' && !!(window as any).native_app);
let app_mode = $derived($events_loc?.launcher?.app_mode || 'web');
@@ -443,6 +490,18 @@
<Building2 size={16}/> Load Accounts
</button>
</div>
<div class="card p-6 space-y-4 border border-gray-500 shadow-lg transition-all group hover:bg-surface-500/5">
<header class="flex items-center gap-2 text-warning-500 border-b border-gray-500 pb-2">
<ShieldAlert size={20}/>
<h4 class="h4 font-bold uppercase tracking-widest">V3 Hardening</h4>
</header>
<button class="btn variant-filled-warning p-4 w-full shadow-md transition-all hover:scale-[1.02] flex items-center justify-center gap-2" disabled={!$ae_loc.jwt} onclick={test_permissive_mode} title="Verifies the API ignores unknown fields (x-ae-ignore-extra-fields).">
<Zap size={16}/> Permissive Mode Test
</button>
<button class="btn variant-filled-error p-4 w-full shadow-md transition-all hover:scale-[1.02] flex items-center justify-center gap-2" disabled={!$ae_loc.jwt} onclick={test_structured_error} title="Deliberately triggers a 400 error to verify rich metadata extraction.">
<ShieldAlert size={16}/> Structured Error Test
</button>
</div>
</div>
<!-- Maintenance -->