check / check (push) Canceled after 0s
A panic in a JSON route's handler answered 500 with an empty body, because the router's recoverer sits outside the middleware that adds `@meta`. `JSONValidationMiddleware` now wraps the handler in the same recoverer, and answers the panic with the JSON error body. `/status.json` and the health check answered chi's empty 405 to any method they do not take. They now answer with the same JSON 405 as the routes under `/api/v1` and `/ip`. The home page's IP lookup card showed only the answer's `data`. It now shows the whole answer, `@meta` included. A table test checks `@meta` on every JSON route, in its successes and its errors. Model: opus-5-5
118 lines
4.2 KiB
Go
118 lines
4.2 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
)
|
|
|
|
// The error messages the JSON routes answer with when no route matches the
|
|
// path, and when the route does not take the method.
|
|
const (
|
|
notFoundMessage = "Not found"
|
|
methodNotAllowedMessage = "Method not allowed"
|
|
|
|
// jsonRoutesAllow is the Allow header of their 405 answer: every one of
|
|
// them takes GET and HEAD, and no other method.
|
|
jsonRoutesAllow = "GET, HEAD"
|
|
)
|
|
|
|
// The paths of the status figures and the health check, the two JSON routes
|
|
// outside /api/v1 and /ip.
|
|
const (
|
|
statusJSONPath = "/status.json"
|
|
healthCheckPath = "/.well-known/healthcheck.json"
|
|
)
|
|
|
|
// setupRoutes configures the HTTP routes
|
|
func (s *Server) setupRoutes() {
|
|
r := chi.NewRouter()
|
|
|
|
// Middleware
|
|
r.Use(DecodedPathMiddleware)
|
|
// GetHead serves HEAD with the GET handler. It looks the route up by the
|
|
// path DecodedPathMiddleware sets, so it comes after it.
|
|
r.Use(middleware.GetHead)
|
|
r.Use(SecurityHeadersMiddleware)
|
|
r.Use(middleware.RequestID)
|
|
r.Use(RealIPMiddleware(s.trustedProxies))
|
|
r.Use(RequestLoggerMiddleware(s.logger.Logger)) // Structured request logging
|
|
// Recoverer answers a panic on a page. JSONValidationMiddleware recovers a
|
|
// panic on a JSON route itself, so that the answer is JSON with @meta.
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(JSONResponseMiddleware)
|
|
// Timeout must come after JSONResponseMiddleware, which sends its answer
|
|
// when the handler returns and would send a 200 ahead of Timeout's 504 for
|
|
// a handler that wrote nothing.
|
|
const requestTimeout = 30 * time.Second // Increased from 8s for slow queries
|
|
r.Use(middleware.Timeout(requestTimeout))
|
|
|
|
// Routes
|
|
r.Get("/", s.handleIndex())
|
|
r.Get("/status", s.handleStatusHTML())
|
|
|
|
// AS and prefix detail pages
|
|
r.Get("/as/{asn}", s.handleASDetail())
|
|
r.Get("/prefix/{prefix}/{len}", s.handlePrefixDetail())
|
|
r.Get("/prefixlength/{length}", s.handlePrefixLength())
|
|
r.Get("/prefixlength6/{length}", s.handlePrefixLength6())
|
|
|
|
// The pages that list what two of the home page's cards count
|
|
r.Get("/bogons", s.handleBogons())
|
|
r.Get("/multiple-origins", s.handleMultipleOrigins())
|
|
|
|
// The JSON routes answer a path or method they do not take with the JSON
|
|
// error body, as they answer everything else.
|
|
jsonNotFound := func(w http.ResponseWriter, _ *http.Request) {
|
|
s.writeJSONError(w, http.StatusNotFound, notFoundMessage)
|
|
}
|
|
jsonMethodNotAllowed := func(w http.ResponseWriter, _ *http.Request) {
|
|
// chi's own 405 answer sets Allow; one set in its place must too.
|
|
w.Header().Set("Allow", jsonRoutesAllow)
|
|
s.writeJSONError(w, http.StatusMethodNotAllowed, methodNotAllowedMessage)
|
|
}
|
|
|
|
// The status figures and the health check. chi sets one 405 answer for a
|
|
// whole router. These two routes are on the pages' router, whose 405 stays
|
|
// chi's own. So each method they do not take is routed to the JSON 405
|
|
// here.
|
|
r.Get(statusJSONPath, JSONValidationMiddleware(s.handleStatusJSON()).ServeHTTP)
|
|
r.Get(healthCheckPath, JSONValidationMiddleware(s.handleHealthCheck()).ServeHTTP)
|
|
|
|
for _, method := range []string{
|
|
http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete,
|
|
http.MethodOptions, http.MethodTrace, http.MethodConnect,
|
|
} {
|
|
r.MethodFunc(method, statusJSONPath, jsonMethodNotAllowed)
|
|
r.MethodFunc(method, healthCheckPath, jsonMethodNotAllowed)
|
|
}
|
|
|
|
// IP info JSON endpoints (replaces old /ip redirect)
|
|
r.Route("/ip", func(r chi.Router) {
|
|
r.Use(JSONValidationMiddleware)
|
|
r.NotFound(jsonNotFound)
|
|
r.MethodNotAllowed(jsonMethodNotAllowed)
|
|
r.Get("/", s.handleIPInfo()) // Client IP
|
|
// GetHead does not reach this route: mounting the group registers
|
|
// /ip and /ip/ for every method, so HEAD arrives here as HEAD.
|
|
r.Head("/", s.handleIPInfo())
|
|
r.Get("/{addr}", s.handleIPInfo()) // Specified IP
|
|
})
|
|
|
|
// API routes
|
|
r.Route("/api/v1", func(r chi.Router) {
|
|
r.Use(JSONValidationMiddleware)
|
|
r.NotFound(jsonNotFound)
|
|
r.MethodNotAllowed(jsonMethodNotAllowed)
|
|
r.Get("/stats", s.handleStats())
|
|
r.Get("/ip/{ip}", s.handleIPLookup())
|
|
r.Get("/as/{asn}", s.handleASDetailJSON())
|
|
r.Get("/prefix/{prefix}/{len}", s.handlePrefixDetailJSON())
|
|
r.Get("/search/as", s.handleASSearch())
|
|
})
|
|
|
|
s.router = r
|
|
}
|