Errors
Validation rules, status codes, and the unified error envelope across all stock-analysis endpoints
Every endpoint uses the standard project envelope (internal/http/response.JSON) and the same response.Error* helpers as the rest of the API.
Error envelope
{
"message": "<human-friendly summary>"
}errors[] (the per-field validation array used elsewhere) is not populated by this module — query params are clamped/normalised rather than validated, so the only 400-class errors here carry plain message strings.
Content-Type: application/json always.
Status code map
| Code | Meaning |
|---|---|
| 200 | OK — including degenerate cases (available: false, tier4 missing, all rows upstream_missing) |
| 400 | Query-param error after a value couldn't be normalised (e.g. tab=foo, type=cashflowwwww) |
| 404 | Symbol not in nepse_companies. Always uppercased before lookup. |
| 429 | Rate limit exceeded (30 req/min/IP under the rl30 group) |
| 500 | DB error or programmer mistake. The recoverer middleware turns panics into 500. |
There is no 401/403 — the module is fully public.
Soft-failure philosophy
Where the API can produce a useful page despite missing/dirty input, it does. Silent normalisation is preferred over a 4xx that would block the entire response:
| Endpoint | Soft-failure rule |
|---|---|
peers | Unknown / cross-sector / duplicate symbols → excludedPeers[] instead of 4xx |
peers | Overflow past 4 peer symbols → silently dropped (over-specification isn't worth the error) |
announcements | Unknown chip in ?types= → silently dropped |
announcements | All-unknown ?types= → expands back to all 5 canonical types so the timeline still renders |
announcements | Unparseable ?from= / ?to= → treated as absent |
dividends | years, upcomingDays outside range → clamped to bounds |
statements / ratios | Empty ?type= / ?tab= → reverts to default (income/valuation) |
This means a fat-fingered URL in the FE almost never produces a 400 — it produces a "best effort" response with a hint surfaced via excludedPeers, note, or implicit defaulting.
Per-endpoint quick lookup
| Endpoint | 200 | 400 | 404 | 500 |
|---|---|---|---|---|
GET /snapshot | always (upstream_missing rows OK) | — | symbol unknown | DB |
GET /ownership | always (empty BOD/Phase-A overrides OK) | — | symbol unknown | DB |
| Endpoint | 200 | 400 | 404 | 500 |
|---|---|---|---|---|
GET /statements | always (available:false for cashflow is 200) | type or period outside whitelist | symbol unknown | DB |
GET /ratios | always | tab outside whitelist | symbol unknown | DB |
| Endpoint | 200 | 400 | 404 | 500 |
|---|---|---|---|---|
GET /dividends | always (empty rows OK) | — (params clamped) | symbol unknown | DB |
GET /upcoming-book-closes | always (empty rows OK) | — (params clamped) | — | DB |
| Endpoint | 200 | 400 | 404 | 500 |
|---|---|---|---|---|
GET /peers | always (cross-sector → excludedPeers) | — | focal symbol unknown | DB |
GET /announcements | always (unknown chips dropped) | — (params clamped) | symbol unknown | DB |
| Endpoint | 200 | 400 | 404 | 500 |
|---|---|---|---|---|
GET /fair-value | always (both methods may collapse to invalid) | — | symbol unknown | DB |
400 messages
These surface when a value can't be normalised back to a whitelist:
| Endpoint | Cause | Message |
|---|---|---|
statements | ?type not in income/balance/cashflow | `invalid type; expected income |
statements | ?period not in quarterly/annual | `invalid period; expected quarterly |
ratios | ?tab not in valuation/profitability/solvency/growth | `invalid tab; expected valuation |
Symbol path-param validation: an empty {symbol} returns 400 symbol is required. The chi route pattern rejects empty path segments before the handler runs in normal usage, but the explicit check guards against unusual middleware ordering.
404 messages
Every "symbol unknown" error uses the same message regardless of which endpoint surfaces it:
{ "message": "Company not found" }The 404 is raised the first time the handler hits Store.GetFundamentalsSnapshotCore (or GetFundamentalsOwnershipCore for ownership) and the row is missing. Several endpoints reuse the snapshot core query as a 404 probe before doing more expensive work — this is cheap (one row from fundamentals_snapshot) and saves a multi-query trip for non-existent symbols.
Examples
$ curl -i "$BASE/api/nepse/companies/NOTASYMBOL/fundamentals/snapshot"
HTTP/1.1 404 Not Found
{"message":"Company not found"}$ curl -i "$BASE/api/nepse/companies/NABIL/fundamentals/ratios?tab=foo"
HTTP/1.1 400 Bad Request
{"message":"invalid tab; expected valuation | profitability | solvency | growth"}# NABIL is a Commercial Bank; NTC is Telecommunications.
$ curl "$BASE/api/nepse/companies/NABIL/fundamentals/peers?symbols=NTC"
HTTP/1.1 200 OK
{
"data": {
"companies": [ { "symbol": "NABIL", "isFocal": true, … } ],
"excludedPeers": [ { "symbol": "NTC", "reason": "different_sector" } ],
…
}
}$ curl "$BASE/api/nepse/companies/NABIL/fundamentals/statements?type=cashflow"
HTTP/1.1 200 OK
{
"data": {
"type": "cashflow",
"available": false,
"reason": "Cash flow statements are not currently provided by upstream (NEPSE MDP). Will be enabled once available.",
"rows": [],
…
}
}Implementation
- Envelope:
internal/http/response/response.go - Validation helpers:
response.NewValidation(...),response.NewNotFound(...) - URL param case-fold:
handlerutil.URLParamUpper(r, "symbol") - Query int clamping:
fundhandlerutil.ClampedQueryInt(r, name, default, lo, hi)
The "soft normalisation over 4xx" policy is intentional. FE prototypes routinely send fat-fingered URLs while users explore filters; returning a useful page with a note or excludedPeers is better UX than a red error banner.