toga-ai 1.0.429 → 1.0.430
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
6
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, api2/_.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql |
|
|
7
7
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
8
|
+
| [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
|
|
8
9
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
9
10
|
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
|
|
10
11
|
| [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Health-check endpoint (/health liveness short-circuit)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-24
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Controller/Index.php
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- v2-api-error-codes.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
`_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200
|
|
21
|
+
**before** any routing, DB bootstrap, or V2 engine work runs. EB/LB (Elastic Beanstalk /
|
|
22
|
+
load balancer) probes depend on this always returning 200 cheaply — the architecture doc's
|
|
23
|
+
critical rule is literally "Don't break `/health`". This doc records the **matching contract**
|
|
24
|
+
so a teammate doesn't reintroduce a brittle exact-string check.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
The short-circuit lives at the top of `Index.php::api()`, before host dispatch and before the
|
|
29
|
+
Core/Logs DB bootstrap. It matches against a class constant:
|
|
30
|
+
|
|
31
|
+
```php
|
|
32
|
+
const HEALTH_CHECK_ROUTES = ['/health', '/v2/health'];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The incoming path is **normalized** before comparison — lowercased, query string stripped,
|
|
36
|
+
trailing slash removed — then matched strictly:
|
|
37
|
+
|
|
38
|
+
```php
|
|
39
|
+
$path = strtolower(rtrim(strtok($_SERVER['REQUEST_URI'] ?? '', '?'), '/'));
|
|
40
|
+
if (in_array($path, self::HEALTH_CHECK_ROUTES, true)) { /* return 200 */ }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This means **all** of these probe variants return 200 and never touch routing or the DB:
|
|
44
|
+
`/health`, `/v2/health`, `/health/` (trailing slash), `/health?x=1` (query string), and any
|
|
45
|
+
case variant. On match it returns 200 with an empty object.
|
|
46
|
+
|
|
47
|
+
## Why the normalization matters (the bug this fixed)
|
|
48
|
+
|
|
49
|
+
The original check was an exact match `$_SERVER['REQUEST_URI'] == '/health'`, so any variant
|
|
50
|
+
(`/v2/health`, trailing slash, query string) **fell through into the V2 routing engine**
|
|
51
|
+
instead of short-circuiting. There it hit auth/DB work and could surface as an **HTTP 500
|
|
52
|
+
(`EO-1`)** — a failing liveness probe. Health probes must degrade to a 200 short-circuit, not
|
|
53
|
+
enter routing. When adding a new probe path, add it to `HEALTH_CHECK_ROUTES` (already
|
|
54
|
+
normalized form: lowercase, no trailing slash, no query) rather than re-adding an exact-string
|
|
55
|
+
branch.
|
|
56
|
+
|
|
57
|
+
## Change history
|
|
58
|
+
- 2026-07-24 — Replaced the brittle exact-string `REQUEST_URI == '/health'` check with a
|
|
59
|
+
`HEALTH_CHECK_ROUTES` constant + normalized (lowercase / strip query / strip trailing slash)
|
|
60
|
+
strict `in_array` match, so `/v2/health`, `/health/`, and `/health?x` all short-circuit to 200
|
|
61
|
+
before routing/DB work instead of falling through to the V2 engine and returning 500 (EO-1).
|
|
62
|
+
Verified via live curl on all variants. (jcardinal)
|
|
63
|
+
</content>
|
|
64
|
+
</invoke>
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: [mhammontree, tcox]
|
|
9
|
+
updated: 2026-07-24
|
|
10
|
+
owners: [mhammontree, tcox, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/TrackingNumber.php
|
|
@@ -35,6 +35,7 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
35
35
|
| **EV-8** | Request field does not exist | A field sent in the payload isn't registered as a **`Core.RecordFields`** row for that record | Register the field in `Core.RecordFields` (see the "add a field to a V2 record" recipe in the ACL doc) |
|
|
36
36
|
| **EV-9** | No permission to write field | The field exists but the caller's role has no **`AclFieldPermissions`** grant (`isWritable=1`) in the CLIENT DB | Add the `AclFieldPermissions` row for the role (clone a writable sibling field's grant) |
|
|
37
37
|
| **EO-1** | Operation failed — identifier "There is no field called 'X' in the '_Model_Client_Y' model" | The DB column **and** `Core.RecordFields` exist, but the **generated model class** `_underscore/Model/Client/<Name>.php` doesn't declare the field. This is the **4th** requirement beyond the 3-file migration — and most often it's a **cross-repo git branch mismatch** (`_underscore` on a branch whose generated model lacks a field the DB/RecordFields already carry) | Declare the field in the generated model class (`public $field = self::FIELD_*`) and put all related repos (`_underscore`, `api2`, `dbchanges2`, `toga2-supply`) on the **same** feature branch — see the ACL doc's writable-field recipe |
|
|
38
|
+
| **EO-1** | Operation failed — **surfaced from a PHP warning/notice, not a real op error** (e.g. "Attempt to read property 'id' on bool", undefined variable) | A latent PHP warning escalates to a 500 because **Sentry's `ErrorHandler` in api2 promotes warnings/notices into thrown exceptions** (see diagnosis note 5). The known instance: in `V2.php::processRoutePairs()` an **unresolved route** leaves the local `$record = false`, and the post-processing payload interceptor layer then dereferenced `$record->id` → warning → 500 | Guard before dereferencing an unresolved record. The fix added a guard clause `if (!$record) return [$rawRequestedRouteName => $outData];` **before** the interceptor/logging layer (so a bad route returns a clean envelope, not a fatal), and initializes `$record = false;` at the top of the `foreach ($lookupByRouteNames ...)` loop so the invalid-HTTP-method / null-`$action` branch can't leave `$record` undefined (an undefined-variable warning would itself escalate to a 500) |
|
|
38
39
|
| **EZ-1** | Unauthorized record/script dispatch | Missing **`AclRecordScripts`** (scripted APIs) or the **`AclRecordPermissions`** four-table chain (records) for the caller's role | Grant `AclRecordScripts` (scripts) or complete the record-CRUD chain |
|
|
39
40
|
| **EZ-2** | Field-level authorization denied (READ) | The field is registered (`Core.RecordFields` present → no `EV-8`) but the caller's role has no **`AclFieldPermissions`** grant to **read** it. The read-side counterpart of `EV-9` (write) | Add the `AclFieldPermissions` row for the role (`isWritable=0` if the field is server-written). For a **custom** `c_` field use `AclCustomFieldPermissions` instead — see the ACL doc's standard-vs-custom table |
|
|
40
41
|
| **EV-5** | Duplicate `transactionId` | The globally-unique `transactionId` was reused | Send a fresh unique `transactionId` per request |
|
|
@@ -54,6 +55,15 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
54
55
|
3. **EV-6 vs EZ-1** are the two halves of exposing a scripted API: EV-6 = no `Core.RecordScripts`
|
|
55
56
|
route (the segment falls through to record lookup); EZ-1 = the route exists but there's no
|
|
56
57
|
`AclRecordScripts` dispatch grant for the caller's role.
|
|
58
|
+
5. **EO-1 can be a masked PHP warning, not an operation error.** Sentry's `ErrorHandler` in
|
|
59
|
+
api2 **escalates PHP warnings/notices into thrown exceptions**, so a latent "read property on
|
|
60
|
+
bool" or "undefined variable" surfaces to the client as an **HTTP 500 (EO-1)** rather than a
|
|
61
|
+
log line. Practical consequence: in `V2.php` you must guard against warnings as if they were
|
|
62
|
+
fatals — an unresolved route (or a parent FK-field path) leaves `$record = false`, and any
|
|
63
|
+
later `$record->id` deref becomes a 500. Return a clean envelope for unresolved records
|
|
64
|
+
**before** the interceptor/logging layer, and initialize loop-locals so no branch leaves a
|
|
65
|
+
variable undefined.
|
|
66
|
+
|
|
57
67
|
4. **EO-1 ("no field called 'X' in the model") is NOT a DB problem.** EV-8 means the Core
|
|
58
68
|
`RecordFields` registration is missing; EO-1 means the DB column and RecordFields are both
|
|
59
69
|
present but the **generated PHP model class** lacks the field. Before touching migrations,
|
|
@@ -71,6 +81,12 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
71
81
|
|
|
72
82
|
## Change history
|
|
73
83
|
|
|
84
|
+
- 2026-07-24 — Added a second **`EO-1`** case: a latent PHP **warning** (unresolved route →
|
|
85
|
+
`$record->id` on `bool` in `V2.php::processRoutePairs()`, or an undefined loop variable)
|
|
86
|
+
escalates to an HTTP 500 because api2's Sentry `ErrorHandler` promotes warnings/notices into
|
|
87
|
+
exceptions. Fix = guard clause returning a clean envelope for unresolved records before the
|
|
88
|
+
post-processing interceptor layer + initialize `$record = false` at the loop top. Added
|
|
89
|
+
diagnosis note 5 (warnings surface as EO-1 500s). (jcardinal)
|
|
74
90
|
- 2026-07-23 — TRUE-79533: added **`EZ-2`** (field-level READ authorization denied) — the read-side
|
|
75
91
|
counterpart of `EV-9`; a registered field (no `EV-8`) with no `AclFieldPermissions` read grant.
|
|
76
92
|
Clarified EV-8/EV-9/EZ-2 as three states of one field and the standard-vs-custom grant-table
|
package/package.json
CHANGED