refactor(service): stub the unused /accounts/* mirror with a 501 "report it" handler (refs #451)

Shrink the route surface the #451 refactor must preserve by retiring the
/accounts/{account}/* compatibility mirror. Across the full recording corpus
(all _/backup/*, _/mitm, _/i195, _/issue-94, captures + data/ + tests/, 139k+
.http files) no speaker or app uses the /accounts prefix, and every operation it
offered is served by the /streaming/account/* paths real clients actually use.

- New HandleUnsupported: returns 501 and logs the full request + client IP + a
  "please report this" message, so any real-world use surfaces instead of being
  silently dropped, and the prefix becomes a clean removal candidate.
- Re-point every /accounts/* route to it. The frozen /streaming/* contract is
  left entirely on its real handlers (those stay even where our corpus didn't
  exercise them — absence of capture is not proof of disuse).
- Migrate the integration tests off the /accounts mirror onto their recorded
  /streaming/account/* equivalents (register/unregister/spotify_full_flow), then
  pin the mirror's 501 contract in unsupported_routes.http.
- Router + frozen-route-coverage golden files updated accordingly.

make test-http-client: 91 requests, 0 failed. go test + golangci-lint clean.

Note for release time: call out the intentional /accounts/* 501 breakage in the
release notes' Noteworthy section (use /streaming/account/* instead).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-06 19:11:24 +02:00
co-authored by Claude Opus 4.8
parent 44f3ccfc18
commit ea6ee3e097
9 changed files with 263 additions and 69 deletions
@@ -0,0 +1,60 @@
package handlers
import (
"io"
"log"
"net/http"
)
// unsupportedReportURL is where we ask operators to report a real client that
// depends on an endpoint we treat as unused.
const unsupportedReportURL = "https://github.com/gesellix/Bose-SoundTouch/issues"
// maxUnsupportedBodyLog caps how much of an unexpected request body we log.
const maxUnsupportedBodyLog = 2048
// HandleUnsupported is wired to frozen routes that no real speaker or app was
// ever observed using (verified against the recorded interaction corpus). It
// exists so the API surface the refactor (issue #451) has to preserve stays as
// small as possible: these routes are candidates for removal, but rather than
// drop them blind we make them fail loudly and observably.
//
// It returns 501 Not Implemented and logs the full request (method, path,
// query, client IP, user-agent and a capped, sanitized body) plus a message
// asking the operator to report it, so that if some device or old app actually
// relies on the route we find out and restore it instead of silently breaking
// it during the refactor.
func (s *Server) HandleUnsupported(w http.ResponseWriter, r *http.Request) {
client := clientHostFromRemoteAddr(r.RemoteAddr)
var body []byte
if r.Body != nil {
body, _ = io.ReadAll(io.LimitReader(r.Body, maxUnsupportedBodyLog))
}
log.Printf("[unsupported] %s %s%s called by client=%s ua=%q — this endpoint is treated as unused and returns 501. "+
"If a speaker or app you rely on needs it, please report it at %s so we can keep it. body=%q",
sanitizeLog(r.Method),
sanitizeLog(r.URL.Path),
sanitizeLog(querySuffix(r)),
sanitizeLog(client),
sanitizeLog(r.UserAgent()),
unsupportedReportURL,
sanitizeLog(string(body)),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotImplemented)
_, _ = w.Write([]byte(`{"error":"not_implemented","message":"This endpoint is not implemented by AfterTouch. ` +
`If your speaker or app depends on it, please report it at ` + unsupportedReportURL + `"}`))
}
// querySuffix returns "?<rawquery>" or "" so the log line shows the query
// without an empty trailing "?".
func querySuffix(r *http.Request) string {
if r.URL.RawQuery == "" {
return ""
}
return "?" + r.URL.RawQuery
}