From 745dd6d539dc0af2dc44d0e78b3622d85724a9a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Apr 2026 16:46:02 +0000 Subject: [PATCH 1/2] Fix server crashes, add timeouts, graceful shutdown, and fix Dockerfile Bug fixes: - Remove check() function that called log.Fatal() in request handlers, crashing the entire server on any error (e.g. missing template file) - Fix unsafe type assertion in GetMyOutboundIP() that could panic - Return errors from GetMyOutboundIP() instead of calling log.Fatal() Performance: - Cache outbound IP at startup instead of dialing UDP on every request - Parse HTML template once at startup instead of on every request Reliability: - Add read/write/idle timeouts to HTTP server to prevent slow-client attacks - Add graceful shutdown on SIGINT/SIGTERM with 15s drain timeout - Deduplicate color-coded request logging into logRequest() helper Dockerfile fixes: - Fix build command to compile entire module (was missing utils package) - Fix EXPOSE port from 8000 to 8081 to match actual listen port - Copy html/ directory into runtime image for /html/ endpoint - Set WORKDIR to / so relative paths resolve correctly Also bumps Go version from 1.20 to 1.21 in go.mod. https://claude.ai/code/session_01GkWCj6JKLSqEtnkBpPoFBL --- Dockerfile | 19 +++---- go.mod | 2 +- simple_http_server.go | 127 ++++++++++++++++++++++++++++-------------- utils/iputils.go | 13 +++-- 4 files changed, 102 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index b17c755..087e8eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,21 +13,18 @@ WORKDIR /build COPY . . # Build the application -RUN go build -o simple_http_server ./simple_http_server.go - - -# Move to /dist directory as the place for resulting binary folder -WORKDIR /dist - -# Copy binary from build to main folder -RUN cp /build/simple_http_server . - -EXPOSE 8000 +RUN go build -o simple_http_server . # Build a small image FROM alpine RUN apk update && apk add curl -COPY --from=builder /dist/simple_http_server / + +COPY --from=builder /build/simple_http_server /simple_http_server +COPY --from=builder /build/html /html + +WORKDIR / + +EXPOSE 8081 HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1:8081/healthcheck || exit 1 ENTRYPOINT ["/simple_http_server"] diff --git a/go.mod b/go.mod index b5abeea..09cd014 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module pareshpawar.com/simple-http-server -go 1.20 +go 1.21 require github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be diff --git a/simple_http_server.go b/simple_http_server.go index 495952c..9a28179 100644 --- a/simple_http_server.go +++ b/simple_http_server.go @@ -1,63 +1,115 @@ package main import ( + "context" "fmt" "html/template" "log" "net/http" "os" + "os/signal" + "syscall" "time" "github.com/common-nighthawk/go-figure" "pareshpawar.com/simple-http-server/utils" ) +var ( + cachedOutboundIP string + indexTemplate *template.Template +) + func main() { + // Resolve outbound IP once at startup + ip, err := utils.GetMyOutboundIP() + if err != nil { + log.Printf("Warning: could not determine outbound IP: %v", err) + cachedOutboundIP = "unavailable" + } else { + cachedOutboundIP = ip.String() + } + + // Parse HTML template once at startup + file, err := os.ReadFile("html/index.html") + if err != nil { + log.Printf("Warning: could not read html/index.html: %v (the /html/ endpoint will be unavailable)", err) + } else { + tmpl, err := template.New("webpage").Parse(string(file)) + if err != nil { + log.Printf("Warning: could not parse html/index.html: %v (the /html/ endpoint will be unavailable)", err) + } else { + indexTemplate = tmpl + } + } + http.HandleFunc("/", handler) http.HandleFunc("/html/", htmlhandler) http.HandleFunc("/healthcheck", healthhandler) + serverBrand := figure.NewColorFigure("Simple HTTP Server", "straight", "green", true) serverBrand.Print() myBrand := figure.NewColorFigure("by PareshPawar.com", "term", "green", true) myBrand.Print() log.Print("pareshpawar/simple-http-server: Simple HTTP Server Running on port 8081") - log.Fatal(http.ListenAndServe("0.0.0.0:8081", nil)) -} -func check(err error) { - if err != nil { - log.Fatal(err) + srv := &http.Server{ + Addr: "0.0.0.0:8081", + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, } + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown:", err) + } + log.Println("Server exited") } -func htmlhandler(w http.ResponseWriter, r *http.Request) { - timestamp := time.Now() - if r.Method == "GET" { - fmt.Print(string("\033[34m")) - } else if r.Method == "POST" { - fmt.Print(string("\033[33m")) - } else if r.Method == "PUT" { - fmt.Print(string("\033[35m")) - } else if r.Method == "DELETE" { - fmt.Print(string("\033[31m")) - } else { - fmt.Print(string("\033[36m")) +func logRequest(r *http.Request) { + colors := map[string]string{ + "GET": "\033[34m", + "POST": "\033[33m", + "PUT": "\033[35m", + "DELETE": "\033[31m", } - fmt.Printf("%s %s %s %s ===> from %s\n", timestamp.Local(), r.Method, r.URL, r.Proto, r.RemoteAddr) + color, ok := colors[r.Method] + if !ok { + color = "\033[36m" + } + fmt.Printf("%s%s %s %s %s ===> from %s\033[0m\n", + color, time.Now().Local(), r.Method, r.URL, r.Proto, r.RemoteAddr) +} - file, err := os.ReadFile("html/index.html") - check(err) +func htmlhandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) - template, err := template.New("webpage").Parse(string(file)) - check(err) + if indexTemplate == nil { + http.Error(w, "HTML template not available", http.StatusInternalServerError) + return + } + timestamp := time.Now() type reqDataStruct struct{ ReqTime, ReqType, Host, Remote, RemoteAddr string } reqData := reqDataStruct{ ReqTime: timestamp.String(), ReqType: r.Proto + " " + r.Method + " " + r.URL.Path, Host: r.Host, Remote: r.RemoteAddr, - RemoteAddr: utils.GetMyOutboundIP().String(), + RemoteAddr: cachedOutboundIP, } data := struct { @@ -68,29 +120,20 @@ func htmlhandler(w http.ResponseWriter, r *http.Request) { Headers: r.Header, } - err = template.Execute(w, data) - check(err) + if err := indexTemplate.Execute(w, data); err != nil { + log.Printf("Error executing template: %v", err) + } } func handler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + timestamp := time.Now() - if r.Method == "GET" { - fmt.Print(string("\033[34m")) - } else if r.Method == "POST" { - fmt.Print(string("\033[33m")) - } else if r.Method == "PUT" { - fmt.Print(string("\033[35m")) - } else if r.Method == "DELETE" { - fmt.Print(string("\033[31m")) - } else { - fmt.Print(string("\033[36m")) - } - fmt.Printf("%s %s %s %s ===> from %s\n", timestamp.Local(), r.Method, r.URL, r.Proto, r.RemoteAddr) - fmt.Fprintf(w, "Request Time ==> %s\n", timestamp) - fmt.Fprintf(w, "Request Type ==> %s %s %s\n", r.Method, r.URL, r.Proto) - fmt.Fprintf(w, "Hostname/Host ==> %s\n", r.Host) - fmt.Fprintf(w, "Remote Address ==> %s\n", r.RemoteAddr) - fmt.Fprintf(w, "Local Address ==> %s\n\n", utils.GetMyOutboundIP()) + fmt.Fprintf(w, "Request Time\t==> %s\n", timestamp) + fmt.Fprintf(w, "Request Type\t==> %s %s %s\n", r.Method, r.URL, r.Proto) + fmt.Fprintf(w, "Hostname/Host \t==> %s\n", r.Host) + fmt.Fprintf(w, "Remote Address \t==> %s\n", r.RemoteAddr) + fmt.Fprintf(w, "Local Address \t==> %s\n\n", cachedOutboundIP) // print request headers for key, value := range r.Header { diff --git a/utils/iputils.go b/utils/iputils.go index 2e0af55..4bf7772 100644 --- a/utils/iputils.go +++ b/utils/iputils.go @@ -1,16 +1,19 @@ package utils import ( - "log" + "fmt" "net" ) -func GetMyOutboundIP() net.IP { +func GetMyOutboundIP() (net.IP, error) { conn, err := net.Dial("udp", "1.1.1.1:80") if err != nil { - log.Fatal(err) + return nil, err } defer conn.Close() - localAddr := conn.LocalAddr().(*net.UDPAddr) - return localAddr.IP + localAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok { + return nil, fmt.Errorf("unexpected address type: %T", conn.LocalAddr()) + } + return localAddr.IP, nil } From 42628d405fd1d57c986db0a5401d16b4c0baedc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Apr 2026 16:46:42 +0000 Subject: [PATCH 2/2] Add compiled binary to .gitignore The Go build produces an extensionless binary on Linux which wasn't covered by the existing *.exe/*.dll/*.so patterns. https://claude.ai/code/session_01GkWCj6JKLSqEtnkBpPoFBL --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3b735ec..be2fc69 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +simple-http-server # Test binary, built with `go test -c` *.test