Skip to content

Standardize server responses to use success/failure functions - #1688

Open
CarlyAThomas wants to merge 20 commits into
OpenEnergyDashboard:developmentfrom
CarlyAThomas:issue1614
Open

Standardize server responses to use success/failure functions#1688
CarlyAThomas wants to merge 20 commits into
OpenEnergyDashboard:developmentfrom
CarlyAThomas:issue1614

Conversation

@CarlyAThomas

Copy link
Copy Markdown

Description

This standardizes how every server route responds, moving all 21 route files (63 endpoints) onto response.js's success()/failure() functions instead of the mix of hand-rolled res.* calls that had grown up across the codebase. The issue laid out four specific things to address, plus a follow-up question about the CSV pipeline and obvius.js, so I've gone through each of those in order below.

1. "The logging on failure should happen here rather than in each location where the error occurs... This means passing the sanitized message to this function and the error to log to the DB."

failure() now takes the raw error as its own parameter, separate from an optional safeMessage meant for the client. The raw error is only ever logged internally; it is never sent to the client under any circumstance, so a route can no longer accidentally leak a DB error or a stack trace just by passing the wrong thing into a response call. Routes no longer call log.warn()/log.error()/etc. themselves — that all happens inside failure() now.

2. "Add a parameter to disable logging with the default of false. That means logs are created unless the developer feels there is a good reason not to do that."

I went slightly beyond the literal ask here, with @huss's approval on the issue. Instead of a plain on/off boolean, failure() takes a LogLevel severity (DEBUG/INFO/WARN/ERROR/SILENT), so a route can keep its original log priority instead of every failure collapsing to one level. This turned out to matter more than I expected once I started migrating files: log.js's emailLevel only e-mails admins for ERROR-priority events, so if every failure had collapsed to a single severity, routine WARN-level validation failures would have started paging admins for things that were never meant to reach them. Logging is still on by default (LogLevel.ERROR); a route has to explicitly opt out with LogLevel.SILENT if it has good reason not to log something, which satisfies the "logs are created unless the developer feels there is a good reason not to" part of the ask.

3. "All routes should be checked so logging info is sent on for failures and not done in the route or any other uses."

I went through all 63 endpoints individually, by hand, rather than trusting a mechanical find-and-replace pass. That review is what turned up most of the real bugs described below — hang bugs where a route logged but never actually responded, catch blocks that responded but bypassed the shared functions entirely, and about 40 call sites where an earlier, more mechanical migration pass had silently dropped the descriptive part of the original log message. All of that is fixed and restored now, endpoint by endpoint.

4. "All routes should be changed to use the modified functions and stop sending back responses directly... Care should be taken not to send back internal error information but only have it logged."

Every one of the 21 route files (63 endpoints) has been migrated onto success()/failure(), with two deliberate, discussed exceptions explained under Limitations below. The client-facing sanitization is structural, not something each route has to remember to do correctly: failure() always substitutes a generic message for any 500-level response regardless of what's passed in, so internal detail can only ever reach the admin-visible logs, never the client.

On the CSV pipeline and obvius.js: both are addressed under Limitations below, since the outcome for each ended up being "leave it alone, for a specific reason" rather than a straightforward migration.

Fixes #1614

Real bugs found and fixed along the way

Going through every endpoint by hand (not just grepping for patterns) turned up a handful of real, pre-existing bugs:

  • Multiple "hang bugs": catch blocks that only logged and never sent any response at all, leaving the client's request pending forever. Found in groups.js (5 endpoints: GET /, /idname, /deep/groups, /children/:group_id, /allChildren/), conversions.js's GET /, meters.js's GET /, users.js's GET /, maps.js's GET /, preferences.js's GET /, and ciks.js's GET / — all the same shape of bug. maps.js's /edit validation branch had a different flavor of the same problem: res.status(400) called with no .send()/.json()/.end(), which sets the status code but never completes the response.
  • baseline.js had a completely broken logger: const log = require('../log') imports the log module (an object), but the file called it directly as log(message, 'error') — a TypeError on every single failure, meaning neither of this file's two endpoints ever actually logged anything, they just crashed silently. Fixed as a side effect of routing through failure().
  • Several "centralized in name only" bugs: catch blocks that did log and respond, but via raw res.sendStatus()/res.send() calls that bypassed the shared response functions entirely, meaning any internal error detail passed to them went straight to the client unsanitized. Examples: groups.js's POST /delete, meters.js's GET /:meter_id, users.js's GET /:user_id, and maps.js's GET /:map_id all logged the error correctly but then responded with a raw res.sendStatus(500) instead of going through failure().
  • A double-response bug in conversions.js's /edit: success(res) was called unconditionally after the try/catch, so it fired a second time even after failure() had already responded inside a caught error.
  • Two un-awaited Promise.all(...).then(...) chains in groups.js (GET / and GET /deep/groups): neither awaited nor given a .catch(), so an error inside either callback fired after the enclosing try/catch had already exited — an unhandled rejection with nowhere to land, independent of the Express-4 async-forwarding gap below.
  • Restored ~40 call sites' worth of lost log context: the initial mechanical migration pass replaced log.error("descriptive text: " + err, err)-style calls with bare failure(res, code, err), which loses everything except err.message. Every catch block across all 21 files was reviewed individually and had its original descriptive context restored via new Error(message, { cause: err }), which also required a small log.js fix so the console/file output continues to show the original error's stack trace (via .cause), not just the wrapper's — this only affects console/file output, never what gets persisted to the DB (which only ever stores message).
  • Express 4 doesn't forward rejected async route handler promises to the global error handler on its own (Express 5 does this natively) — added express-async-errors as a standalone, one-line fix. This is temporary; tracked for removal in Upgrade Express from v4 to v5 #1676 (the Express 5 migration, split out of this issue's scope at @huss's request) via a TODO comment already in app.js pointing at that issue.

A few smaller things fixed in passing

While I was already touching these exact lines:

  • units.js's /addUnit validation log said "edit units" instead of "add units" (copy-paste from the edit endpoint).
  • maps.js's /delete catch block said "Error while deleting group" instead of "map" (copy-paste from groups.js).
  • conversions.js's /simulate-delete client-facing validation message said "delete conversions" instead of "simulate deletion of conversions" (copy-paste from /delete, right above it).
  • meters.js had a TODO left by the original authors: /edit and /addMeter's success paths called res.json(...) directly instead of success(), with a comment saying they weren't sure success() could return values. I checked — res.send() JSON-encodes objects identically to res.json(), so it works fine — and converted both. Once I'd confirmed that pattern was safe, I applied it consistently anywhere else a route's success path was bypassing success() for the same reason (preferences.js, logs.js, baseline.js, ciks.js, compareReadings.js, unitReadings.js, conversionArray.js).

Test coverage added

responseParamsTest.js already existed and covered success()/failure()'s response-sending mechanics (status codes, comment handling, edge cases), but every one of those tests passed error = null, so failure()'s actual logging behavior — the core of what this issue asked for — was never exercised. Added 5 tests, using a sinon stub on log.log() so nothing actually hits the real DB/e-mail during the test:

  • Logs at the given severity — passing LogLevel.WARN actually results in log.log() being called with WARN, the error's message, and the error object itself.
  • Defaults to LogLevel.ERROR when no severity is passed — this is the "logs are created unless the developer opts out" behavior from the issue.
  • LogLevel.SILENT suppresses logging entirely — even with a real error present, log.log() is never called.
  • No error, no log — if error is null/falsy, nothing gets logged regardless of severity.
  • 500-level responses never leak the raw error to the client — constructs an Error with a fake secret in its message, sends it through failure() at a 500 code, and asserts the response body is only ever the generic message, never containing the secret — while confirming the error was still logged internally. This is the core safety guarantee of the whole redesign, and previously nothing verified it.

I also considered adding tests characterizing the "phantom success" delete bug noted above (units/conversions/users), but held off — whether deleting a nonexistent id should return 200 or something else is an API-design decision, not something to lock in via a test as a side effect of this PR. Flagging it here rather than guessing.

Catching this branch up with development

This took long enough that development moved a lot underneath it — 106 commits, 48 files, including the new session-invalidation/auth work (login.js became loginLogout.js) and changes overlapping 14 of the 18 route files this PR touches. I rebased onto current development and went through every conflict by hand rather than taking either side wholesale, so both upstream's new validation logic/features and this issue's response-standardization work survive together. Full test suite passes after the rebase — 942 passing, 1 pending (down from 4 pending before the rebase, since upstream's own date-validation fix let 3 previously-skipped compareReadingsParamsTest.js tests get re-enabled).

Type of change

  • Note merging this changes the database configuration.
  • This change requires a documentation update (left unchecked since none of the existing developer/admin docs are part of this repo to update directly — see the note under Limitations)

Checklist

  • I have followed the OED pull request ideas
  • I have removed text in ( ) from the issue request
  • You acknowledge that every person contributing to this work has signed the OED Contributing License Agreement and each author is listed in the Description section.

Author: @CarlyAThomas

Limitations

Two files stay outside the response.js consolidation, both discussed and agreed with @huss on the issue:

csvPipeline/success.js and csvPipeline/failure.js are left as-is. They were originally in scope, but PR #1591 (an in-flight, unmerged XSS fix using DOMPurify) is independently modifying the same two files for a different reason, and consolidating them now would conflict with that work and undermine its fix. Fusing them into one shared function with a mode parameter is still a real possibility, just as a separate follow-up once #1591 lands.

obvius.js keeps its own separate success/failure implementation — different signature, a hardware-specific 406 status, and a plaintext body format that shouldn't change without risking ~7-8 year old field hardware we can't test against. The only change there is renaming the local functions to successObvius/failureObvius, so the distinction from response.js's functions is visible at every call site instead of only in a comment.

A few pre-existing bugs and gaps turned up while reviewing every endpoint. These are input-validation/data-shaping issues, not response-sanitization ones, so I left them alone as out of scope for this issue, but wanted them on record:

  • groups.js's GET /children/:group_id has no input validation on :group_id, unlike its siblings, so malformed input reaches the database directly instead of being rejected cleanly.
  • readings.js's GET /line/count/meters/:meter_ids has no numeric pattern on meter_ids (its sibling endpoint and compareReadings.js's equivalent both do), so non-numeric input reaches the DB as NaN and turns into an ERROR-level, admin-emailed log entry instead of a clean 400.
  • The same "phantom success" pattern shows up three times: POST /api/units/delete, POST /api/conversions/delete, and POST /api/users/delete all run a DELETE ... WHERE with no RETURNING clause through conn.none(), which only throws if rows are returned, not based on how many were affected — so deleting something that doesn't exist still comes back as a 200 success. Confirmed live for all three.
  • GET /api/users/:user_id sends the raw DB row back to the client, including the bcrypt passwordHash field. Nothing this PR changes, but probably worth a look — there's no real reason an admin caller needs the hash itself.
  • Some 400s leak raw jsonschema validator text (like instance.group_id does not match pattern "^\d+$") to the client — that's the library's own internal formatting, not something OED wrote on purpose, though it's not a server-internals leak either (nothing about DB structure or file paths). Left the wording exactly as it was, same as everywhere else in this PR.

Two of the route test files have a pre-existing bug that leaves them testing nothing. Both baselineParamsTest.js and conversionArrayParamsTest.js (from PR #1528) hit the wrong URL — /api/baseline and /api/conversionArray/refresh instead of the actual mounted paths, /api/baselines and /api/conversion-array. Every request in both files silently lands on the SPA's index.html catch-all instead of the real route, so ~30 tests per file are commented out with a note saying the route "isn't properly mounted" — it is, the URLs just have a typo. I verified both endpoints thoroughly by hand with curl instead. I haven't checked whether other files from #1528 have the same issue; the ones I actually ran this session all passed with real, meaningful counts.

Two flaky test failures showed up while I was running the full suite, and I chased both down before assuming they were mine. csvParamsTest.js's "should handle multiple file upload attempts" fails intermittently with an EPIPE (multer doesn't drain the request stream after a file-count error — nothing in csv.js/csvPipeline is touched by this branch at all). logsRouteTests.js's "should handle date range filtering" inserts a log, waits 1s, records a cutoff timestamp, waits another 1s, inserts a second log, then queries by date range — but it assumes each POST /api/logs/info has finished writing to the DB by the time it responds 200. It hasn't: log.js's Logger.log() fires its DB insert in an un-awaited async IIFE, so the HTTP response can return before the row is actually committed. That's invisible in an isolated run (the insert finishes in milliseconds either way), but under the full suite's DB connection-pool contention from hundreds of concurrent tests, that gap widens enough to occasionally break the test's own timing assumptions. Neither is something this branch touches or introduces. I reproduced both failing on the unmodified base commit on a repeat run, and both pass cleanly every time in isolation on either branch — they're pre-existing flakiness under load, not something this branch causes.

Heads up on overlap with #1666. @aduques is working on #1666 (making client-facing error messages show the specific validation details instead of generic text), and their investigation found the same users.js/groups.js raw-response issues this PR fixes. The mechanism change (routing through success()/failure(), consistent response shape) is what I did here; I deliberately left every endpoint's original message wording untouched, so their work on message content — like replacing users.js's generic "Invalid params" with the specific validation errors — is completely separate and still fully needed. @huss already weighed in on the issue that it's fine for both to proceed in parallel.

No documentation currently covers any of this, and I think it should exist. I checked the developer docs (they live in a separate repo, OpenEnergyDashboard.github.io, not this one) for anything on error handling, logging, log levels, or admin e-mail notifications. The closest existing page just says where log output ends up (log.txt, nohup.out) — nothing about how success()/failure() work, what LogLevel controls, or when an admin actually gets e-mailed. Since this affects how every future route is written, what the client sees on failure, and when admins get paged, I think it's worth a dedicated page rather than something I fold into this PR. I'd like to raise this with @huss directly — either as a documentation proposal in this PR thread, or as its own follow-up question — rather than guess at what should go where.

CarlyAThomas and others added 20 commits July 30, 2026 01:12
…the global error handler

Express 4 does not do this natively (Express 5 does); this closes that
gap as a standalone fix ahead of the Express 5 migration tracked in OpenEnergyDashboard#1676.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
failure() call sites now pass new Error(message, { cause: originalErr })
to preserve descriptive context in the DB-persisted log message. This
surfaces the original error's stack in console/file output too, not just
the wrapper's -- console/file only, the DB-persisted message is
unaffected either way.
obvius.js intentionally keeps its own separate implementation (different
signature, hardware-specific 406 status and plaintext format that can't
be safely changed) rather than being merged into response.js, per
maintainer discussion on OpenEnergyDashboard#1614. Renaming makes that distinction visible
at every call site instead of only in a comment.
…t-facing messages

failure() now takes the raw error (log-only, never sent to the client)
separately from an optional safe message (client-facing, only used under
500). Replaces the boolean skipLog parameter with a LogLevel severity
(DEBUG/INFO/WARN/ERROR/SILENT) so routes can preserve their original
logging severity instead of collapsing to ERROR -- log.js only e-mails
admins for ERROR-priority events, so this avoids turning routine
WARN-level validation failures into admin spam. Updates
responseParamsTest.js for the new signature.
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.* calls, fixing several real bugs found
along the way: multiple catch blocks that only logged and never
responded (hung the request forever), two un-awaited
Promise.all().then() chains that let errors escape their enclosing
try/catch, and branches that bypassed response.js's sanitization
guarantee entirely via raw res.send()/res.sendStatus(). Preserves the
original descriptive log context via new Error(message, { cause: err })
so admin-visible logs stay as informative as before.
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.*/log.* calls. Preserves each original
descriptive log message via new Error(message, { cause: err }), and
restores the two validation branches (edit, delete) that originally logged
at LogLevel.WARN back to that severity instead of silently escalating to
the new default of LogLevel.ERROR -- addUnit's validation branch was
already ERROR in the original, so it needed no severity change. Also
fixes a pre-existing copy-paste bug where addUnit's log message said "end units" instead of "add units".
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.*/log.* calls. Preserves each original
descriptive log message via new Error(message, { cause: err }), and
restores the two validation branches (edit, simulate-delete) that
originally logged at LogLevel.WARN back to that severity instead of
silently escalating to the new default of LogLevel.ERROR.

Fixes several real bugs found along the way: GET /'s catch block only
logged and never responded (a hang bug, same family as groups.js's);
addConversion's and simulate-delete's success paths bypassed
success()/response.js entirely via raw res.sendStatus()/res.json(); and
edit had a double-response bug where success(res) fired unconditionally
after the try/catch, even when failure() had already responded inside the
catch block.

Also fixes a pre-existing wording bug where simulate-delete's
client-facing validation message incorrectly said "delete conversions"
(copied from the /delete endpoint above it) instead of "simulate deletion
of conversions".
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.*/log.* calls. Preserves each original
descriptive log message via new Error(message, { cause: err }), including
restoring err['detail'] into both the log and (implicitly, via generic
500 text) the client-visible failure on /edit and /addMeter -- same
pattern as groups.js's POST /create.

Restores the two validation branches (edit, addMeter) that originally
logged at LogLevel.WARN back to that severity instead of silently
escalating to the new default of LogLevel.ERROR.

Fixes a real hang bug: GET /'s catch block only logged and never
responded, same family as groups.js's/conversions.js's GET /. GET
/:meter_id's catch also previously bypassed failure() via raw
res.sendStatus(500) despite already logging.

Completes a pre-existing TODO on /edit and /addMeter: both success paths
called res.json(...) directly instead of success(), per the original
authors' own comment explaining success() couldn't return values --
confirmed via testing that success()'s res.send() JSON-encodes objects
identically to res.json(), so both are now converted, removing the
now-unused success import in the process.

GET /:meter_id's two 400 branches (malformed meter_id, and "meter exists
but isn't displayable") are deliberately indistinguishable from each
other per the original comment; kept them symmetric with no safeMessage
(empty body) rather than reproducing res.sendStatus(400)'s incidental
"Bad Request" text, since the latter was never a deliberate message.
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.*/log.* calls, preserving each endpoint's
original response body shape exactly (both plain-string and
{message: ...}-object forms) since the frontend depends on that shape.
Restores descriptive log context via new Error(message, { cause: err })
on all 5 catch blocks that had it (GET /, GET /:user_id, POST /create,
POST /edit, POST /delete).

Most of this file's non-500 branches (GET /token's 4 response points,
and every endpoint's basic validation branch) never logged anything in
the original code -- the migration preserves that by passing a null
error or LogLevel.SILENT where appropriate, rather than defaulting to
logging everything.

Fixes a real hang bug: GET /'s catch block only logged and never
responded at all. GET /:user_id's catch also previously bypassed
failure() via raw res.sendStatus(500) despite already logging.

The admin-lockout-prevention branch on POST /edit was already a
deliberate log.error() in the original (not a WARN mismatch like other
files) and is preserved at that severity.
Routes both success and failure paths through the shared
success()/failure() functions instead of raw res.*/log.* calls,
preserving the original {token, username, role}/{text: ...} response
body shapes exactly. Restores the "Unable to check user password for
<username>" log context (including the username, which was useful
context in the original) on the genuine-error catch branch, lost in the
mechanical migration.

The bad-credentials branch (wrong password/nonexistent user) never
logged in the original either, preserved via LogLevel.SILENT.
Routes every success/failure path through the shared success()/failure()
functions instead of raw res.*/log.* calls. Preserves each original
descriptive log message via new Error(message, { cause: err }) on the 4
genuine-error catch blocks (GET /, GET /:map_id, POST /create, POST
/edit), and fixes a copy-paste bug where POST /delete's log message said
"Error while deleting group" instead of "map".

Fixes two hang bugs: GET /'s catch block only logged and never
responded, same family as other files' GET /. POST /edit's validation
branch called res.status(HTTP_CODES.BAD_REQUEST) with no .send()/.json()/
.end() -- that sets the status code but never actually completes the
response, so the request hung indefinitely on invalid edit data. Both
now respond correctly since failure() always calls res.send() internally.

Validation branches on /create and /edit pass their descriptive message
as failure()'s error param only (not safeMessage), matching each
endpoint's original behavior of only ever logging these details, never
showing them to the client -- consistent with the empty-body-over-
sendStatus's-incidental-text precedent from meters.js/users.js/login.js.
Routes the validation-failure, JWT-verify-failure, and success paths
through failure()/success() instead of raw res.*/log.* calls, preserving
the original response body shapes exactly. Neither non-success branch
logged in the original code -- preserved via omitting the error argument
(validation branch) and LogLevel.SILENT (JWT-verify branch).
Routes success/failure paths through the shared success()/failure()
functions instead of raw res.*/log.* calls. Converts both success paths
from res.json(rows) to success(res, rows), completing the pattern (rows
is always a single object here via Preferences.mapRow(), so res.send()
JSON-encodes it identically to res.json()). Restores descriptive log
context via new Error(message, { cause: err }) on both catch blocks.

Fixes a real hang bug: GET /'s catch block only logged and never
responded at all, same family as other files' GET /.
Routes success/failure paths through the shared success()/failure()
functions instead of raw res.*/log.* calls. The client-logging endpoints
(POST /info, /warn, /error) keep their level-specific log.info()/
log.warn()/log.error() calls on the success path untouched, since
recording the client's message at the requested level is the actual
purpose of those routes, not something to route through failure().

Their validation-failure branches (and GET /logsmsg's) pass the
descriptive message as failure()'s error param only, matching the
maps.js/logs-family precedent: logs at the correct default ERROR
severity, client gets an empty body instead of raw sendStatus(400)'s
incidental "Bad Request" text.

Restores descriptive log context on GET /logsmsg's catch block via
new Error(message, { cause: err }), and converts its success path from
res.json(rows) to success(res, rows) -- rows is an array here, which
Express's res.send() JSON-encodes identically to res.json().
Routes success/failure paths through the shared success()/failure()
functions instead of a broken standalone log() call. baseline.js was
importing ../log as const log = require('../log') (the whole module
object, not a callable) and then calling log(message, 'error') directly
-- that throws TypeError every time either catch block runs, meaning
neither ever actually logged anything before this change; this was the
only file in the codebase with that pattern. Converting to failure()
removes the broken calls and fixes the bug as a side effect, restoring
real admin-visible error logging for the first time.

Also converts GET /'s success path from res.json(rawBaselines) to
success(res, rawBaselines) (an array, which Express's res.send()
JSON-encodes identically to res.json()), and restores descriptive log
context via new Error(message, { cause: err }) on both catch blocks.

Confirmed live that baselineParamsTest.js has no real coverage of this
file due to a pre-existing URL typo (tests hit /api/baseline, singular;
the route is mounted at /api/baselines) -- unrelated to this change,
documented in OED NOTES.md, flagged as a possible follow-up issue.
Routes the success/failure path through the shared success()/failure()
functions instead of a broken log.error()-only catch block that never
sent a response at all -- the same hang-bug family found in every other
file's GET /. Converts the success path from res.json(...) to
success(res, ...) (an array, JSON-encoded identically either way), and
restores descriptive log context via new Error(message, { cause: err }).

Confirmed live via a genuine database outage (stopped the database
container): the client gets a real 500 instead of hanging, and the log
shows the fully restored context plus the original DNS-resolution
error's stack via the "Caused by:" chain.
This file originally had no try/catch at all -- both endpoints relied
entirely on the global error handler (and, before this branch, on
nothing, since express-async-errors didn't exist yet either). Real
try/catch blocks wrapping the data-fetch were already added earlier in
this branch's work; this commit adds descriptive context to those catch
blocks' failure() calls via new Error(message, { cause: err }) for
consistency with every other file's catch blocks, distinguishing the
meter and group endpoints' errors from each other.

Converts both success paths from res.json(...) to success(res, ...)
(both return plain objects keyed by meter/group id, JSON-encoded
identically either way).

Confirmed live via the known oversized-meter_id repro from earlier in
this branch's work (meter_compare_readings_unit(...) does not exist):
the client still gets the generic sanitized 500, and the log now shows
the full restored context instead of the bare error.
Routes success/failure paths through the shared success()/failure()
functions instead of raw res.*/log.* calls. Restores descriptive log
context on both outer catch blocks via new Error(message, { cause: err
}), and restores the WARN severity (with cause preservation) on both
endpoints' timeInterval-parsing failure branches, which the mechanical
migration had collapsed to the new default of ERROR.

Left the two nosemgrep-suppressed res.send() calls on the success paths
untouched, since converting them to success() would orphan the
suppression comments from the exact line they're anchored to.

Found and documented in OED NOTES.md (not fixed here, out of OpenEnergyDashboard#1614's
scope as an input-validation issue): GET /line/count/meters/:meter_ids'
meter_ids schema has no numeric pattern, letting non-numeric input reach
the DB as NaN and escalate to an ERROR-level admin-emailed log entry
instead of a clean 400. Also documented: GET /line/raw/meter/:meter_id's
identical class of bug (a type/string mismatch making the endpoint
permanently return 400) was already found and fixed upstream via issue
noted as a real reconciliation point for the eventual development sync,
since that PR's changes to this file (a file-size/role access-control
feature, a schema fix, and a partial old-style response.js migration on
one endpoint) don't overlap cleanly with what we did here.
This file originally had no try/catch at all across any of its 8
endpoints (same as compareReadings.js); real try/catch blocks wrapping
each data-fetch were already added earlier in this branch's work. This
commit adds descriptive context to all 8 catch blocks' failure() calls
via new Error(message, { cause: err }), one per line/bar/radar/threeD x
meters/groups combination, for consistency with every other file's catch
blocks and to distinguish each endpoint's errors from one another in the
admin logs.

Converts all 8 success paths from res.json(forJson) to success(res,
forJson) -- all return plain objects (via lodash mapValues, keyed by
meter/group id) or similarly structured data, never raw strings, so
JSON-encoding is identical either way.
Real try/catch (replacing a literal // TODO: Add try/catch error
handling... comment) and the success()/failure() conversion were already
added earlier in this branch's work. This commit adds a descriptive
context wrapper to the catch block via new Error(message, { cause: err
}), for consistency with every other file's catch blocks -- there was no
original logging to restore here, same as compareReadings.js/
unitReadings.js, since the endpoint never had any error handling at all
before.

Confirmed live (validation failure, no-op success, and a real redoCik
success). conversionArrayParamsTest.js has zero real coverage of this
endpoint due to a pre-existing URL typo (tests hit /api/conversionArray/
refresh, camelCase; the route is mounted at /api/conversion-array,
hyphenated) -- same bug class as baseline.js's test file, both from PR
OpenEnergyDashboard#1528, documented in OED NOTES.md.
@huss

huss commented Aug 2, 2026

Copy link
Copy Markdown
Member

@CarlyAThomas Thank you for this substantial contribution, esp. being your first one. I have reviewed your well documented changes and comments provided with this PR. I am very appreciative of the extra work you did outside the direct issue you were working on. At this point I wanted to give some early thoughts:

  • It will take a little while to get through the many changes since they impact many files/areas. Please know I am trying to get the time to go through them all with the care they deserve. If I am delaying you then please let me know.
  • You mention a number of items that are not done within the code changes of this PR but indicating they probably deserve consideration. In at least one case you talk about future work. These seem very useful and I will be commenting on them later. For now, I wanted to get a sense of what you might want for when I respond. I always prefer to give the person who located a concern the option to create the issue/documentation on the situation. Do you think you might want to do that for some/all of the items you discussed? If not, OED can create them as desired. This will help me know, if possible, what would be best to say/provide in my thoughts on each of these. Obviously, OED is not asking you to commit to definitely do any issues but it would remain an option if you want to after any discussion is done on them.

Again, thanks for your efforts and any information/thoughts/questions. I hope this is clear for you.

@CarlyAThomas

Copy link
Copy Markdown
Author

Thanks! And no worries on timing—I know this is a large review, so I'd much rather you take the time needed than rush through it.

As for the additional items, I'd be happy to create the issues for the ones that make sense. My main goal is simply to make sure they're valid observations and that the scope is appropriate before they're tracked. Once we've discussed them, I'm happy to write them up if you think they'd be worthwhile additions to the issue tracker.

@huss

huss commented Aug 4, 2026

Copy link
Copy Markdown
Member

Once we've discussed them, I'm happy to write them up if you think they'd be worthwhile additions to the issue tracker.

Thank you and this sounds great. It will be a little while but I'll get to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

standardize server responses to use success/failure functions

2 participants