Skip to content

fix(poracle): a refused human or profile write is the caller's error, not a 500 - #824

Merged
hokiepokedad2 merged 2 commits into
developfrom
fix/human-proxy-refusals
Aug 24, 2026
Merged

fix(poracle): a refused human or profile write is the caller's error, not a 500#824
hokiepokedad2 merged 2 commits into
developfrom
fix/human-proxy-refusals

Conversation

@hokiepokedad2

Copy link
Copy Markdown
Contributor

PoracleHumanProxy handled only 404 and one 409. Everything else fell through to EnsureSuccessStatusCode(), so a 400 from PoracleNG — the caller's own mistake, with a perfectly good explanation attached — was reported to the user as 500 "An unexpected error occurred" and logged as a server fault. That is the shape #539 fixed on the tracking proxy; it was never done here.

This was never a 5.2.1 problem

Probed both dev servers with deliberately invalid requests. 5.1.0 and 5.2.1 answer identically on every v1 route this proxy calls:

Request Both servers
POST /api/humans {"garbage":true} 400 id and name are required
POST .../setLocation/abc/def 400 invalid latitude
POST .../setAreas {"not":"anarray"} 400 cannot unmarshal object into Go value of type []string
POST .../switchProfile/notanumber 400 invalid profile number
POST .../switchProfile/99 404 Profile not found
POST /api/profiles/{id}/update {"bad":1} 400 profile_no must be specified
any route, missing account 404 User not found

422 is matched anyway: 5.2.1's /api/v2/humans surface answers 422 problem+json for the same mistakes, and this proxy will move onto it eventually. Matching it now means that migration doesn't re-open the bug.

No test data was left behind.

The real find: a 404 was signing people out

Previously every 404 threw AccountGoneException → 401 → the SPA signs you out. Two of them are not dead accounts:

  • switchProfile/{n} answers 404 Profile not found. ProfileController pre-checks, so this is a race — but ProfileOverviewService calls SwitchProfileAsync(originalProfileNo) in a finally after duplicate and import, where an AccountGoneException would sign the user out and mask whatever actually failed.
  • gin's plaintext 404 page not found for a route an older PoracleNG lacks was reported as "your account was deleted".

A 404 now means account-gone only when PoracleNG says so (user not found / human not found). The other two surface as a refusal carrying StatusCode = 404, matching what ProfileController already returns for a missing profile. A genuine User not found still throws AccountGoneException on every path, with its own test, a [Theory] across stop/setAreas/getProfiles, and a filter test asserting the new filter declines AccountGoneException so registration order cannot make a dead account answer 400.

A new exception type rather than reusing AlarmValidationException

Reuse was smaller and I nearly took it. Against it: that type's own doc-comment says "a rejection of an alarm", and it shares PoracleProblemDetails.Unexplained — literally "Poracle rejected the alarm." — which is what a profile rename or a saved-place delete would have shown whenever PoracleNG explained nothing itself.

PoracleRequestRefusedException + PoracleRequestRefusedExceptionFilter, registered globally beside the others, same { "error": … } body. The filters directory already holds eight narrowly-named exception/filter pairs, so this is the house pattern rather than an exception to it. Describe gained a (body, fallback) overload; the one-argument form is untouched, so nothing on the tracking side moved.

Call sites

14 changed through a single EnsureAcceptedAsync: human create/start/stop/admin-disable/set-location/set-areas/switch-profile, profile get/add/update/delete/copy, and place get/add/delete (after its 409 check, which still runs first).

2 left alone: GetHumanAsync and CheckLocationAsync return null on any non-success and never called EnsureSuccessStatusCode. HumanService, ProfileService.WithActiveProfileAsync and TestAlertService all branch on that null; making them throw is a much larger change than this. A test pins that they still answer null on a 400.

AddPlaceAsync's in-band refusal — a results[].error inside a 200 — is untouched and tested.

500s and non-delete-place 409s still throw HttpRequestException, tested, so a later edit cannot quietly dress a server fault up as a caller error. Nothing catches HttpRequestException around these calls, so no existing handler silently swallows the new type.

Tests

2407 passing, 31 new. PoracleHumanProxy.cs was reverted and the new tests run against the old code first: 15 red, 16 green — the green ones being exactly the legitimate-case tests (409 still names the referencing rules, 500 still faults, every 200 path still succeeds, reads still answer null).

…aller's mistake

PoracleHumanProxy read 404 and one 409 on the delete-place path and let every
other non-success reach EnsureSuccessStatusCode(), whose HttpRequestException
the global handler flattens into 500 "An unexpected error occurred". So
"state is required (true/false)", "profile_no must be specified" and
"invalid latitude" -- all of which PoracleNG had explained -- arrived as a
server fault and were logged as one. #539 closed exactly this on the tracking
proxy and never reached the other half of the surface.

Fourteen call sites now go through EnsureAcceptedAsync, which reads 400 and
422 through the existing PoracleProblemDetails and throws
PoracleRequestRefusedException. A new filter answers it, rather than reusing
AlarmValidationException: that type is documented as an alarm rejection and
shares an alarm-worded fallback, so a profile rename would have been told
"Poracle rejected the alarm." Describe() gained a fallback overload for the
same reason.

GetHumanAsync and CheckLocationAsync keep returning null on failure --
HumanService, ProfileService and TestAlertService all branch on it.

Also stops a 404 that is not a dead account signing the user out. PoracleNG
answers 404 "Profile not found" for a missing profile number and gin answers a
plaintext "404 page not found" for a route an older build lacks; both were read
as AccountGoneException and produced a 401, and inside
ProfileOverviewService's restore-the-profile finally that happened while
swallowing the real failure. Account-gone now needs PoracleNG to say so.

Statuses and bodies verified live against 5.1.0 (:3040) and 5.2.1 (:3042):
the v1 routes answer 400 identically on both, and 5.2.1's /api/v2/humans
surface answers RFC 9457 at 422 for the same mistakes, which is why both are
matched. No test data was created.
@hokiepokedad2
hokiepokedad2 merged commit 22b936e into develop Aug 24, 2026
6 checks passed
@hokiepokedad2
hokiepokedad2 deleted the fix/human-proxy-refusals branch August 24, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant