mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: forbid direct response body JSON decode in codersdk (#27859)
Add a ruleguard rule forbidding direct `json.NewDecoder(res.Body).Decode(...)` on `*http.Response` in codersdk packages, so new typed endpoints use `codersdk.ReadBodyAsJSON` and keep returning structured errors for non-JSON bodies. The rule matches both the chained call form and decoders assigned to a variable first. Intentional raw-body paths carry documented `//nolint:gocritic` exceptions: the 16 agent-direct HTTP decodes in `workspacesdk/agentconn.go` route through a single `decodeAgentJSON` helper (agent-direct over tailnet, so `ReadBodyAsJSON`'s reverse proxy/SSO error guidance does not apply), and the Azure IMDS attested-document decode in `agentsdk/azure.go` keeps an inline exception. The two `UseNumber` decoders in `licenses.go` are migrated to a new `codersdk.ReadBodyAsJSONUseNumber`, so `coder licenses add/list` also return structured errors for non-JSON bodies instead of `invalid character '<' looking for beginning of value`. Note for local verification: golangci-lint caches results, so run `golangci-lint cache clean` after modifying `scripts/rules.go` or the rule may silently not fire. Final PR of the stack on #27804, #27857, and #27858. Refs #27044. Stack plan Inventory (full-tree audit): 280 migratable call sites across 47 files; 17 excluded (16 agent-direct HTTP sites in `workspacesdk/agentconn.go`, 1 Azure IMDS decode in `agentsdk/azure.go`). 1. **#27857** `refactor(codersdk): use ReadBodyAsJSON in typed endpoints`: mechanical migration of all sites except `chats.go` (224 sites, 46 files). 2. **#27858** `refactor(codersdk): use shared error helpers in chat endpoints`: migrate the 56 `chats.go` sites and consolidate the duplicated `readRawBodyAsError`/`newResponseError` helpers onto the shared `client.go` error path, with regression tests for the 409 usage-limit flow. 3. **#27859** `chore: forbid direct response body JSON decode in codersdk`: ruleguard rule with documented exceptions for the intentional raw-body paths, plus `ReadBodyAsJSONUseNumber` for the `licenses.go` decoders. Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
This commit is contained in:
@@ -529,3 +529,32 @@ func netAddrNil(m dsl.Matcher) {
|
||||
m.Match("$_.RemoteAddr().Network()").Report("RemoteAddr() may return nil and segfault if you call Network()")
|
||||
m.Match("$_.LocalAddr().Network()").Report("LocalAddr() may return nil and segfault if you call Network()")
|
||||
}
|
||||
|
||||
// codersdkResponseBodyDecode ensures that codersdk typed endpoint methods
|
||||
// decode HTTP response bodies through codersdk.ReadBodyAsJSON, which
|
||||
// returns a structured *codersdk.Error when an intermediary such as a
|
||||
// reverse proxy or SSO portal responds with HTML, an empty body, or other
|
||||
// non-JSON content. Responses that intentionally bypass the Coder API
|
||||
// error contract (agent-direct HTTP over tailnet, cloud metadata
|
||||
// services) suppress this rule with a nolint:gocritic comment explaining
|
||||
// why.
|
||||
//
|
||||
// Both the chained call form and decoders assigned to a variable first
|
||||
// are matched.
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func codersdkResponseBodyDecode(m dsl.Matcher) {
|
||||
m.Import("encoding/json")
|
||||
m.Import("net/http")
|
||||
m.Match(
|
||||
`json.NewDecoder($res.Body).Decode($_)`,
|
||||
`$_ := json.NewDecoder($res.Body)`,
|
||||
`$_ = json.NewDecoder($res.Body)`,
|
||||
).
|
||||
Where(
|
||||
(m["res"].Type.Is("*http.Response") || m["res"].Type.Is("http.Response")) &&
|
||||
m.File().PkgPath.Matches(`github.com/coder/coder/v2/codersdk`) &&
|
||||
!m.File().Name.Matches(`_test\.go$`),
|
||||
).
|
||||
Report("Use codersdk.ReadBodyAsJSON to decode typed API responses so non-JSON bodies produce a structured error. For responses that are intentionally not Coder API JSON, add a nolint:gocritic comment explaining why.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user