fix(errors): map application error codes to HTTP statuses - #135
Merged
Conversation
A "not found" left this service as HTTP 500. So did bad parameters, and every other application error - a client could not tell a missing document from a broken server, which is what #59 asked for four hundred days ago. The mapping was never missing. sderrors.Code has carried GRPCCode() for a long time (CodeNotFound -> codes.NotFound, CodeBadParameters -> codes.InvalidArgument, ...) and grpc-gateway renders the status from there. What was missing is GRPCStatus() on *sderrors.Error, which arrived in a newer github.com/sdinsure/agent than this module pinned. Without it every error left gRPC as codes.Unknown and the gateway rendered 500, discarding a code the error was carrying the whole time: [500] {"code":2, "message":"code(1), document nope not found ..."} ^^^ HTTP ^^^ Unknown ^^^ CodeNotFound, right there Consumers whose module graph already selected a newer sdinsure got correct statuses out of these same handlers - which is why grandturks saw a proper 501 from DeleteDocument while restcol standalone saw 500. v0.0.0-20240507074650-281b67936555 -> v0.0.0-20260812141753-3e9fb7b72eaa Measured after the bump, not assumed: missing document DELETE -> 404 (was 500) missing document GET -> 404 (was 500) unknown project DELETE -> 400 (was 500) Two API changes needed migrating: - runtime.ProjectInfor gained Visibility(). restcol has no per-project visibility - ModelProject carries an ID and a type, and authorization is AllowEveryOne - so it returns "", which the interface defines as unknown/unresolved and which sdinsure's own invalidProjectInfor returns. Inventing "PUBLIC" would tell a future authorization path that every restcol project is world-visible, on no evidence. - logger.NewLogger gained a verbose bool. Passing false preserves the old behaviour: the previous constructor logged at info and above. One latent bug surfaced and had to be fixed with it. The bump carries gorm.io/driver/postgres 1.5.7 -> 1.5.11 and pgx v5.5.5 -> v5.9.2, and the newer pgx calls driver.Valuer on nil pointers where the old one short-circuited to NULL. SwagValueValue.Value has had a pointer receiver since 073e3a3, so a nil *SwagValueValue satisfies driver.Valuer, and ModelFieldSchema.FieldExample is routinely nil: unable to encode (*SwagValueValue)(nil) into text format for jsonb (OID 3802): proto: google.protobuf.Value: none of the oneof fields is set That failed every insert carrying a schema field without an example, taking out TestDocument and TestStorage. Value now returns NULL for a nil receiver, and Scan tolerates a NULL column instead of panicking on its type assertion. Worth noting this was only visible because #134 made the postgres-backed tests run. On the old CI it would have merged green and broken writes in production. The integration assertions now check the HTTP status as well as the application code. The previous comment explaining why they could not is removed along with the reason for it. Closes #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first CI run of this branch failed, and not on a test:
# github.com/footprintai/restcol/pkg/storage/testutil
go: no such tool "covdata"
# github.com/footprintai/restcol/pkg/version
go: no such tool "covdata"
Every test passed. The two packages named are the ones with no test files,
where -coverprofile needs the covdata tool.
The cause is a version split I introduced without noticing. sdinsure
v0.0.0-20260812141753 declares `go 1.25.0`, so `go mod tidy` raised this
module's directive from 1.23.0 to 1.25.0 and dropped `toolchain go1.24.1` -
while the workflow still installed 1.24.1. Confirmed it is a real
requirement rather than tidy being opinionated:
$ GOTOOLCHAIN=go1.24.1 go build ./...
go: github.com/sdinsure/agent@... requires go >= 1.25.0 (running go 1.24.1)
So CI was compiling through an auto-downloaded 1.25 toolchain while its
tool lookup still resolved against the 1.24.1 install, and only the two
packages that need covdata noticed.
Aligns the workflow with the directive: go-version 1.24.1 -> 1.25.0 in both
jobs. Verified with GOTOOLCHAIN=go1.25.0 running CI's exact command,
`go test -race ./... -p 1 -timeout 15m -coverprofile=...`: no covdata error,
and every postgres-backed package green under -race.
Refs: #59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #59 — "something like error with not found should be converted into http 404".
What was wrong
A "not found" left this service as HTTP 500. So did bad parameters, and every other application error. A client could not tell a missing document from a broken server.
The mapping was never missing.
sderrors.Codehas carriedGRPCCode()for a long time (CodeNotFound → codes.NotFound,CodeBadParameters → codes.InvalidArgument, …), and grpc-gateway renders the HTTP status from the gRPC status. What was missing isGRPCStatus()on*sderrors.Error, which landed in a newergithub.com/sdinsure/agentthan this module pinned. Without it, every error left gRPC ascodes.Unknownand the gateway rendered 500 — discarding a code the error was carrying the whole time:Consumers whose module graph already selected a newer sdinsure got correct statuses out of these same handlers — which is why grandturks saw a proper 501 from
DeleteDocumentwhile restcol standalone saw 500.Measured, not assumed
DELETEGETDELETE(
GetCollectionon a missing collection still answers 200 with an empty body — that is its own deliberate NotFound→empty-response behaviour, not this mapping.)Migration
runtime.ProjectInforgainedVisibility(). restcol has no per-project visibility —ModelProjectcarries an ID and a type, and authorization isAllowEveryOne— so it returns"", which the interface defines as unknown/unresolved and which sdinsure's owninvalidProjectInforreturns. Inventing"PUBLIC"would tell a future authorization path that every restcol project is world-visible, on no evidence.logger.NewLoggergained averbose bool.falsepreserves the old behaviour — the previous constructor logged at info and above;truewould add debug.A latent bug this surfaced
The bump carries
gorm.io/driver/postgres 1.5.7 → 1.5.11andpgx v5.5.5 → v5.9.2, and the newer pgx callsdriver.Valueron nil pointers where the old one short-circuited to NULL.SwagValueValue.Valuehas had a pointer receiver since 073e3a3, so a nil*SwagValueValuesatisfiesdriver.Valuer— andModelFieldSchema.FieldExampleis routinely nil (a field with no example):That failed every insert carrying a schema field without an example, taking out
TestDocumentandTestStorage.Valuenow returns NULL for a nil receiver, andScantolerates a NULL column instead of panicking on its type assertion.Worth stating plainly: this was only visible because #134 made the postgres-backed tests run. On the old
-shortCI it would have merged green and broken writes in production. That is #134 earning its keep on its first outing.Verification
Full suite against
postgres:16-alpine:pkg/storage/{documents,collections,projects},pkg/app,integrationtest— okmake gen-check— 0 (generated code still in sync)go vet ./...— cleanThe integration assertions now check the HTTP status as well as the application code; the comment explaining why they couldn't is removed along with the reason for it.
Refs: #126, #134, FootprintAI/grandturks#936
🤖 Generated with Claude Code