Skip to content

Stop logging phantom 500s and cap the catalog refresh retries - #3

Merged
puppe1990 merged 1 commit into
mainfrom
fix/phantom-500-and-refresh-retry
Sep 10, 2026
Merged

Stop logging phantom 500s and cap the catalog refresh retries#3
puppe1990 merged 1 commit into
mainfrom
fix/phantom-500-and-refresh-retry

Conversation

@puppe1990

Copy link
Copy Markdown
Owner

Problema 1 — os 123 "500" em /u/{login}/all nunca foram erro de servidor

Os logs de produção mostravam 123 respostas 500 (todas nessa rota, a mais pesada), sempre acompanhadas de:

http: superfluous response.WriteHeader call from ...middleware.(*statusRecorder).WriteHeader

Investigando o caminho de render:

  • gonertia executa o root template direto no ResponseWriter (i.rootTemplate.Execute(w, templateData)), e setHTMLResponse só seta o Content-Type — o status é gravado implicitamente no primeiro write.
  • Quando o cliente aborta no meio do download, esse write falha e Execute retorna erro com o status já comprometido.
  • O handler então chamava http.Error, que o net/http rejeita como WriteHeader supérfluo: loga um 500 fantasma e ainda concatena o texto do erro no HTML parcial.

Não é erro de dado. O app.html só interpola {{ .inertia }} e {{ .inertiaHead }}, ambos template.HTML pré-renderizados — o template não tem como falhar na execução. Logo, todo erro ali é erro de escrita. Corroborando: os mesmos IPs aparecem com 200 e 500 na mesma rota, e os 500 terminam antes (mediana 3,97s) que os 200 (6,23s) — assinatura de cliente que desiste no meio.

Correção: o handler rastreia se a resposta já começou; se começou, loga o erro em vez de reescrever o status.

Problema 2 — PerformRefreshCatalog com o mesmo padrão de retry

  • Percorria a watch list inteira depois de um erro.
  • Deixava rate limit falhar o job, queimando as 3 tentativas em segundos contra uma janela de até 1 hora.

Agora para no primeiro erro e reusa a política compartilhada de rate limit (skipRateLimited, extraída para internal/jobs/rate_limit.go).

Testes (TDD, escritos antes)

  • TestHomeHandler_AbortedClientDoesNotRewriteHeader — com o código antigo falhava com statuses = [200 500], reproduzindo exatamente o superfluous de produção.
  • TestPerformRefreshCatalog_StopsOnFirstError — 3 logins falhando geravam 3 chamadas; agora 1.
  • TestPerformRefreshCatalog_RateLimitedIsSkipped — rate limit não marca o job como failed.

make ci local: race tests, golangci-lint (0 issues) e Prettier verdes.

Two leftovers from the traffic rate-limit incident.

The 123 "500" responses on /u/{login}/all were never server errors. Inertia
executes the root template straight into the response writer, so when a
client aborts mid-page the write fails with the status already committed.
The handler then called http.Error, which net/http rejects as a superfluous
WriteHeader, logged a phantom 500, and appended error text to the partial
HTML. Rendering cannot fail on data here: the template only interpolates two
pre-rendered template.HTML values, so every one of those failures was a
write error. The handler now tracks whether the response started and logs
the failure instead of rewriting the status when it did.

PerformRefreshCatalog still had the old shape: it walked the entire watch
list after an error and let a rate limit fail the job outright, so the
worker burned its 3 attempts in seconds against an hour-long window. It now
stops at the first error and reuses the shared rate-limit policy.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@puppe1990
puppe1990 merged commit da0e3a7 into main Sep 10, 2026
4 checks passed
@puppe1990
puppe1990 deleted the fix/phantom-500-and-refresh-retry branch September 10, 2026 19:25
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.

1 participant