diff --git a/openapi.go b/openapi.go index 595fb4f..e4cdc01 100644 --- a/openapi.go +++ b/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 { diff --git a/openapi_test.go b/openapi_test.go index 0f25317..6c1c11a 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -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)