Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .chloggen/add-cors-exposed-headers.yaml
Original file line number Diff line number Diff line change
@@ -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]
6 changes: 6 additions & 0 deletions config/confighttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"]
Expand All @@ -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
5 changes: 5 additions & 0 deletions config/confighttp/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions config/confighttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"`
Comment on lines +346 to +348
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CORSConfig gained ExposedHeaders, but the package’s config.schema.yaml still defines cors_config without an exposed_headers property. This will make the new setting invisible/invalid for schema-driven tooling. Update config/confighttp/config.schema.yaml to include exposed_headers (type array of strings) with a matching description.

Copilot uses AI. Check for mistakes.

// 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.
Expand Down
35 changes: 35 additions & 0 deletions config/confighttp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}),
Comment on lines +458 to +467
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates wiring into cors.Options, but it doesn’t verify that exposed_headers can be set via YAML/unmarshal (the core motivation in the PR description). Consider extending the existing YAML unmarshal “comprehensive config” testdata + assertions to include exposed_headers, so the mapstructure tag is exercised.

Copilot uses AI. Check for mistakes.
}

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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions config/confighttp/testdata/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ server:
allowed_headers:
- "Content-Type"
- "Accept"
exposed_headers:
- "X-Request-Id"
- "X-Trace-Id"
max_age: 7200

# Authentication configuration
Expand Down
Loading