From 2a4db6bb4aa04e104d9da10fd485d7ad016bafc9 Mon Sep 17 00:00:00 2001 From: Rajneesh Chaudhary Date: Wed, 15 Apr 2026 13:34:52 +0530 Subject: [PATCH] confighttp: add ExposedHeaders to CORSConfig Signed-off-by: Rajneesh Chaudhary --- .chloggen/add-cors-exposed-headers.yaml | 25 ++++++++++++++++++ config/confighttp/README.md | 6 +++++ config/confighttp/config.schema.yaml | 5 ++++ config/confighttp/server.go | 5 ++++ config/confighttp/server_test.go | 35 +++++++++++++++++++++++++ config/confighttp/testdata/config.yaml | 3 +++ 6 files changed, 79 insertions(+) create mode 100644 .chloggen/add-cors-exposed-headers.yaml diff --git a/.chloggen/add-cors-exposed-headers.yaml b/.chloggen/add-cors-exposed-headers.yaml new file mode 100644 index 00000000000..f1fbb53bd90 --- /dev/null +++ b/.chloggen/add-cors-exposed-headers.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp) +component: pkg/confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `ExposedHeaders` field to `CORSConfig` to allow setting the `Access-Control-Expose-Headers` response header. + +# One or more tracking issues or pull requests related to the change +issues: [15119] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user, api] diff --git a/config/confighttp/README.md b/config/confighttp/README.md index ba13f30fcfa..137b6033202 100644 --- a/config/confighttp/README.md +++ b/config/confighttp/README.md @@ -103,6 +103,9 @@ will not be enabled. [default safelist][cors-headers]. By default, safelist headers and `X-Requested-With` will be allowed. To allow any request header, set to `["*"]`. + - `exposed_headers`: Sets the value of the + [`Access-Control-Expose-Headers`][cors-expose] response header, indicating + which headers are safe to expose to the API of a CORS response. - `max_age`: Sets the value of the [`Access-Control-Max-Age`][cors-cache] header, allowing clients to cache the response to CORS preflight requests. If not set, browsers use a default of 5 seconds. @@ -143,6 +146,8 @@ receivers: - https://*.test.com allowed_headers: - Example-Header + exposed_headers: + - Example-Expose-Header max_age: 7200 endpoint: 0.0.0.0:55690 compression_algorithms: ["", "gzip"] @@ -157,5 +162,6 @@ processors: [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS [cors-headers]: https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header [cors-cache]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age +[cors-expose]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers [origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin [attribute-processor]: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/attributesprocessor/README.md diff --git a/config/confighttp/config.schema.yaml b/config/confighttp/config.schema.yaml index 52ed2849aa6..0304d5d1169 100644 --- a/config/confighttp/config.schema.yaml +++ b/config/confighttp/config.schema.yaml @@ -101,6 +101,11 @@ $defs: type: array items: type: string + exposed_headers: + description: ExposedHeaders sets the value of the Access-Control-Expose-Headers response header, indicating which headers are safe to expose to the API of a CORS response. + type: array + items: + type: string max_age: description: MaxAge sets the value of the Access-Control-Max-Age response header. Set it to the number of seconds that browsers should cache a CORS preflight response for. type: integer diff --git a/config/confighttp/server.go b/config/confighttp/server.go index 390028f0b59..b1960bdaa42 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -247,6 +247,7 @@ func (sc *ServerConfig) ToServer(ctx context.Context, extensions map[component.I AllowedOrigins: corsConfig.AllowedOrigins, AllowCredentials: true, AllowedHeaders: corsConfig.AllowedHeaders, + ExposedHeaders: corsConfig.ExposedHeaders, MaxAge: corsConfig.MaxAge, } handler = cors.New(co).Handler(handler) @@ -342,6 +343,10 @@ type CORSConfig struct { // allow any request header. AllowedHeaders []string `mapstructure:"allowed_headers,omitempty"` + // ExposedHeaders sets the value of the Access-Control-Expose-Headers response + // header, indicating which headers are safe to expose to the API of a CORS response. + ExposedHeaders []string `mapstructure:"exposed_headers,omitempty"` + // MaxAge sets the value of the Access-Control-Max-Age response header. // Set it to the number of seconds that browsers should cache a CORS // preflight response for. diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 75520a13fa6..2abcb1c16ea 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -455,6 +455,39 @@ func TestHTTPCorsWithSettings(t *testing.T) { assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin")) } +func TestHTTPCorsExposedHeaders(t *testing.T) { + sc := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "localhost:0", + Transport: confignet.TransportTypeTCP, + }, + CORS: configoptional.Some(CORSConfig{ + AllowedOrigins: []string{"http://allowed.com"}, + ExposedHeaders: []string{"X-Custom-Header", "X-Another-Header"}, + }), + } + + ln, err := sc.ToListener(context.Background()) + require.NoError(t, err) + + startServer(t, sc, ln, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + url := "http://" + ln.Addr().String() + + // ExposedHeaders are returned on actual requests, not preflight. + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) + require.NoError(t, err) + req.Header.Set("Origin", "http://allowed.com") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + assert.Equal(t, "X-Custom-Header, X-Another-Header", resp.Header.Get("Access-Control-Expose-Headers")) +} + func TestHTTPServerHeaders(t *testing.T) { tests := []struct { name string @@ -1146,6 +1179,8 @@ func TestServerUnmarshalYAMLComprehensiveConfig(t *testing.T) { assert.Equal(t, expectedOrigins, serverConfig.CORS.Get().AllowedOrigins) corsHeaders := []string{"Content-Type", "Accept"} assert.Equal(t, corsHeaders, serverConfig.CORS.Get().AllowedHeaders) + exposedHeaders := []string{"X-Request-Id", "X-Trace-Id"} + assert.Equal(t, exposedHeaders, serverConfig.CORS.Get().ExposedHeaders) assert.Equal(t, 7200, serverConfig.CORS.Get().MaxAge) // Verify response headers diff --git a/config/confighttp/testdata/config.yaml b/config/confighttp/testdata/config.yaml index 3aae5f75b63..456a6705328 100644 --- a/config/confighttp/testdata/config.yaml +++ b/config/confighttp/testdata/config.yaml @@ -79,6 +79,9 @@ server: allowed_headers: - "Content-Type" - "Accept" + exposed_headers: + - "X-Request-Id" + - "X-Trace-Id" max_age: 7200 # Authentication configuration