Skip to content

fix(errors): map application error codes to HTTP statuses - #135

Merged
hsinatfootprintai merged 2 commits into
mainfrom
fix/59-error-codes-to-http-status
Aug 13, 2026
Merged

fix(errors): map application error codes to HTTP statuses#135
hsinatfootprintai merged 2 commits into
mainfrom
fix/59-error-codes-to-http-status

Conversation

@hsinatfootprintai

Copy link
Copy Markdown
Contributor

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.Code has carried GRPCCode() for a long time (CodeNotFound → codes.NotFound, CodeBadParameters → codes.InvalidArgument, …), and grpc-gateway renders the HTTP status from the gRPC status. What was missing is GRPCStatus() on *sderrors.Error, which landed 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 in collection ..."}
       ^^^ 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, not assumed

case before after
missing document DELETE 500 404
missing document GET 500 404
unknown project DELETE 500 400

(GetCollection on a missing collection still answers 200 with an empty body — that is its own deliberate NotFound→empty-response behaviour, not this mapping.)

Migration

  • 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. false preserves the old behaviour — the previous constructor logged at info and above; true would add debug.

A latent bug this surfaced

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 (a field with no example):

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 stating plainly: this was only visible because #134 made the postgres-backed tests run. On the old -short CI 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, integrationtestok
  • every remaining package — ok
  • make gen-check0 (generated code still in sync)
  • go vet ./... — clean

The 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

Hannah Cheng and others added 2 commits August 13, 2026 21:49
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>
@hsinatfootprintai
hsinatfootprintai merged commit 0e6680a into main Aug 13, 2026
2 checks passed
@hsinatfootprintai
hsinatfootprintai deleted the fix/59-error-codes-to-http-status branch August 13, 2026 14:08
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.

envelope error message and properly convert error code to http header

1 participant