Skip to content

Commit 80f970f

Browse files
committed
improved error handling
1 parent 09e75b8 commit 80f970f

7 files changed

Lines changed: 61 additions & 31 deletions

File tree

caddy/caddy_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -943,15 +943,15 @@ func TestMaxWaitTime(t *testing.T) {
943943
for range 10 {
944944
go func() {
945945
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10", t)
946-
if statusCode == http.StatusGatewayTimeout {
946+
if statusCode == http.StatusServiceUnavailable {
947947
success.Store(true)
948948
}
949949
wg.Done()
950950
}()
951951
}
952952
wg.Wait()
953953

954-
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
954+
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
955955
}
956956

957957
func TestMaxWaitTimeWorker(t *testing.T) {
@@ -990,23 +990,26 @@ func TestMaxWaitTimeWorker(t *testing.T) {
990990
for range 10 {
991991
go func() {
992992
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10&iteration=1", t)
993-
if statusCode == http.StatusGatewayTimeout {
993+
if statusCode == http.StatusServiceUnavailable {
994994
success.Store(true)
995995
}
996996
wg.Done()
997997
}()
998998
}
999999
wg.Wait()
1000-
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
1000+
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
10011001

10021002
// Fetch metrics
10031003
resp, err := http.Get("http://localhost:2999/metrics")
10041004
require.NoError(t, err, "failed to fetch metrics")
1005-
defer resp.Body.Close()
1005+
t.Cleanup(func() {
1006+
require.NoError(t, resp.Body.Close())
1007+
})
10061008

10071009
// Read and parse metrics
10081010
metrics := new(bytes.Buffer)
10091011
_, err = metrics.ReadFrom(resp.Body)
1012+
require.NoError(t, err)
10101013

10111014
expectedMetrics := `
10121015
# TYPE frankenphp_worker_queue_depth gauge

caddy/module.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package caddy
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"log/slog"
78
"net/http"
@@ -150,6 +151,8 @@ func needReplacement(s string) bool {
150151
return strings.ContainsAny(s, "{}")
151152
}
152153

154+
var errRejected = &frankenphp.ErrRejected{}
155+
153156
// ServeHTTP implements caddyhttp.MiddlewareHandler.
154157
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
155158
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
@@ -192,8 +195,11 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
192195
frankenphp.WithOriginalRequest(&origReq),
193196
frankenphp.WithWorkerName(workerName),
194197
)
198+
if err != nil {
199+
return caddyhttp.Error(http.StatusInternalServerError, err)
200+
}
195201

196-
if err = frankenphp.ServeHTTP(w, fr); err != nil {
202+
if err = frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, errRejected) {
197203
return caddyhttp.Error(http.StatusInternalServerError, err)
198204
}
199205

context.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package frankenphp
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"log/slog"
68
"net/http"
79
"os"
@@ -117,23 +119,23 @@ func (fc *frankenPHPContext) closeContext() {
117119
}
118120

119121
// validate checks if the request should be outright rejected
120-
func (fc *frankenPHPContext) validate() bool {
122+
func (fc *frankenPHPContext) validate() error {
121123
if strings.Contains(fc.request.URL.Path, "\x00") {
122-
fc.rejectBadRequest("Invalid request path")
124+
fc.reject(ErrInvalidRequestPath)
123125

124-
return false
126+
return ErrInvalidRequestPath
125127
}
126128

127129
contentLengthStr := fc.request.Header.Get("Content-Length")
128130
if contentLengthStr != "" {
129131
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
130-
fc.rejectBadRequest("invalid Content-Length header: " + contentLengthStr)
132+
e := fmt.Errorf("%w: %s", ErrInvalidContentLengthHeader, contentLengthStr)
131133

132-
return false
134+
fc.reject(e)
133135
}
134136
}
135137

136-
return true
138+
return nil
137139
}
138140

139141
func (fc *frankenPHPContext) clientHasClosed() bool {
@@ -149,27 +151,27 @@ func (fc *frankenPHPContext) clientHasClosed() bool {
149151
}
150152
}
151153

152-
// reject sends a response with the given status code and message
153-
func (fc *frankenPHPContext) reject(statusCode int, message string) error {
154+
// reject sends a response with the given status code and error
155+
func (fc *frankenPHPContext) reject(err error) {
154156
if fc.isDone {
155-
return nil
157+
return
158+
}
159+
160+
re := &ErrRejected{}
161+
if !errors.As(err, re) {
162+
// Should never happen
163+
panic("only instance of ErrRejected can be passed to reject")
156164
}
157165

158166
rw := fc.responseWriter
159167
if rw != nil {
160-
rw.WriteHeader(statusCode)
161-
_, _ = rw.Write([]byte(message))
168+
rw.WriteHeader(re.status)
169+
_, _ = rw.Write([]byte(err.Error()))
162170

163171
if f, ok := rw.(http.Flusher); ok {
164172
f.Flush()
165173
}
166174
}
167175

168176
fc.closeContext()
169-
170-
return ErrMaxTimeExceeded
171-
}
172-
173-
func (fc *frankenPHPContext) rejectBadRequest(message string) {
174-
fc.reject(http.StatusBadRequest, message)
175177
}

frankenphp.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ var (
5151
ErrRequestContextCreation = errors.New("error during request context creation")
5252
ErrScriptExecution = errors.New("error during PHP script execution")
5353
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")
54-
ErrMaxTimeExceeded = errors.New("max request handling time exceeded")
54+
55+
ErrInvalidRequestPath = ErrRejected{"invalid request path", http.StatusBadRequest}
56+
ErrInvalidContentLengthHeader = ErrRejected{"invalid Content-Length header", http.StatusBadRequest}
57+
ErrMaxWaitTimeExceeded = ErrRejected{"maximum request handling time exceeded", http.StatusServiceUnavailable}
5558

5659
isRunning bool
5760
onServerShutdown []func()
@@ -64,6 +67,15 @@ var (
6467
maxWaitTime time.Duration
6568
)
6669

70+
type ErrRejected struct {
71+
message string
72+
status int
73+
}
74+
75+
func (e ErrRejected) Error() string {
76+
return e.message
77+
}
78+
6779
type syslogLevel int
6880

6981
const (
@@ -332,8 +344,8 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
332344

333345
fc.responseWriter = responseWriter
334346

335-
if !fc.validate() {
336-
return nil
347+
if err := fc.validate(); err != nil {
348+
return err
337349
}
338350

339351
// Detect if a worker is available to handle this request

frankenphp_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,17 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
7878
}
7979

8080
err := frankenphp.Init(initOpts...)
81-
require.Nil(t, err)
81+
require.NoError(t, err)
8282
defer frankenphp.Shutdown()
8383

8484
handler := func(w http.ResponseWriter, r *http.Request) {
8585
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
8686
assert.NoError(t, err)
8787

8888
err = frankenphp.ServeHTTP(w, req)
89-
assert.NoError(t, err)
89+
if err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
90+
assert.Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err))
91+
}
9092
}
9193

9294
var ts *httptest.Server
@@ -109,6 +111,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
109111

110112
func testRequest(req *http.Request, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
111113
t.Helper()
114+
112115
w := httptest.NewRecorder()
113116
handler(w, req)
114117
resp := w.Result()
@@ -988,7 +991,7 @@ func FuzzRequest(f *testing.F) {
988991
// The response status must be 400 if the request path contains null bytes
989992
if strings.Contains(req.URL.Path, "\x00") {
990993
assert.Equal(t, 400, resp.StatusCode)
991-
assert.Contains(t, body, "Invalid request path")
994+
assert.Contains(t, body, "invalid request path")
992995
return
993996
}
994997

threadregular.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ func handleRequestWithRegularPHPThreads(fc *frankenPHPContext) error {
113113
// the request has timed out stalling
114114
metrics.DequeuedRequest()
115115

116-
return fc.reject(504, "Gateway Timeout")
116+
fc.reject(ErrMaxWaitTimeExceeded)
117+
118+
return ErrMaxWaitTimeExceeded
117119
}
118120
}
119121
}

worker.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ func (worker *worker) handleRequest(fc *frankenPHPContext) error {
258258
// the request has timed out stalling
259259
metrics.DequeuedWorkerRequest(worker.name)
260260

261-
return fc.reject(504, "Gateway Timeout")
261+
fc.reject(ErrMaxWaitTimeExceeded)
262+
263+
return ErrMaxWaitTimeExceeded
262264
}
263265
}
264266
}

0 commit comments

Comments
 (0)