Skip to content

Enforce GET reads and POST writes in the contract - #86

Merged
Snowpile merged 4 commits into
mainfrom
fix/write-method-contract-local
Nov 8, 2026
Merged

Snowpile merged 4 commits into
mainfrom
fix/write-method-contract-local

Conversation

@thavelin

@thavelin thavelin commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #73.

The route contract now expects GET for List*/Get*/Lookup* and exactly HealthRoutes.Healthcheck, and POST for every other RPC. A mismatch reports the expected method and naming rule. Path violations continue to accumulate independently, and the exception map remains empty.

Regression cases cover valid reads and writes, unrecognized read names, PUT/PATCH/DELETE, the exact Healthcheck exception and lookalikes, and accumulated reasons across routes and prefixes. The request standard documents the enforced naming rule. No production or generated code differs from main.

Validation: focused contract regressions and both Go module test suites passed, as did Go build/vet, protobuf lint/format/public-breaking checks, and formatting/diff checks. Full local make check stopped at Windows path-separator handling in the existing TypeScript entry-point checker. CI runs the complete checks and regeneration on Linux.

@thavelin
thavelin requested a review from ptmo516 October 15, 2026 14:09
Comment thread go/contract/routes_test.go Outdated
if isRead(r) && r.Method != "GET" {
reasons[key] = append(reasons[key], "read over "+r.Method)
}
if !isRead(r) && r.Method == "GET" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"write over GET" asserts more than the check knows. isRead only recognises List*/Get*/Lookup* (plus Healthcheck), so a read-only RPC under any other name — say SearchTopics over GET — is reported as a write. Failing closed is right; the message is what's off, since it misdescribes the RPC and doesn't tell the author what to do.

Either of these is a one-string change (plus the expected strings in routes_conformance_test.go):

  • Say what the check actually knows: "GET on an RPC not named List*/Get*/Lookup*"
  • Or keep the framing and name the remedy: "write over GET (reads are named List*/Get*/Lookup*)"

Related: the AGENTS.md row says List*/Get*/Lookup* use GET, but not that every read must carry one of those names — which is what this check now enforces. If that's intended, worth saying in the row too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9cca3d7. The diagnostic now gives the expected method and explains the read naming rule without calling an unrecognized RPC a write. Updated AGENTS.md and added the SearchTopics-over-GET case, keeping the exact HealthRoutes.Healthcheck exception. Focused tests and make test pass locally.

Comment thread go/contract/routes_test.go Outdated
Comment on lines +98 to +106
if r.Method != "GET" && r.Method != "POST" {
reasons[key] = append(reasons[key], "unsupported method: "+r.Method)
}
if isRead(r) && r.Method != "GET" {
reasons[key] = append(reasons[key], "read over "+r.Method)
}
if !isRead(r) && r.Method == "GET" {
reasons[key] = append(reasons[key], "write over GET")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, not a blocker.

These three checks can be one. Each RPC has exactly one correct method, so:

want, kind := "POST", "write"
if isRead(r) {
	want, kind = "GET", "read"
}
if r.Method != want {
	reasons[key] = append(reasons[key], kind+" over "+r.Method)
}
  • Still enforces "no other methods": anything but the one expected method fails.
  • One reason per mistake. Today a read over PUT reports unsupported method: PUT; read over PUT, while a write over PUT reports only unsupported method: PUT and never points at POST. With this, every reason implies its fix.

Cost: the expected strings in routes_conformance_test.go change (unsupported method: X → read over X / write over X). If you're touching the write over GET wording anyway (other comment), it lands on the same lines.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified this to one expected-method check in 9cca3d7: GET for recognized reads, POST otherwise. Each method mismatch now has one reason, and path errors still accumulate separately. Updated the expectations; focused tests and make test pass locally.

Comment thread go/contract/routes_conformance_test.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional. Consider moving these tests into routes_test.go, beside nonConformingRoutes, and dropping this file. go/contract keeps one test file per topic, and "conformance" already names the reusable suites (storetest, sessiontest), which this isn't.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved both regression tests beside nonConformingRoutes in routes_test.go and removed the separate file in 9cca3d7. Tests pass locally.

Comment thread go/contract/routes_conformance_test.go Outdated
Comment on lines +23 to +28
{"error - HEAD", "TopicRoutes", "CreateTopic", "HEAD", "/topic", "unsupported method: HEAD"},
{"error - OPTIONS", "TopicRoutes", "CreateTopic", "OPTIONS", "/topic", "unsupported method: OPTIONS"},
{"error - unknown method", "TopicRoutes", "CreateTopic", "BREW", "/topic", "unsupported method: BREW"},
{"error - empty method", "TopicRoutes", "CreateTopic", "", "/topic", "unsupported method: "},
{"error - lowercase method", "TopicRoutes", "CreateTopic", "post", "/topic", "unsupported method: post"},
{"error - read over HEAD", "TopicRoutes", "GetTopic", "HEAD", "/topic", "unsupported method: HEAD; read over HEAD"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional. These rows, and health over OPTIONS, test methods the manifest can't contain: routegen emits only GET/POST/PUT/PATCH/DELETE, uppercase, and fails make gen on anything else (routegen/internal/model/model.go, already tested in describe_test.go). Consider dropping them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped those rows, including read-over-HEAD and health-over-OPTIONS, in 9cca3d7. Confirmed routegen already rejects unsupported patterns and tests that boundary. Kept PUT/PATCH/DELETE coverage here. Focused tests and make test pass locally.

@thavelin
thavelin requested a review from ptmo516 October 22, 2026 02:57
@thavelin
thavelin requested a review from jmbarzee November 5, 2026 04:32

@Snowpile Snowpile left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Merges cleanly on main, make test passes, and it meets all three of #73's criteria.

Optional cleanups:

  • The AGENTS.md row could read "List*/Get*/Lookup* and HealthRoutes.Healthcheck use GET; every other RPC uses POST, so a read must carry one of those names."
  • The two expected diagnostics are repeated ~20 times in TestNonConformingRoutes_Methods; two constants would shorten it.
  • The doc comment on isRead ("Reads are named for what they do…") was dropped; worth keeping, now with the Healthcheck exception.

@Snowpile
Snowpile added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 2cdb4ac Nov 8, 2026
2 of 3 checks passed
@Snowpile
Snowpile deleted the fix/write-method-contract-local branch November 8, 2026 22:59
@thavelin

Copy link
Copy Markdown
Collaborator Author

Prepared the three optional cleanups in a separate follow-up to the merged PR: the shorter rule text, an isRead comment covering Healthcheck, and local constants for the repeated test expectations. The 25 regression subcases, manifest check and both-module make test pass locally.

Follow-up: #146.

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.

Hold every write to POST and refuse PUT, PATCH and DELETE

3 participants