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
The hand-written time limit middleware answered 408 and wrote that answer from its own goroutine while the handler could still be writing. It is replaced by chi's middleware.Timeout, placed after JSONResponseMiddleware so its 504 is not lost behind a 200. After a database read or other lookup, a handler asks deadlinePassed whether the request's time limit passed and, if so, answers 504 instead of its 500, 404 or partial result: the AS and prefix reads, cut off while reading rows, return part of them, or none, without an error. JSONValidationMiddleware answers 504 with the JSON error body when the limit has passed and the handler wrote nothing. status.json and /api/v1/stats answer 504 when their 4-second database read runs out. Model: opus-5-5
This commit is contained in:
@@ -201,12 +201,16 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.logger.Error("Database stats timeout in status.json")
|
||||
writeJSONError(w, http.StatusRequestTimeout, "Database timeout")
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
case err := <-errChan:
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
} else {
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
@@ -386,7 +390,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:
|
||||
@@ -415,11 +419,16 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
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:
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
} else {
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
@@ -622,6 +631,13 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// The time limit passed while resolving host names.
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(ipsToLookup) == 0 {
|
||||
writeJSONError(w, http.StatusBadRequest, "No valid IPs or hostnames provided")
|
||||
|
||||
@@ -664,6 +680,13 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// The time limit passed while looking up the IPs.
|
||||
if deadlinePassed(ctx) {
|
||||
writeJSONError(w, http.StatusGatewayTimeout, requestTimeoutMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Return response (even if no results, include errors)
|
||||
if len(response.Results) == 0 && len(response.Errors) > 0 {
|
||||
writeJSONError(w, http.StatusNotFound, "No routes found: "+response.Errors[0])
|
||||
@@ -702,6 +725,13 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||
}
|
||||
|
||||
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
|
||||
// A read cut off by the time limit can return part of its rows, or
|
||||
// none, without an error.
|
||||
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 +790,13 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||
prefix := parsed.Masked().String()
|
||||
|
||||
routes, _, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
|
||||
// A read cut off by the time limit can return part of its rows, or
|
||||
// none, without an error.
|
||||
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 +839,13 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
}
|
||||
|
||||
asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
|
||||
// A read cut off by the time limit can return part of its rows, or
|
||||
// none, without an error.
|
||||
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)
|
||||
@@ -931,6 +975,13 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
||||
prefix := parsed.Masked().String()
|
||||
|
||||
routes, originASNs, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
|
||||
// A read cut off by the time limit can return part of its rows, or
|
||||
// none, without an error.
|
||||
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)
|
||||
@@ -1080,8 +1131,12 @@ func (s *Server) handlePrefixLength() http.HandlerFunc {
|
||||
const maxPrefixes = 500
|
||||
prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
} else {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1171,8 +1226,12 @@ func (s *Server) handlePrefixLength6() http.HandlerFunc {
|
||||
const maxPrefixes = 500
|
||||
prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
if deadlinePassed(r.Context()) {
|
||||
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
|
||||
} else {
|
||||
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user