Files
CLIProxyAPI/internal/api/server_stop_test.go
Luis Pater 759c57c261 fix(api): immediately close connections on stop and guard state access
- Switch HTTP server stop from graceful shutdown to immediate close to prevent hanging on active requests
- Guard listeners and configuration access with dedicated mutexes
- Instantiate service shutdown context with a fresh timeout deadline upon exit

Closes: #6094
2026-09-24 23:31:51 +08:00

90 lines
2.3 KiB
Go

package api
import (
"context"
"net"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestServerStop_ViolentShutdownImmediatelyClosesWithoutContextDeadlineError(t *testing.T) {
gin.SetMode(gin.TestMode)
// Create a listener on a random available port.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
port := ln.Addr().(*net.TCPAddr).Port
cfg := &config.Config{
Host: "127.0.0.1",
Port: port,
}
server := NewServer(cfg, nil, nil, "")
engine := gin.New()
reqStarted := make(chan struct{})
engine.GET("/slow", func(c *gin.Context) {
close(reqStarted)
time.Sleep(2 * time.Second)
c.String(http.StatusOK, "ok")
})
server.server.Handler = engine
// Start the server with the listener.
go func() {
_ = server.server.Serve(ln)
}()
// Start an in-flight request.
reqDone := make(chan error, 1)
go func() {
resp, errGet := http.Get("http://" + ln.Addr().String() + "/slow")
if errGet != nil {
reqDone <- errGet
return
}
defer func() {
if errCloseBody := resp.Body.Close(); errCloseBody != nil {
t.Logf("response body close error: %v", errCloseBody)
}
}()
reqDone <- nil
}()
<-reqStarted
// Call server.Stop with an expired context (simulating the expired shutdown deadline).
// Under violent shutdown (s.server.Close), the server should immediately close in-flight
// connections without waiting and without returning context deadline exceeded.
expiredCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
defer cancel()
stopStart := time.Now()
errStop := server.Stop(expiredCtx)
stopDuration := time.Since(stopStart)
if stopDuration >= time.Second {
t.Fatalf("server.Stop took %v, want immediate violent shutdown (<1s)", stopDuration)
}
if errStop != nil {
t.Fatalf("server.Stop error = %v, want nil for violent shutdown", errStop)
}
// Verify the in-flight request was terminated / interrupted.
select {
case errReq := <-reqDone:
if errReq == nil {
t.Fatal("expected in-flight request to be interrupted/error, but it completed successfully")
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for in-flight request to be interrupted by server.Stop")
}
}