feat(api): document request ID response headers
Add X-Request-ID to the generated OpenAPI response headers so the spec matches the runtime contract for request ID propagation. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
1bb2f68b3f
commit
4420651fcf
2 changed files with 43 additions and 2 deletions
35
openapi.go
35
openapi.go
|
|
@ -170,6 +170,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(dataSchema),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
"400": map[string]any{
|
||||
"description": "Bad request",
|
||||
|
|
@ -178,6 +179,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
"401": map[string]any{
|
||||
"description": "Unauthorised",
|
||||
|
|
@ -186,6 +188,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
"403": map[string]any{
|
||||
"description": "Forbidden",
|
||||
|
|
@ -194,6 +197,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
"429": map[string]any{
|
||||
"description": "Too many requests",
|
||||
|
|
@ -202,7 +206,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": rateLimitHeaders(),
|
||||
"headers": mergeHeaders(standardResponseHeaders(), rateLimitHeaders()),
|
||||
},
|
||||
"504": map[string]any{
|
||||
"description": "Gateway timeout",
|
||||
|
|
@ -211,6 +215,7 @@ func operationResponses(dataSchema map[string]any) map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -226,6 +231,7 @@ func healthResponses() map[string]any {
|
|||
"schema": envelopeSchema(map[string]any{"type": "string"}),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
"429": map[string]any{
|
||||
"description": "Too many requests",
|
||||
|
|
@ -234,7 +240,7 @@ func healthResponses() map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": rateLimitHeaders(),
|
||||
"headers": mergeHeaders(standardResponseHeaders(), rateLimitHeaders()),
|
||||
},
|
||||
"504": map[string]any{
|
||||
"description": "Gateway timeout",
|
||||
|
|
@ -243,6 +249,7 @@ func healthResponses() map[string]any {
|
|||
"schema": envelopeSchema(nil),
|
||||
},
|
||||
},
|
||||
"headers": standardResponseHeaders(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +326,30 @@ func rateLimitHeaders() map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
// standardResponseHeaders documents headers emitted by the response envelope
|
||||
// middleware on all responses when request IDs are enabled.
|
||||
func standardResponseHeaders() map[string]any {
|
||||
return map[string]any{
|
||||
"X-Request-ID": map[string]any{
|
||||
"description": "Request identifier propagated from the client or generated by the server",
|
||||
"schema": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// mergeHeaders combines multiple OpenAPI header maps into one.
|
||||
func mergeHeaders(sets ...map[string]any) map[string]any {
|
||||
merged := make(map[string]any)
|
||||
for _, set := range sets {
|
||||
for name, value := range set {
|
||||
merged[name] = value
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// operationID builds a stable OpenAPI operationId from the HTTP method and path.
|
||||
// The generated identifier is lower snake_case and preserves path parameter names.
|
||||
func operationID(method, path string, operationIDs map[string]int) string {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) {
|
|||
if _, ok := headers["Retry-After"]; !ok {
|
||||
t.Fatal("expected Retry-After header on /health 429 response")
|
||||
}
|
||||
if _, ok := headers["X-Request-ID"]; !ok {
|
||||
t.Fatal("expected X-Request-ID header on /health 429 response")
|
||||
}
|
||||
|
||||
// Verify system tag exists.
|
||||
tags := spec["tags"].([]any)
|
||||
|
|
@ -250,6 +253,9 @@ func TestSpecBuilder_Good_SecuredResponses(t *testing.T) {
|
|||
if _, ok := headers["Retry-After"]; !ok {
|
||||
t.Fatal("expected Retry-After header in secured operation 429 response")
|
||||
}
|
||||
if _, ok := headers["X-Request-ID"]; !ok {
|
||||
t.Fatal("expected X-Request-ID header in secured operation 429 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_EnvelopeWrapping(t *testing.T) {
|
||||
|
|
@ -292,6 +298,10 @@ func TestSpecBuilder_Good_EnvelopeWrapping(t *testing.T) {
|
|||
getOp := fetchPath["get"].(map[string]any)
|
||||
responses := getOp["responses"].(map[string]any)
|
||||
resp200 := responses["200"].(map[string]any)
|
||||
headers := resp200["headers"].(map[string]any)
|
||||
if _, ok := headers["X-Request-ID"]; !ok {
|
||||
t.Fatal("expected X-Request-ID header on 200 response")
|
||||
}
|
||||
content := resp200["content"].(map[string]any)
|
||||
appJSON := content["application/json"].(map[string]any)
|
||||
schema := appJSON["schema"].(map[string]any)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue