Skip to content

Commit 47d9ed1

Browse files
committed
Harden server lifecycle and require HTTP server config
1 parent 0d67470 commit 47d9ed1

File tree

15 files changed

+353
-224
lines changed

15 files changed

+353
-224
lines changed

config/config.go

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,17 @@ func (c contextKey) String() string {
2525
const (
2626
ctxKeyConfiguration = contextKey("configurationKey")
2727
httpStatusOKClass = 2
28+
bytesPerKiB = 1024
2829
// DefaultSlowQueryThresholdMilliseconds is defined in datastore_logger.go.
2930

30-
DefaultSlowQueryThreshold = 200 * time.Millisecond
31+
DefaultSlowQueryThreshold = 200 * time.Millisecond
32+
defaultHTTPReadTimeout = 30 * time.Second
33+
defaultHTTPReadHeaderTimeout = 5 * time.Second
34+
defaultHTTPWriteTimeout = 30 * time.Second
35+
defaultHTTPIdleTimeout = 90 * time.Second
36+
defaultHTTPShutdownTimeout = 15 * time.Second
37+
defaultHTTPMaxHeaderKilobytes = 1024
38+
defaultHTTPMaxHeaderBytes = defaultHTTPMaxHeaderKilobytes * bytesPerKiB
3139
)
3240

3341
// ToContext adds service configuration to the current supplied context.
@@ -77,6 +85,17 @@ func FillEnv(v any) error {
7785
return env.Parse(v)
7886
}
7987

88+
func parseDurationOrDefault(value string, fallback time.Duration) time.Duration {
89+
if value != "" {
90+
duration, err := time.ParseDuration(value)
91+
if err == nil {
92+
return duration
93+
}
94+
}
95+
96+
return fallback
97+
}
98+
8099
type ConfigurationDefault struct {
81100
LogLevel string `envDefault:"info" env:"LOG_LEVEL" yaml:"log_level"`
82101
LogFormat string `envDefault:"info" env:"LOG_FORMAT" yaml:"log_format"`
@@ -106,9 +125,15 @@ type ConfigurationDefault struct {
106125
DebugEndpointsEnabledValue bool `envDefault:"false" env:"FRAME_DEBUG_ENDPOINTS" yaml:"frame_debug_endpoints"`
107126
DebugEndpointsBasePathValue string `envDefault:"/debug/frame" env:"FRAME_DEBUG_ENDPOINTS_BASEPATH" yaml:"frame_debug_endpoints_basepath"`
108127

109-
ServerPort string `envDefault:":7000" env:"PORT" yaml:"server_port"`
110-
HTTPServerPort string `envDefault:":8080" env:"HTTP_PORT" yaml:"http_server_port"`
111-
GrpcServerPort string `envDefault:":50051" env:"GRPC_PORT" yaml:"grpc_server_port"`
128+
ServerPort string `envDefault:":7000" env:"PORT" yaml:"server_port"`
129+
HTTPServerPort string `envDefault:":8080" env:"HTTP_PORT" yaml:"http_server_port"`
130+
131+
HTTPServerReadTimeout string `envDefault:"30s" env:"HTTP_SERVER_READ_TIMEOUT" yaml:"http_server_read_timeout"`
132+
HTTPServerReadHeaderTimeout string `envDefault:"5s" env:"HTTP_SERVER_READ_HEADER_TIMEOUT" yaml:"http_server_read_header_timeout"`
133+
HTTPServerWriteTimeout string `envDefault:"30s" env:"HTTP_SERVER_WRITE_TIMEOUT" yaml:"http_server_write_timeout"`
134+
HTTPServerIdleTimeout string `envDefault:"90s" env:"HTTP_SERVER_IDLE_TIMEOUT" yaml:"http_server_idle_timeout"`
135+
HTTPServerShutdownTimeout string `envDefault:"15s" env:"HTTP_SERVER_SHUTDOWN_TIMEOUT" yaml:"http_server_shutdown_timeout"`
136+
HTTPServerMaxHeaderBytes int `envDefault:"1024" env:"HTTP_SERVER_MAX_HEADER_KB" yaml:"http_server_max_header_kb"`
112137

113138
// Worker pool settings
114139
WorkerPoolCPUFactorForWorkerCount int `envDefault:"10" env:"WORKER_POOL_CPU_FACTOR_FOR_WORKER_COUNT" yaml:"worker_pool_cpu_factor_for_worker_count"`
@@ -292,7 +317,6 @@ func (c *ConfigurationDefault) ProfilerPort() string {
292317
type ConfigurationPorts interface {
293318
Port() string
294319
HTTPPort() string
295-
GrpcPort() string
296320
}
297321

298322
var _ ConfigurationPorts = new(ConfigurationDefault)
@@ -321,18 +345,6 @@ func (c *ConfigurationDefault) HTTPPort() string {
321345
return ":8080"
322346
}
323347

324-
func (c *ConfigurationDefault) GrpcPort() string {
325-
if i, err := strconv.Atoi(c.GrpcServerPort); err == nil && i > 0 {
326-
return fmt.Sprintf(":%s", strings.TrimSpace(c.GrpcServerPort))
327-
}
328-
329-
if strings.HasPrefix(c.GrpcServerPort, ":") || strings.Contains(c.GrpcServerPort, ":") {
330-
return c.GrpcServerPort
331-
}
332-
333-
return c.Port()
334-
}
335-
336348
type ConfigurationTelemetry interface {
337349
DisableOpenTelemetry() bool
338350
SamplingRatio() float64
@@ -380,6 +392,45 @@ func (c *ConfigurationDefault) GetExpiryDuration() time.Duration {
380392
return time.Second
381393
}
382394

395+
type ConfigurationHTTPServer interface {
396+
HTTPReadTimeout() time.Duration
397+
HTTPReadHeaderTimeout() time.Duration
398+
HTTPWriteTimeout() time.Duration
399+
HTTPIdleTimeout() time.Duration
400+
HTTPShutdownTimeout() time.Duration
401+
HTTPMaxHeaderBytes() int
402+
}
403+
404+
var _ ConfigurationHTTPServer = new(ConfigurationDefault)
405+
406+
func (c *ConfigurationDefault) HTTPReadTimeout() time.Duration {
407+
return parseDurationOrDefault(c.HTTPServerReadTimeout, defaultHTTPReadTimeout)
408+
}
409+
410+
func (c *ConfigurationDefault) HTTPReadHeaderTimeout() time.Duration {
411+
return parseDurationOrDefault(c.HTTPServerReadHeaderTimeout, defaultHTTPReadHeaderTimeout)
412+
}
413+
414+
func (c *ConfigurationDefault) HTTPWriteTimeout() time.Duration {
415+
return parseDurationOrDefault(c.HTTPServerWriteTimeout, defaultHTTPWriteTimeout)
416+
}
417+
418+
func (c *ConfigurationDefault) HTTPIdleTimeout() time.Duration {
419+
return parseDurationOrDefault(c.HTTPServerIdleTimeout, defaultHTTPIdleTimeout)
420+
}
421+
422+
func (c *ConfigurationDefault) HTTPShutdownTimeout() time.Duration {
423+
return parseDurationOrDefault(c.HTTPServerShutdownTimeout, defaultHTTPShutdownTimeout)
424+
}
425+
426+
func (c *ConfigurationDefault) HTTPMaxHeaderBytes() int {
427+
if c.HTTPServerMaxHeaderBytes > 0 {
428+
return c.HTTPServerMaxHeaderBytes * bytesPerKiB
429+
}
430+
431+
return defaultHTTPMaxHeaderBytes
432+
}
433+
383434
type ConfigurationOAUTH2 interface {
384435
LoadOauth2Config(ctx context.Context) error
385436
GetOauth2WellKnownOIDC() string

config/config_test.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ func (s *ConfigSuite) TestFallbacksTable() {
9898
cfg ConfigurationDefault
9999
wantPort string
100100
wantHTTP string
101-
wantGRPC string
102101
wantProfile string
103102
wantExpiry time.Duration
104103
}{
@@ -107,13 +106,11 @@ func (s *ConfigSuite) TestFallbacksTable() {
107106
cfg: ConfigurationDefault{
108107
ServerPort: "7000",
109108
HTTPServerPort: "8080",
110-
GrpcServerPort: "50051",
111109
ProfilerPortAddr: ":6600",
112110
WorkerPoolExpiryDuration: "1500ms",
113111
},
114112
wantPort: ":7000",
115113
wantHTTP: ":8080",
116-
wantGRPC: ":50051",
117114
wantProfile: ":6600",
118115
wantExpiry: 1500 * time.Millisecond,
119116
},
@@ -122,13 +119,11 @@ func (s *ConfigSuite) TestFallbacksTable() {
122119
cfg: ConfigurationDefault{
123120
ServerPort: "invalid",
124121
HTTPServerPort: "invalid",
125-
GrpcServerPort: "invalid",
126122
ProfilerPortAddr: "",
127123
WorkerPoolExpiryDuration: "invalid",
128124
},
129125
wantPort: ":80",
130126
wantHTTP: ":8080",
131-
wantGRPC: ":80",
132127
wantProfile: ":6060",
133128
wantExpiry: time.Second,
134129
},
@@ -137,12 +132,10 @@ func (s *ConfigSuite) TestFallbacksTable() {
137132
cfg: ConfigurationDefault{
138133
ServerPort: "0.0.0.0:7000",
139134
HTTPServerPort: ":8088",
140-
GrpcServerPort: "127.0.0.1:50052",
141135
WorkerPoolExpiryDuration: "1s",
142136
},
143137
wantPort: "0.0.0.0:7000",
144138
wantHTTP: ":8088",
145-
wantGRPC: "127.0.0.1:50052",
146139
wantProfile: ":6060",
147140
wantExpiry: time.Second,
148141
},
@@ -152,13 +145,44 @@ func (s *ConfigSuite) TestFallbacksTable() {
152145
s.Run(tc.name, func() {
153146
s.Equal(tc.wantPort, tc.cfg.Port())
154147
s.Equal(tc.wantHTTP, tc.cfg.HTTPPort())
155-
s.Equal(tc.wantGRPC, tc.cfg.GrpcPort())
156148
s.Equal(tc.wantProfile, tc.cfg.ProfilerPort())
157149
s.Equal(tc.wantExpiry, tc.cfg.GetExpiryDuration())
158150
})
159151
}
160152
}
161153

154+
func (s *ConfigSuite) TestHTTPServerSettings() {
155+
cfg := &ConfigurationDefault{
156+
HTTPServerReadTimeout: "31s",
157+
HTTPServerReadHeaderTimeout: "6s",
158+
HTTPServerWriteTimeout: "32s",
159+
HTTPServerIdleTimeout: "91s",
160+
HTTPServerShutdownTimeout: "16s",
161+
HTTPServerMaxHeaderBytes: 2048,
162+
}
163+
164+
s.Equal(31*time.Second, cfg.HTTPReadTimeout())
165+
s.Equal(6*time.Second, cfg.HTTPReadHeaderTimeout())
166+
s.Equal(32*time.Second, cfg.HTTPWriteTimeout())
167+
s.Equal(91*time.Second, cfg.HTTPIdleTimeout())
168+
s.Equal(16*time.Second, cfg.HTTPShutdownTimeout())
169+
s.Equal(2048*1024, cfg.HTTPMaxHeaderBytes())
170+
171+
cfg.HTTPServerReadTimeout = "invalid"
172+
cfg.HTTPServerReadHeaderTimeout = "invalid"
173+
cfg.HTTPServerWriteTimeout = "invalid"
174+
cfg.HTTPServerIdleTimeout = "invalid"
175+
cfg.HTTPServerShutdownTimeout = "invalid"
176+
cfg.HTTPServerMaxHeaderBytes = 0
177+
178+
s.Equal(30*time.Second, cfg.HTTPReadTimeout())
179+
s.Equal(5*time.Second, cfg.HTTPReadHeaderTimeout())
180+
s.Equal(30*time.Second, cfg.HTTPWriteTimeout())
181+
s.Equal(90*time.Second, cfg.HTTPIdleTimeout())
182+
s.Equal(15*time.Second, cfg.HTTPShutdownTimeout())
183+
s.Equal(1024*1024, cfg.HTTPMaxHeaderBytes())
184+
}
185+
162186
func (s *ConfigSuite) TestDatabaseAndEventConfig() {
163187
cfg := &ConfigurationDefault{
164188
DatabasePrimaryURL: []string{"postgres://primary"},

docs/architecture.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Frame is a fast, extensible Go framework built around a minimal core `Service` a
99

1010
## Mental Model
1111

12-
Frame bootstraps a `Service` that owns shared runtime state and managers. Options configure the service and register startup hooks. The service then starts HTTP/gRPC servers, background workers, and pluggable components.
12+
Frame bootstraps a `Service` that owns shared runtime state and managers. Options configure the service and register startup hooks. The service then starts the HTTP server, background workers, and pluggable components.
1313

1414
```
1515
ctx, svc := frame.NewService(
@@ -49,15 +49,16 @@ Run
4949
-> validate startup errors
5050
-> initialize queue manager
5151
-> start background consumer (if configured)
52-
-> start HTTP/gRPC server(s)
52+
-> start HTTP server
5353
-> start profiler (if enabled)
5454
-> execute startup hooks
5555
-> block until shutdown or error
5656
5757
Stop
58+
-> drain HTTP server
5859
-> stop profiler
59-
-> cancel service context
6060
-> run cleanup hooks
61+
-> cancel service context
6162
```
6263

6364
## Extension Points (Plugin Architecture)

docs/examples.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ These examples are small, compilable, and use the canonical Frame bootstrap patt
77
- Path: `examples/http-basic`
88
- Run: `go run ./examples/http-basic`
99

10-
## gRPC Basic
11-
12-
- Path: `examples/grpc-basic`
13-
- Run: `go run ./examples/grpc-basic`
14-
1510
## Queue Basic
1611

1712
- Path: `examples/queue-basic`

docs/getting-started.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,16 @@ import (
6666
"net/http"
6767

6868
"github.com/pitabwire/frame"
69-
"google.golang.org/grpc"
7069
)
7170

7271
func main() {
73-
grpcServer := grpc.NewServer()
74-
7572
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
7673
w.Write([]byte("HTTP ok"))
7774
})
7875

7976
ctx, svc := frame.NewService(
8077
frame.WithName("hello"),
8178
frame.WithHTTPHandler(http.DefaultServeMux),
82-
frame.WithGRPCServer(grpcServer),
83-
frame.WithGRPCPort(":50051"),
8479
)
8580

8681
if err := svc.Run(ctx, ":8080"); err != nil {

docs/server.md

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# HTTP and gRPC Server
1+
# HTTP Server
22

3-
Frame provides an HTTP server with optional gRPC sidecar support. The server layer is configured via `Service` options and environment-based config interfaces.
3+
Frame provides an HTTP server with configurable lifecycle and transport limits. The server layer is configured via `Service` options and environment-based config interfaces.
44

55
## HTTP Server Basics
66

@@ -40,24 +40,6 @@ _ = svc.Run(ctx, ":8080")
4040
- Default path: `/healthz`
4141
- Register custom checks using `AddHealthCheck`.
4242

43-
## gRPC Server
44-
45-
```go
46-
grpcServer := grpc.NewServer()
47-
48-
ctx, svc := frame.NewService(
49-
frame.WithHTTPHandler(http.DefaultServeMux),
50-
frame.WithGRPCServer(grpcServer),
51-
frame.WithGRPCPort(":50051"),
52-
)
53-
54-
_ = svc.Run(ctx, ":8080")
55-
```
56-
57-
Optional:
58-
- `WithEnableGRPCServerReflection()`
59-
- `WithGRPCServerListener(listener net.Listener)`
60-
6143
## HTTP/2 Support
6244

6345
Frame configures HTTP/2 support automatically:
@@ -82,12 +64,13 @@ type ServerDriver interface {
8264

8365
You can inject a custom driver with `WithDriver` for testing or custom transport.
8466

85-
## Default HTTP Timeouts
86-
87-
These defaults are hard-coded in `service.go`:
67+
## Default HTTP Limits
8868

89-
- Read timeout: 5s
90-
- Write timeout: 10s
91-
- Idle timeout: 90s
69+
These defaults are configurable via `config.ConfigurationHTTPServer` and environment variables:
9270

93-
Adjust by supplying a custom driver.
71+
- `HTTP_SERVER_READ_TIMEOUT=30s`
72+
- `HTTP_SERVER_READ_HEADER_TIMEOUT=5s`
73+
- `HTTP_SERVER_WRITE_TIMEOUT=30s`
74+
- `HTTP_SERVER_IDLE_TIMEOUT=90s`
75+
- `HTTP_SERVER_SHUTDOWN_TIMEOUT=15s`
76+
- `HTTP_SERVER_MAX_HEADER_KB=1024`

docs/service.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ctx, svc := frame.NewServiceWithContext(ctx, frame.WithName("orders"))
2222
## Service Lifecycle
2323

2424
- `NewService` applies options and initializes core managers.
25-
- `Run` starts HTTP/gRPC servers, background processing, and startup hooks.
25+
- `Run` starts the HTTP server, background processing, and startup hooks.
2626
- `Stop` executes cleanup and shuts down gracefully.
2727

2828
### Startup Hooks (Ordering Matters)
@@ -65,11 +65,7 @@ Service options are composable and can be applied at construction or later via `
6565

6666
- `WithHTTPHandler(h http.Handler)`
6767
- `WithHTTPMiddleware(mw ...func(http.Handler) http.Handler)`
68-
- `WithGRPCServer(grpcServer *grpc.Server)`
69-
- `WithGRPCPort(port string)`
70-
- `WithEnableGRPCServerReflection()`
71-
- `WithGRPCServerListener(listener net.Listener)`
72-
- `WithDriver(driver ServerDriver)`
68+
- `WithDriver(driver server.Driver)`
7369

7470
### Configuration and Logging
7571

@@ -133,4 +129,3 @@ Health checks are registered using `AddHealthCheck`, and are served on `/healthz
133129

134130
- Startup errors are collected via `AddStartupError` and returned when `Run` is called.
135131
- `ErrorIsNotFound(err)` helps normalize "not found" checks across data, gRPC, and HTTP.
136-

examples/grpc-basic/README.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)