Skip to content

Commit 127d595

Browse files
alnraeneasr
authored andcommitted
feat: emit status code 499 for requests canceled by the client
1 parent fc7f23f commit 127d595

File tree

4 files changed

+60
-0
lines changed

4 files changed

+60
-0
lines changed

clientclosed.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright © 2023 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package herodot
5+
6+
// StatusClientClosedRequest (reported as 499 Client Closed Request) is a faux
7+
// but de-facto standard HTTP status code first used by nginx, indicating the
8+
// client canceled the request. Because the client canceled, it is never
9+
// actually reported back to them. 499 is useful purely in logging, tracing,
10+
// etc.
11+
//
12+
// http://nginx.org/en/docs/dev/development_guide.html
13+
const StatusClientClosedRequest int = 499

json.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package herodot
55

66
import (
77
"bytes"
8+
"context"
89
"encoding/json"
910
stderr "errors"
1011
"net/http"
@@ -83,6 +84,10 @@ func (h *JSONWriter) WriteCode(w http.ResponseWriter, r *http.Request, code int,
8384
code = http.StatusOK
8485
}
8586

87+
if errors.Is(r.Context().Err(), context.Canceled) {
88+
code = StatusClientClosedRequest
89+
}
90+
8691
w.Header().Set("Content-Type", "application/json; charset=utf-8")
8792
w.WriteHeader(code)
8893
_, _ = w.Write(bs.Bytes())
@@ -114,6 +119,10 @@ func (h *JSONWriter) WriteErrorCode(w http.ResponseWriter, r *http.Request, code
114119
code = http.StatusInternalServerError
115120
}
116121

122+
if errors.Is(r.Context().Err(), context.Canceled) {
123+
code = StatusClientClosedRequest
124+
}
125+
117126
if !o.noLog {
118127
// All errors land here, so it's a really good idea to do the logging here as well!
119128
h.Reporter.ReportError(r, code, coalesceError(err), "An error occurred while handling a request")

json_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ package herodot
55

66
import (
77
"bytes"
8+
"context"
89
"encoding/json"
910
stderr "errors"
1011
"fmt"
1112
"io"
1213
"net/http"
1314
"net/http/httptest"
1415
"testing"
16+
"time"
1517

1618
"github.com/pkg/errors"
1719
"github.com/stretchr/testify/assert"
@@ -260,3 +262,30 @@ func TestWriteCodeJSONUnescapedHTML(t *testing.T) {
260262
assert.Equal(t, fmt.Sprintf("\"%s\"\n", foo), string(result))
261263
assert.Equal(t, http.StatusOK, resp.StatusCode)
262264
}
265+
266+
func TestCanceledJSON(t *testing.T) {
267+
h := NewJSONWriter(nil)
268+
rec := httptest.NewRecorder()
269+
done := make(chan struct{})
270+
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
271+
defer close(done)
272+
<-r.Context().Done()
273+
h.WriteError(rec, r, errors.New("some unrelated error"))
274+
}))
275+
defer ts.Close()
276+
277+
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
278+
defer cancel()
279+
req, err := http.NewRequestWithContext(ctx, "GET", ts.URL, nil)
280+
require.NoError(t, err)
281+
282+
_, err = ts.Client().Do(req)
283+
require.ErrorIs(t, err, context.DeadlineExceeded)
284+
285+
<-done
286+
resp := rec.Result()
287+
body, err := io.ReadAll(resp.Body)
288+
require.NoError(t, err)
289+
assert.Contains(t, string(body), "some unrelated error")
290+
assert.Equal(t, 499, resp.StatusCode)
291+
}

plain.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package herodot
55

66
import (
7+
"context"
78
"fmt"
89
"net/http"
910

@@ -43,6 +44,10 @@ func (h *TextWriter) WriteCode(w http.ResponseWriter, r *http.Request, code int,
4344
code = http.StatusOK
4445
}
4546

47+
if errors.Is(r.Context().Err(), context.Canceled) {
48+
code = StatusClientClosedRequest
49+
}
50+
4651
w.Header().Set("Content-Type", h.contentType)
4752
w.WriteHeader(code)
4853
fmt.Fprintf(w, "%s", e)
@@ -75,6 +80,10 @@ func (h *TextWriter) WriteErrorCode(w http.ResponseWriter, r *http.Request, code
7580
code = http.StatusInternalServerError
7681
}
7782

83+
if errors.Is(r.Context().Err(), context.Canceled) {
84+
code = StatusClientClosedRequest
85+
}
86+
7887
// All errors land here, so it's a really good idea to do the logging here as well!
7988
h.Reporter.ReportError(r, code, err, "An error occurred while handling a request")
8089

0 commit comments

Comments
 (0)