fix(poracle): a refused human or profile write is the caller's error, not a 500 - #824
Merged
Conversation
…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.
…fusals # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PoracleHumanProxyhandled only 404 and one 409. Everything else fell through toEnsureSuccessStatusCode(), 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:
POST /api/humans {"garbage":true}id and name are requiredPOST .../setLocation/abc/definvalid latitudePOST .../setAreas {"not":"anarray"}cannot unmarshal object into Go value of type []stringPOST .../switchProfile/notanumberinvalid profile numberPOST .../switchProfile/99Profile not foundPOST /api/profiles/{id}/update {"bad":1}profile_no must be specifiedUser not found422 is matched anyway: 5.2.1's
/api/v2/humanssurface 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 404Profile not found.ProfileControllerpre-checks, so this is a race — butProfileOverviewServicecallsSwitchProfileAsync(originalProfileNo)in afinallyafter duplicate and import, where anAccountGoneExceptionwould sign the user out and mask whatever actually failed.404 page not foundfor 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 carryingStatusCode = 404, matching whatProfileControlleralready returns for a missing profile. A genuineUser not foundstill throwsAccountGoneExceptionon every path, with its own test, a[Theory]across stop/setAreas/getProfiles, and a filter test asserting the new filter declinesAccountGoneExceptionso registration order cannot make a dead account answer 400.A new exception type rather than reusing
AlarmValidationExceptionReuse 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.Describegained 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:
GetHumanAsyncandCheckLocationAsyncreturnnullon any non-success and never calledEnsureSuccessStatusCode.HumanService,ProfileService.WithActiveProfileAsyncandTestAlertServiceall 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 — aresults[].errorinside 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 catchesHttpRequestExceptionaround these calls, so no existing handler silently swallows the new type.Tests
2407 passing, 31 new.
PoracleHumanProxy.cswas 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).