Answer 504, not 408, when a request outlasts its time limit (closes #70)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A request past the server's 30-second limit answered 408, which tells the client it was too slow sending its request, and /api/v1/stats answered 500 when its 4-second read ran out. The limit is now chi's middleware.Timeout, which answers 504 and never writes from a second goroutine while the handler is writing. Right after each database read or host name lookup, whatever came back, a handler asks deadlinePassed whether its time limit has passed and, if so, answers 504, with the JSON error body on JSON routes: a cut-off read can return part of its rows without an error. The health check is unchanged. Model: opus-5-5
This commit was merged in pull request #80.
This commit is contained in:
@@ -198,19 +198,24 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
|
||||
}()
|
||||
|
||||
var dbStats database.Stats
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case err = <-errChan:
|
||||
case dbStats = <-statsChan:
|
||||
}
|
||||
if deadlinePassed(ctx) {
|
||||
s.logger.Error("Database stats timeout in status.json")
|
||||
writeJSONError(w, http.StatusRequestTimeout, "Database timeout")
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
case err := <-errChan:
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
// Success
|
||||
}
|
||||
|
||||
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
||||
@@ -231,6 +236,11 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
|
||||
var whoisStats *WHOISStatsInfo
|
||||
if s.asnFetcher != nil {
|
||||
whoisStats = s.getWHOISStats(ctx)
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stats := Stats{
|
||||
@@ -386,7 +396,7 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
// Check if context is already cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
http.Error(w, "Request timeout", http.StatusRequestTimeout)
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
default:
|
||||
@@ -412,18 +422,24 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
}()
|
||||
|
||||
var dbStats database.Stats
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case err = <-errChan:
|
||||
case dbStats = <-statsChan:
|
||||
}
|
||||
if deadlinePassed(ctx) {
|
||||
s.logger.Error("Database stats timeout")
|
||||
// Don't write response here - timeout middleware already handles it
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
case err := <-errChan:
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
// Success
|
||||
}
|
||||
|
||||
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
||||
@@ -462,6 +478,11 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
var whoisStats *WHOISStatsInfo
|
||||
if s.asnFetcher != nil {
|
||||
whoisStats = s.getWHOISStats(ctx)
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate connection duration
|
||||
@@ -613,6 +634,11 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
} else {
|
||||
// It's a hostname - resolve it
|
||||
resolved, err := net.DefaultResolver.LookupHost(ctx, t)
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
response.Errors = append(response.Errors, t+": "+err.Error())
|
||||
|
||||
@@ -634,6 +660,11 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
// Look up each IP
|
||||
for _, ip := range ipsToLookup {
|
||||
ipInfo, err := s.db.GetIPInfoContext(ctx, ip)
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
response.Errors = append(response.Errors, ip+": "+err.Error())
|
||||
|
||||
@@ -642,6 +673,11 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
|
||||
// Do PTR lookup for this IP
|
||||
ptrs, err := net.DefaultResolver.LookupAddr(ctx, ip)
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
if err == nil && len(ptrs) > 0 {
|
||||
// Remove trailing dots from PTR records
|
||||
for i, ptr := range ptrs {
|
||||
@@ -702,6 +738,11 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||
}
|
||||
|
||||
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
|
||||
if deadlinePassed(r.Context()) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||
@@ -760,6 +801,11 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||
prefix := parsed.Masked().String()
|
||||
|
||||
routes, _, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
|
||||
if deadlinePassed(r.Context()) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||
@@ -802,6 +848,11 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
}
|
||||
|
||||
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
http.Error(w, "AS not found", http.StatusNotFound)
|
||||
@@ -815,6 +866,11 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
|
||||
// Get peers
|
||||
peers, err := s.db.GetASPeersContext(r.Context(), asn)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get AS peers", "error", err)
|
||||
// Continue without peers rather than failing the whole request
|
||||
@@ -931,6 +987,11 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
||||
prefix := parsed.Masked().String()
|
||||
|
||||
routes, originASNs, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
http.Error(w, "Prefix not found", http.StatusNotFound)
|
||||
@@ -1079,6 +1140,11 @@ func (s *Server) handlePrefixLength() http.HandlerFunc {
|
||||
// Get random sample of prefixes
|
||||
const maxPrefixes = 500
|
||||
prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -1170,6 +1236,11 @@ func (s *Server) handlePrefixLength6() http.HandlerFunc {
|
||||
// Get random sample of prefixes
|
||||
const maxPrefixes = 500
|
||||
prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
Reference in New Issue
Block a user