Skip to content

Commit 74a61d5

Browse files
committed
feat: improve HTTP integration with JSON, Map, and Headers helpers
1 parent d8f7532 commit 74a61d5

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

http.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"net/http"
66
)
77

8+
// ContentType is the standard media type for Problem Details over HTTP.
9+
const ContentType = "application/problem+json; charset=utf-8"
10+
811
// Write automatically writes the Problem Details as a JSON HTTP response
912
// with the correct Application/Problem+JSON content type.
1013
func (p *Problem) Write(w http.ResponseWriter) error {
11-
w.Header().Set("Content-Type", "application/problem+json; charset=utf-8")
14+
w.Header().Set("Content-Type", ContentType)
1215
w.Header().Set("X-Content-Type-Options", "nosniff")
1316
w.WriteHeader(p.Status)
1417

@@ -22,6 +25,31 @@ func (p *Problem) Write(w http.ResponseWriter) error {
2225
return nil
2326
}
2427

25-
// Handler is a generic middleware interface or handler concept, but realistically
26-
// you just need `problem.Write(w)`.
27-
// We can provide a functional wrapper if you wanted, but usually `Write` is enough.
28+
// JSON returns the JSON encoding of the Problem.
29+
// This is useful for frameworks like Gin, Echo, or Fiber where you might
30+
// want to write the raw JSON bytes directly and set the Content-Type manually.
31+
func (p *Problem) JSON() []byte {
32+
b, _ := json.Marshal(p)
33+
return b
34+
}
35+
36+
// Headers returns a map of HTTP headers that should be set when returning this problem.
37+
// This is convenient for translating to other framework's header structures.
38+
func (p *Problem) Headers() map[string]string {
39+
return map[string]string{
40+
"Content-Type": ContentType,
41+
"X-Content-Type-Options": "nosniff",
42+
}
43+
}
44+
45+
// Map converts the Problem to a generic map map[string]any.
46+
// This is particularly useful when returning Problem Details via frameworks
47+
// that prefer or require maps for custom JSON serialization overrides.
48+
func (p *Problem) Map() map[string]any {
49+
var m map[string]any
50+
// Reusing Marshal/Unmarshal avoids duplicating MarshalJSON logic
51+
// and preserves consistency with the struct's existing serialization.
52+
b, _ := json.Marshal(p)
53+
_ = json.Unmarshal(b, &m)
54+
return m
55+
}

problem_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,41 @@ func TestProblemHTTPWrite(t *testing.T) {
163163
t.Errorf("expected nosniff, got %v", snif)
164164
}
165165
}
166+
167+
func TestProblemHTTPHelpers(t *testing.T) {
168+
p := problem.New(problem.BadRequest, problem.WithDetail("Invalid ID"))
169+
170+
t.Run("Headers", func(t *testing.T) {
171+
headers := p.Headers()
172+
if headers["Content-Type"] != problem.ContentType {
173+
t.Errorf("expected Content-Type %s, got %s", problem.ContentType, headers["Content-Type"])
174+
}
175+
if headers["X-Content-Type-Options"] != "nosniff" {
176+
t.Errorf("expected X-Content-Type-Options nosniff, got %s", headers["X-Content-Type-Options"])
177+
}
178+
})
179+
180+
t.Run("JSON", func(t *testing.T) {
181+
data := p.JSON()
182+
var m map[string]any
183+
if err := json.Unmarshal(data, &m); err != nil {
184+
t.Fatalf("JSON() returned invalid json: %v", err)
185+
}
186+
if m["title"] != "Bad Request" || m["detail"] != "Invalid ID" {
187+
t.Errorf("JSON() content mismatch: %v", m)
188+
}
189+
})
190+
191+
t.Run("Map", func(t *testing.T) {
192+
m := p.Map()
193+
if m["title"] != "Bad Request" {
194+
t.Errorf("Map() expected title 'Bad Request', got %v", m["title"])
195+
}
196+
if m["status"].(float64) != 400 {
197+
t.Errorf("Map() expected status 400, got %v", m["status"])
198+
}
199+
if m["detail"] != "Invalid ID" {
200+
t.Errorf("Map() expected detail 'Invalid ID', got %v", m["detail"])
201+
}
202+
})
203+
}

0 commit comments

Comments
 (0)