Standardize server responses to use success/failure functions - #1688
Standardize server responses to use success/failure functions#1688CarlyAThomas wants to merge 20 commits into
Conversation
…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.
|
@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:
Again, thanks for your efforts and any information/thoughts/questions. I hope this is clear for you. |
|
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. |
Thank you and this sounds great. It will be a little while but I'll get to this. |
Description
This standardizes how every server route responds, moving all 21 route files (63 endpoints) onto
response.js'ssuccess()/failure()functions instead of the mix of hand-rolledres.*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 andobvius.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 optionalsafeMessagemeant 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 calllog.warn()/log.error()/etc. themselves — that all happens insidefailure()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 aLogLevelseverity (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'semailLevelonly e-mails admins forERROR-priority events, so if every failure had collapsed to a single severity, routineWARN-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 withLogLevel.SILENTif 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 any500-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:
groups.js(5 endpoints:GET /,/idname,/deep/groups,/children/:group_id,/allChildren/),conversions.js'sGET /,meters.js'sGET /,users.js'sGET /,maps.js'sGET /,preferences.js'sGET /, andciks.js'sGET /— all the same shape of bug.maps.js's/editvalidation 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.jshad a completely broken logger:const log = require('../log')imports the log module (an object), but the file called it directly aslog(message, 'error')— aTypeErroron 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 throughfailure().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'sPOST /delete,meters.js'sGET /:meter_id,users.js'sGET /:user_id, andmaps.js'sGET /:map_idall logged the error correctly but then responded with a rawres.sendStatus(500)instead of going throughfailure().conversions.js's/edit:success(res)was called unconditionally after the try/catch, so it fired a second time even afterfailure()had already responded inside a caught error.Promise.all(...).then(...)chains ingroups.js(GET /andGET /deep/groups): neither awaited nor given a.catch(), so an error inside either callback fired after the enclosingtry/catchhad already exited — an unhandled rejection with nowhere to land, independent of the Express-4 async-forwarding gap below.~40 call sites' worth of lost log context: the initial mechanical migration pass replacedlog.error("descriptive text: " + err, err)-style calls with barefailure(res, code, err), which loses everything excepterr.message. Every catch block across all 21 files was reviewed individually and had its original descriptive context restored vianew Error(message, { cause: err }), which also required a smalllog.jsfix 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 storesmessage).express-async-errorsas 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 aTODOcomment already inapp.jspointing at that issue.A few smaller things fixed in passing
While I was already touching these exact lines:
units.js's/addUnitvalidation log said "edit units" instead of "add units" (copy-paste from the edit endpoint).maps.js's/deletecatch block said "Error while deleting group" instead of "map" (copy-paste fromgroups.js).conversions.js's/simulate-deleteclient-facing validation message said "delete conversions" instead of "simulate deletion of conversions" (copy-paste from/delete, right above it).meters.jshad a TODO left by the original authors:/editand/addMeter's success paths calledres.json(...)directly instead ofsuccess(), with a comment saying they weren't suresuccess()could return values. I checked —res.send()JSON-encodes objects identically tores.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 bypassingsuccess()for the same reason (preferences.js,logs.js,baseline.js,ciks.js,compareReadings.js,unitReadings.js,conversionArray.js).Test coverage added
responseParamsTest.jsalready existed and coveredsuccess()/failure()'s response-sending mechanics (status codes, comment handling, edge cases), but every one of those tests passederror = null, sofailure()'s actual logging behavior — the core of what this issue asked for — was never exercised. Added 5 tests, using asinonstub onlog.log()so nothing actually hits the real DB/e-mail during the test:LogLevel.WARNactually results inlog.log()being called withWARN, the error's message, and the error object itself.LogLevel.ERRORwhen no severity is passed — this is the "logs are created unless the developer opts out" behavior from the issue.LogLevel.SILENTsuppresses logging entirely — even with a real error present,log.log()is never called.errorisnull/falsy, nothing gets logged regardless of severity.Errorwith a fake secret in its message, sends it throughfailure()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 return200or 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
developmentThis took long enough that
developmentmoved a lot underneath it — 106 commits, 48 files, including the new session-invalidation/auth work (login.jsbecameloginLogout.js) and changes overlapping 14 of the 18 route files this PR touches. I rebased onto currentdevelopmentand 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-skippedcompareReadingsParamsTest.jstests get re-enabled).Type of change
Checklist
Author: @CarlyAThomas
Limitations
Two files stay outside the
response.jsconsolidation, both discussed and agreed with @huss on the issue:csvPipeline/success.jsandcsvPipeline/failure.jsare 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.jskeeps its own separatesuccess/failureimplementation — different signature, a hardware-specific406status, 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 tosuccessObvius/failureObvius, so the distinction fromresponse.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'sGET /children/:group_idhas no input validation on:group_id, unlike its siblings, so malformed input reaches the database directly instead of being rejected cleanly.readings.js'sGET /line/count/meters/:meter_idshas no numeric pattern onmeter_ids(its sibling endpoint andcompareReadings.js's equivalent both do), so non-numeric input reaches the DB asNaNand turns into anERROR-level, admin-emailed log entry instead of a clean400.POST /api/units/delete,POST /api/conversions/delete, andPOST /api/users/deleteall run aDELETE ... WHEREwith noRETURNINGclause throughconn.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 a200success. Confirmed live for all three.GET /api/users/:user_idsends the raw DB row back to the client, including the bcryptpasswordHashfield. Nothing this PR changes, but probably worth a look — there's no real reason an admin caller needs the hash itself.400s leak rawjsonschemavalidator text (likeinstance.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.jsandconversionArrayParamsTest.js(from PR #1528) hit the wrong URL —/api/baselineand/api/conversionArray/refreshinstead of the actual mounted paths,/api/baselinesand/api/conversion-array. Every request in both files silently lands on the SPA'sindex.htmlcatch-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 withcurlinstead. 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 anEPIPE(multer doesn't drain the request stream after a file-count error — nothing incsv.js/csvPipelineis 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 eachPOST /api/logs/infohas finished writing to the DB by the time it responds200. It hasn't:log.js'sLogger.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.jsraw-response issues this PR fixes. The mechanism change (routing throughsuccess()/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 replacingusers.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 howsuccess()/failure()work, whatLogLevelcontrols, 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.