Skip to content

Commit 420777e

Browse files
committed
feat(sse): prevent setting default response schema for SSE when it is already set in the user-provided operation
1 parent a8a668c commit 420777e

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

sse/sse.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func (s Sender) Comment(comment string) error {
8282
// the context, input, and a `send` function that can be used to send messages
8383
// to the client. Flushing is handled automatically as long as the adapter's
8484
// `BodyWriter` implements `http.Flusher`.
85+
// The default response schema can be overridden by setting the
86+
// `op.Responses["200"].Content["text/event-stream"]` field before calling this function.
8587
func Register[I any](api huma.API, op huma.Operation, eventTypeMap map[string]any, f func(ctx context.Context, input *I, send Sender)) {
8688
// Start by defining the SSE schema & operation response.
8789
if op.Responses == nil {
@@ -144,8 +146,15 @@ func Register[I any](api huma.API, op huma.Operation, eventTypeMap map[string]an
144146
},
145147
},
146148
}
147-
op.Responses["200"].Content["text/event-stream"] = &huma.MediaType{
148-
Schema: schema,
149+
150+
opMediaType := op.Responses["200"].Content["text/event-stream"]
151+
152+
if opMediaType == nil {
153+
op.Responses["200"].Content["text/event-stream"] = &huma.MediaType{
154+
Schema: schema,
155+
}
156+
} else if opMediaType.Schema == nil {
157+
opMediaType.Schema = schema
149158
}
150159

151160
// Register the operation with the API, using the built-in streaming

sse/sse_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,77 @@ type sseTest struct {
9393
}
9494

9595
var sseTests = []sseTest{
96+
{
97+
Title: "sse preserves existing text/event-stream content",
98+
TestFunc: func(t *testing.T) {
99+
_, api := humatest.New(t)
100+
existingSchema := &huma.Schema{
101+
Title: "Existing Schema",
102+
Description: "This schema should be preserved",
103+
Type: huma.TypeString,
104+
}
105+
106+
sse.Register(api, huma.Operation{
107+
OperationID: "sse",
108+
Method: http.MethodGet,
109+
Path: "/sse",
110+
Responses: map[string]*huma.Response{
111+
"200": {
112+
Content: map[string]*huma.MediaType{
113+
"text/event-stream": {
114+
Schema: existingSchema,
115+
},
116+
},
117+
},
118+
},
119+
}, map[string]any{
120+
"message": &DefaultMessage{},
121+
}, func(ctx context.Context, input *struct{}, send sse.Sender) {
122+
send.Data(DefaultMessage{Message: "Hello, world!"})
123+
})
124+
125+
o := api.OpenAPI()
126+
response := o.Paths["/sse"].Get.Responses["200"]
127+
content := response.Content["text/event-stream"]
128+
129+
assert.Equal(t, "Existing Schema", content.Schema.Title)
130+
assert.Equal(t, "This schema should be preserved", content.Schema.Description)
131+
assert.Equal(t, huma.TypeString, content.Schema.Type)
132+
},
133+
},
134+
{
135+
Title: "sse sets default schema and preserves other fields in text/event-stream content",
136+
TestFunc: func(t *testing.T) {
137+
_, api := humatest.New(t)
138+
139+
example := DefaultMessage{Message: "Example message"}
140+
sse.Register(api, huma.Operation{
141+
OperationID: "sse",
142+
Method: http.MethodGet,
143+
Path: "/sse",
144+
Responses: map[string]*huma.Response{
145+
"200": {
146+
Content: map[string]*huma.MediaType{
147+
"text/event-stream": {
148+
Example: example,
149+
},
150+
},
151+
},
152+
},
153+
}, map[string]any{
154+
"message": &DefaultMessage{},
155+
}, func(ctx context.Context, input *struct{}, send sse.Sender) {
156+
send.Data(DefaultMessage{Message: "Hello, world!"})
157+
})
158+
159+
o := api.OpenAPI()
160+
response := o.Paths["/sse"].Get.Responses["200"]
161+
content := response.Content["text/event-stream"]
162+
163+
assert.Equal(t, example, content.Example)
164+
assert.NotNil(t, content.Schema)
165+
},
166+
},
96167
{
97168
Title: "sse",
98169
TestFunc: func(t *testing.T) {
@@ -263,6 +334,34 @@ data: {"message":"Hello, world!"}
263334
"no body write should occur before the user handler sends an event")
264335
},
265336
},
337+
{
338+
Title: "sse adds text/event-stream content when not present",
339+
TestFunc: func(t *testing.T) {
340+
_, api := humatest.New(t)
341+
342+
sse.Register(api, huma.Operation{
343+
OperationID: "sse",
344+
Method: http.MethodGet,
345+
Path: "/sse",
346+
}, map[string]any{
347+
"message": &DefaultMessage{},
348+
}, func(ctx context.Context, input *struct{}, send sse.Sender) {
349+
send.Data(DefaultMessage{Message: "Hello, world!"})
350+
})
351+
352+
o := api.OpenAPI()
353+
response := o.Paths["/sse"].Get.Responses["200"]
354+
content := response.Content["text/event-stream"]
355+
356+
// The SSE schema should be added
357+
assert.NotNil(t, content, "text/event-stream content should be present")
358+
assert.NotNil(t, content.Schema, "schema should be present")
359+
assert.Equal(t, "Server Sent Events", content.Schema.Title)
360+
assert.Equal(t, huma.TypeArray, content.Schema.Type)
361+
assert.NotNil(t, content.Schema.Items, "schema items should be present")
362+
assert.NotNil(t, content.Schema.Items.Extensions["oneOf"], "oneOf extension should be present")
363+
},
364+
},
266365
{
267366
Title: "sse stable event order in openapi",
268367
TestFunc: func(t *testing.T) {

0 commit comments

Comments
 (0)