Skip to content

Commit c7f7745

Browse files
committed
feat: return header for error ID
1 parent 3a4a6e7 commit c7f7745

File tree

5 files changed

+123
-1
lines changed

5 files changed

+123
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ _book/
1515
dist/
1616
coverage.*
1717
.bin
18+
.claude/

json.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ type ErrorContainer struct {
1717
Error *DefaultError `json:"error"`
1818
}
1919

20+
func (e *ErrorContainer) ID() string {
21+
return e.Error.ID()
22+
}
23+
2024
type ErrorReporter interface {
2125
ReportError(r *http.Request, code int, err error, args ...interface{})
2226
}
@@ -159,13 +163,15 @@ func (h *JSONWriter) WriteErrorCode(w http.ResponseWriter, r *http.Request, code
159163
}
160164

161165
w.Header().Set("Content-Type", "application/json")
162-
w.WriteHeader(code)
163166

164167
// Enhancing must happen after logging or context will be lost.
165168
var payload interface{} = err
166169
if h.ErrorEnhancer != nil {
167170
payload = h.ErrorEnhancer(r, err)
168171
}
172+
if id, ok := payload.(interface{ ID() string }); ok {
173+
w.Header().Set("Ory-Error-Id", id.ID())
174+
}
169175
if de, ok := payload.(*DefaultError); ok && !h.EnableDebug {
170176
de2 := *de
171177
de2.DebugField = ""
@@ -179,6 +185,8 @@ func (h *JSONWriter) WriteErrorCode(w http.ResponseWriter, r *http.Request, code
179185
payload = ec2
180186
}
181187

188+
w.WriteHeader(code)
189+
182190
if err := json.NewEncoder(w).Encode(payload); err != nil {
183191
// There was an error, but there's actually not a lot we can do except log that this happened.
184192
h.Reporter.ReportError(r, code, errors.WithStack(err), "Could not write ErrorContainer to response writer")

json_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,51 @@ func TestCanceledJSON(t *testing.T) {
422422
assert.Contains(t, string(body), "some unrelated error")
423423
assert.Equal(t, 499, resp.StatusCode)
424424
}
425+
426+
func TestOryErrorIDHeader(t *testing.T) {
427+
for k, tc := range []struct {
428+
name string
429+
err error
430+
expectedHeader string
431+
}{
432+
{
433+
name: "error with ID sets header",
434+
err: &ErrMisconfiguration,
435+
expectedHeader: "invalid_configuration",
436+
},
437+
{
438+
name: "error without ID does not set header",
439+
err: &ErrNotFound,
440+
expectedHeader: "",
441+
},
442+
{
443+
name: "custom error with ID sets header",
444+
err: &DefaultError{
445+
IDField: "custom_error_id",
446+
CodeField: http.StatusBadRequest,
447+
StatusField: http.StatusText(http.StatusBadRequest),
448+
ErrorField: "custom error",
449+
},
450+
expectedHeader: "custom_error_id",
451+
},
452+
{
453+
name: "upstream error sets header",
454+
err: &ErrUpstreamError,
455+
expectedHeader: "upstream_error",
456+
},
457+
} {
458+
t.Run(fmt.Sprintf("case=%d/%s", k, tc.name), func(t *testing.T) {
459+
h := NewJSONWriter(nil)
460+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
461+
h.WriteError(w, r, tc.err)
462+
}))
463+
t.Cleanup(ts.Close)
464+
465+
resp, err := http.Get(ts.URL + "/do")
466+
require.NoError(t, err)
467+
defer resp.Body.Close()
468+
469+
assert.Equal(t, tc.expectedHeader, resp.Header.Get("Ory-Error-Id"))
470+
})
471+
}
472+
}

plain.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ func (h *TextWriter) WriteErrorCode(w http.ResponseWriter, r *http.Request, code
8787
// All errors land here, so it's a really good idea to do the logging here as well!
8888
h.Reporter.ReportError(r, code, err, "An error occurred while handling a request")
8989

90+
if id, ok := err.(interface{ ID() string }); ok {
91+
w.Header().Set("Ory-Error-Id", id.ID())
92+
}
9093
w.Header().Set("Content-Type", h.contentType)
9194
w.WriteHeader(code)
9295
fmt.Fprintf(w, "%s", err)

plain_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright © 2023 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package herodot
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestTextWriterOryErrorIDHeader(t *testing.T) {
17+
for k, tc := range []struct {
18+
name string
19+
err error
20+
expectedHeader string
21+
}{
22+
{
23+
name: "error with ID sets header",
24+
err: &ErrMisconfiguration,
25+
expectedHeader: "invalid_configuration",
26+
},
27+
{
28+
name: "error without ID does not set header",
29+
err: &ErrNotFound,
30+
expectedHeader: "",
31+
},
32+
{
33+
name: "custom error with ID sets header",
34+
err: &DefaultError{
35+
IDField: "custom_text_error_id",
36+
CodeField: http.StatusBadRequest,
37+
StatusField: http.StatusText(http.StatusBadRequest),
38+
ErrorField: "custom error",
39+
},
40+
expectedHeader: "custom_text_error_id",
41+
},
42+
{
43+
name: "upstream error sets header",
44+
err: &ErrUpstreamError,
45+
expectedHeader: "upstream_error",
46+
},
47+
} {
48+
t.Run(fmt.Sprintf("case=%d/%s", k, tc.name), func(t *testing.T) {
49+
h := NewTextWriter(&stdReporter{}, "plain")
50+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
h.WriteError(w, r, tc.err)
52+
}))
53+
t.Cleanup(ts.Close)
54+
55+
resp, err := http.Get(ts.URL + "/do")
56+
require.NoError(t, err)
57+
defer resp.Body.Close()
58+
59+
assert.Equal(t, tc.expectedHeader, resp.Header.Get("Ory-Error-Id"))
60+
})
61+
}
62+
}

0 commit comments

Comments
 (0)