Files
zeekay 8eb04ec95d merge: upstream/main (-X ours, brand preserved)
# Conflicts:
#	.chloggen/add-cors-exposed-headers.yaml
#	.chloggen/feature_configtls-insecure-cipher-suites.yaml
#	.chloggen/fix-mdatagen-underscore-in-semconv-bug.yaml
#	.chloggen/mdatagen_numeric_validators.yaml
#	AGENTS.md
#	AGENTS.md~upstream_main
#	CLAUDE.md
#	CLAUDE.md~upstream_main
#	confmap/internal/featuregates.go
#	service/telemetry/otelconftelemetry/internal/migration/v0.3.0_test.go
2026-06-02 10:21:37 -07:00

1212 lines
35 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package confighttp
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configauth"
"go.opentelemetry.io/collector/config/confignet"
"go.opentelemetry.io/collector/config/configopaque"
"go.opentelemetry.io/collector/config/configoptional"
"go.opentelemetry.io/collector/config/configtls"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/confmaptest"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/collector/extension/extensionauth"
)
var (
_ extension.Extension = (*mockAuthServer)(nil)
_ extensionauth.Server = (*mockAuthServer)(nil)
)
type mockAuthServer struct {
component.StartFunc
component.ShutdownFunc
extensionauth.ServerAuthenticateFunc
}
func newMockAuthServer(auth func(ctx context.Context, sources map[string][]string) (context.Context, error)) extension.Extension {
return &mockAuthServer{ServerAuthenticateFunc: auth}
}
func TestHTTPServerSettingsError(t *testing.T) {
tests := []struct {
settings ServerConfig
err string
}{
{
err: "^failed to load TLS config: failed to load CA CertPool File: failed to load cert /doesnt/exist:",
settings: ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
TLS: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: "/doesnt/exist",
},
}),
},
},
{
err: "^failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither",
settings: ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
TLS: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CertFile: "/doesnt/exist",
},
}),
},
},
{
err: "failed to load client CA CertPool: failed to load CA /doesnt/exist:",
settings: ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
TLS: configoptional.Some(configtls.ServerConfig{
ClientCAFile: "/doesnt/exist",
}),
},
},
}
for _, tt := range tests {
t.Run(tt.err, func(t *testing.T) {
_, err := tt.settings.ToListener(context.Background())
assert.Regexp(t, tt.err, err)
})
}
}
func TestHTTPServerTLS(t *testing.T) {
tests := []struct {
name string
tlsServerCreds configoptional.Optional[configtls.ServerConfig]
tlsClientCreds *configtls.ClientConfig
hasError bool
forceHTTP1 bool
}{
{
name: "noTLS",
tlsServerCreds: configoptional.None[configtls.ServerConfig](),
tlsClientCreds: &configtls.ClientConfig{
Insecure: true,
},
},
{
name: "TLS",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
ServerName: "localhost",
},
},
{
name: "TLS (HTTP/1.1)",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
ServerName: "localhost",
},
forceHTTP1: true,
},
{
name: "NoServerCertificates",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
ServerName: "localhost",
},
hasError: true,
},
{
name: "mTLS",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
ClientCAFile: filepath.Join("testdata", "ca.crt"),
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "client.crt"),
KeyFile: filepath.Join("testdata", "client.key"),
},
ServerName: "localhost",
},
},
{
name: "NoClientCertificate",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
ClientCAFile: filepath.Join("testdata", "ca.crt"),
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
ServerName: "localhost",
},
hasError: true,
},
{
name: "WrongClientCA",
tlsServerCreds: configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
ClientCAFile: filepath.Join("testdata", "server.crt"),
}),
tlsClientCreds: &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "client.crt"),
KeyFile: filepath.Join("testdata", "client.key"),
},
ServerName: "localhost",
},
hasError: true,
},
}
// prepare
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
TLS: tt.tlsServerCreds,
}
ln, err := sc.ToListener(context.Background())
require.NoError(t, err)
startServer(t, sc, ln, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, errWrite := fmt.Fprint(w, "tt")
assert.NoError(t, errWrite)
}))
prefix := "https://"
expectedProto := "HTTP/2.0"
if tt.tlsClientCreds.Insecure {
prefix = "http://"
expectedProto = "HTTP/1.1"
}
cc := &ClientConfig{
Endpoint: prefix + ln.Addr().String(),
TLS: *tt.tlsClientCreds,
ForceAttemptHTTP2: true,
}
client, errClient := cc.ToClient(context.Background(), nil, nilProvidersSettings)
require.NoError(t, errClient)
if tt.forceHTTP1 {
expectedProto = "HTTP/1.1"
client.Transport.(*http.Transport).ForceAttemptHTTP2 = false
}
resp, errResp := client.Get(cc.Endpoint)
if tt.hasError {
require.Error(t, errResp)
} else {
require.NoError(t, errResp)
body, errRead := io.ReadAll(resp.Body)
require.NoError(t, errRead)
assert.Equal(t, "tt", string(body))
assert.Equal(t, expectedProto, resp.Proto)
}
})
}
}
func TestHttpServerTransport(t *testing.T) {
if runtime.GOOS == "linux" {
t.Run("unix", func(t *testing.T) {
addr := "@" + t.Name() // abstract unix socket
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: addr,
Transport: confignet.TransportTypeUnix,
},
}
ln, err := sc.ToListener(context.Background())
require.NoError(t, err)
startServer(t, sc, ln, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
client := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", addr)
},
},
Timeout: 5 * time.Second, // Set a client-level timeout
}
resp, err := client.Get("http://whatever/foo")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
})
}
}
func TestHTTPCors(t *testing.T) {
tests := []struct {
name string
CORSConfig configoptional.Optional[CORSConfig]
allowedWorks bool
disallowedWorks bool
extraHeaderWorks bool
}{
{
name: "noCORS",
allowedWorks: false,
disallowedWorks: false,
extraHeaderWorks: false,
},
{
name: "emptyCORS",
CORSConfig: configoptional.Some(NewDefaultCORSConfig()),
allowedWorks: false,
disallowedWorks: false,
extraHeaderWorks: false,
},
{
name: "OriginCORS",
CORSConfig: configoptional.Some(CORSConfig{
AllowedOrigins: []string{"allowed-*.com"},
}),
allowedWorks: true,
disallowedWorks: false,
extraHeaderWorks: false,
},
{
name: "CacheableCORS",
CORSConfig: configoptional.Some(CORSConfig{
AllowedOrigins: []string{"allowed-*.com"},
MaxAge: 360,
}),
allowedWorks: true,
disallowedWorks: false,
extraHeaderWorks: false,
},
{
name: "HeaderCORS",
CORSConfig: configoptional.Some(CORSConfig{
AllowedOrigins: []string{"allowed-*.com"},
AllowedHeaders: []string{"ExtraHeader"},
}),
allowedWorks: true,
disallowedWorks: false,
extraHeaderWorks: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
CORS: tt.CORSConfig,
}
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()
expectedStatus := http.StatusNoContent
if !tt.CORSConfig.HasValue() || len(tt.CORSConfig.Get().AllowedOrigins) == 0 {
expectedStatus = http.StatusOK
}
// Verify allowed domain gets responses that allow CORS.
verifyCorsResp(t, url, "allowed-origin.com", tt.CORSConfig, false, expectedStatus, tt.allowedWorks)
// Verify allowed domain and extra headers gets responses that allow CORS.
verifyCorsResp(t, url, "allowed-origin.com", tt.CORSConfig, true, expectedStatus, tt.extraHeaderWorks)
// Verify disallowed domain gets responses that disallow CORS.
verifyCorsResp(t, url, "disallowed-origin.com", tt.CORSConfig, false, expectedStatus, tt.disallowedWorks)
})
}
}
func TestHTTPCorsInvalidSettings(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
CORS: configoptional.Some(CORSConfig{AllowedHeaders: []string{"some-header"}}),
}
// This effectively does not enable CORS but should also not cause an error
s, err := sc.ToServer(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
require.NoError(t, err)
require.NotNil(t, s)
require.NoError(t, s.Close())
}
func TestHTTPCorsWithSettings(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
CORS: configoptional.Some(CORSConfig{
AllowedOrigins: []string{"*"},
}),
Auth: configoptional.Some(AuthConfig{
Config: configauth.Config{
AuthenticatorID: mockID,
},
}),
}
extensions := map[component.ID]component.Component{
mockID: newMockAuthServer(func(ctx context.Context, _ map[string][]string) (context.Context, error) {
return ctx, errors.New("Settings failed")
}),
}
srv, err := sc.ToServer(context.Background(), extensions, componenttest.NewNopTelemetrySettings(), nil)
require.NoError(t, err)
require.NotNil(t, srv)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodOptions, "/", http.NoBody)
req.Header.Set("Origin", "http://localhost")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
srv.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNoContent, rec.Result().StatusCode)
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
headers configopaque.MapList
}{
{
name: "noHeaders",
headers: nil,
},
{
name: "withHeaders",
headers: configopaque.MapList{
{Name: "x-new-header-1", Value: "value1"},
{Name: "x-new-header-2", Value: "value2"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
ResponseHeaders: tt.headers,
}
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()
// Verify allowed domain gets responses that allow CORS.
verifyHeadersResp(t, url, tt.headers)
})
}
}
func verifyCorsResp(t *testing.T, url, origin string, set configoptional.Optional[CORSConfig], extraHeader bool, wantStatus int, wantAllowed bool) {
req, err := http.NewRequest(http.MethodOptions, url, http.NoBody)
require.NoError(t, err, "Error creating trace OPTIONS request: %v", err)
req.Header.Set("Origin", origin)
if extraHeader {
req.Header.Set("ExtraHeader", "foo")
req.Header.Set("Access-Control-Request-Headers", "extraheader")
}
req.Header.Set("Access-Control-Request-Method", "POST")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "Error sending OPTIONS to http server")
require.NotNil(t, resp.Body)
require.NoError(t, resp.Body.Close(), "Error closing OPTIONS response body")
assert.Equal(t, wantStatus, resp.StatusCode)
gotAllowOrigin := resp.Header.Get("Access-Control-Allow-Origin")
gotAllowMethods := resp.Header.Get("Access-Control-Allow-Methods")
wantAllowOrigin := ""
wantAllowMethods := ""
wantMaxAge := ""
if wantAllowed {
wantAllowOrigin = origin
wantAllowMethods = "POST"
if set.HasValue() && set.Get().MaxAge != 0 {
wantMaxAge = strconv.Itoa(set.Get().MaxAge)
}
}
assert.Equal(t, wantAllowOrigin, gotAllowOrigin)
assert.Equal(t, wantAllowMethods, gotAllowMethods)
assert.Equal(t, wantMaxAge, resp.Header.Get("Access-Control-Max-Age"))
}
func verifyHeadersResp(t *testing.T, url string, expected configopaque.MapList) {
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
require.NoError(t, err, "Error creating request")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "Error sending request to http server")
require.NotNil(t, resp.Body)
require.NoError(t, resp.Body.Close(), "Error closing response body")
assert.Equal(t, http.StatusOK, resp.StatusCode)
for k, v := range expected.Iter {
assert.Equal(t, string(v), resp.Header.Get(k))
}
}
func TestServerAuth(t *testing.T) {
// prepare
authCalled := false
sc := ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
Auth: configoptional.Some(AuthConfig{
Config: configauth.Config{
AuthenticatorID: mockID,
},
}),
}
extensions := map[component.ID]component.Component{
mockID: newMockAuthServer(func(ctx context.Context, _ map[string][]string) (context.Context, error) {
authCalled = true
return ctx, nil
}),
}
handlerCalled := false
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalled = true
})
srv, err := sc.ToServer(context.Background(), extensions, componenttest.NewNopTelemetrySettings(), handler)
require.NoError(t, err)
// tt
srv.Handler.ServeHTTP(&httptest.ResponseRecorder{}, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
// verify
assert.True(t, handlerCalled)
assert.True(t, authCalled)
}
func TestInvalidServerAuth(t *testing.T) {
sc := ServerConfig{
Auth: configoptional.Some(AuthConfig{
Config: configauth.Config{
AuthenticatorID: nonExistingID,
},
}),
}
srv, err := sc.ToServer(context.Background(), nil, componenttest.NewNopTelemetrySettings(), http.NewServeMux())
require.Error(t, err)
require.Nil(t, srv)
}
func TestFailedServerAuth(t *testing.T) {
// prepare
sc := ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
Auth: configoptional.Some(AuthConfig{
Config: configauth.Config{
AuthenticatorID: mockID,
},
}),
}
extensions := map[component.ID]component.Component{
mockID: newMockAuthServer(func(ctx context.Context, _ map[string][]string) (context.Context, error) {
return ctx, errors.New("invalid authorization")
}),
}
srv, err := sc.ToServer(context.Background(), extensions, componenttest.NewNopTelemetrySettings(), http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
require.NoError(t, err)
// tt
response := &httptest.ResponseRecorder{}
srv.Handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
// verify
assert.Equal(t, http.StatusUnauthorized, response.Result().StatusCode)
assert.Equal(t, fmt.Sprintf("%v %s", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), response.Result().Status)
}
func TestFailedServerAuthWithErrorHandler(t *testing.T) {
// prepare
sc := ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
Auth: configoptional.Some(AuthConfig{
Config: configauth.Config{
AuthenticatorID: mockID,
},
}),
}
extensions := map[component.ID]component.Component{
mockID: newMockAuthServer(func(ctx context.Context, _ map[string][]string) (context.Context, error) {
return ctx, errors.New("invalid authorization")
}),
}
eh := func(w http.ResponseWriter, _ *http.Request, err string, statusCode int) {
assert.Equal(t, http.StatusUnauthorized, statusCode)
// custom error handler uses real error string
assert.Equal(t, "invalid authorization", err)
// custom error handler changes returned status code
http.Error(w, err, http.StatusInternalServerError)
}
srv, err := sc.ToServer(context.Background(), extensions, componenttest.NewNopTelemetrySettings(), http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), WithErrorHandler(eh))
require.NoError(t, err)
// tt
response := &httptest.ResponseRecorder{}
srv.Handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
// verify
assert.Equal(t, http.StatusInternalServerError, response.Result().StatusCode)
assert.Equal(t, fmt.Sprintf("%v %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)), response.Result().Status)
}
func TestServerWithErrorHandler(t *testing.T) {
// prepare
sc := ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
}
eh := func(w http.ResponseWriter, _ *http.Request, _ string, statusCode int) {
assert.Equal(t, http.StatusBadRequest, statusCode)
// custom error handler changes returned status code
http.Error(w, "invalid request", http.StatusInternalServerError)
}
srv, err := sc.ToServer(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
WithErrorHandler(eh),
)
require.NoError(t, err)
// tt
response := &httptest.ResponseRecorder{}
req, err := http.NewRequest(http.MethodGet, srv.Addr, http.NoBody)
require.NoError(t, err, "Error creating request: %v", err)
req.Header.Set("Content-Encoding", "something-invalid")
srv.Handler.ServeHTTP(response, req)
// verify
assert.Equal(t, http.StatusInternalServerError, response.Result().StatusCode)
}
func TestServerWithDecoder(t *testing.T) {
// prepare
sc := NewDefaultServerConfig()
sc.NetAddr.Endpoint = "localhost:0"
decoder := func(body io.ReadCloser) (io.ReadCloser, error) {
return body, nil
}
srv, err := sc.ToServer(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
WithDecoder("something-else", decoder),
)
require.NoError(t, err)
// tt
response := &httptest.ResponseRecorder{}
req, err := http.NewRequest(http.MethodGet, srv.Addr, bytes.NewBuffer([]byte("something")))
require.NoError(t, err, "Error creating request: %v", err)
req.Header.Set("Content-Encoding", "something-else")
srv.Handler.ServeHTTP(response, req)
// verify
assert.Equal(t, http.StatusOK, response.Result().StatusCode)
}
func TestServerWithDecompression(t *testing.T) {
// prepare
sc := ServerConfig{
MaxRequestBodySize: 1000, // 1 KB
}
body := []byte(strings.Repeat("a", 1000*1000)) // 1 MB
srv, err := sc.ToServer(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
actualBody, err := io.ReadAll(req.Body)
assert.ErrorContains(t, err, "http: request body too large")
assert.Len(t, actualBody, 1000)
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
} else {
resp.WriteHeader(http.StatusOK)
}
}),
)
require.NoError(t, err)
testSrv := httptest.NewServer(srv.Handler)
defer testSrv.Close()
req, err := http.NewRequest(http.MethodGet, testSrv.URL, compressZstd(t, body))
require.NoError(t, err, "Error creating request: %v", err)
req.Header.Set("Content-Encoding", "zstd")
// tt
c := http.Client{}
resp, err := c.Do(req)
require.NoError(t, err, "Error sending request: %v", err)
_, err = io.ReadAll(resp.Body)
require.NoError(t, err, "Error reading response body: %v", err)
// verifications is done mostly within the tt, but this is only a sanity check
// that we got into the tt handler
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
func TestDefaultMaxRequestBodySize(t *testing.T) {
tests := []struct {
name string
settings ServerConfig
expected int64
}{
{
name: "default",
settings: ServerConfig{},
expected: defaultMaxRequestBodySize,
},
{
name: "zero",
settings: ServerConfig{MaxRequestBodySize: 0},
expected: defaultMaxRequestBodySize,
},
{
name: "negative",
settings: ServerConfig{MaxRequestBodySize: -1},
expected: defaultMaxRequestBodySize,
},
{
name: "custom",
settings: ServerConfig{MaxRequestBodySize: 100},
expected: 100,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.settings.ToServer(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
)
require.NoError(t, err)
assert.Equal(t, tt.expected, tt.settings.MaxRequestBodySize)
})
}
}
func TestAuthWithQueryParams(t *testing.T) {
// prepare
authCalled := false
sc := ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
Auth: configoptional.Some(AuthConfig{
RequestParameters: []string{"auth"},
Config: configauth.Config{
AuthenticatorID: mockID,
},
}),
}
extensions := map[component.ID]component.Component{
mockID: newMockAuthServer(func(ctx context.Context, sources map[string][]string) (context.Context, error) {
require.Len(t, sources, 1)
assert.Equal(t, "1", sources["auth"][0])
authCalled = true
return ctx, nil
}),
}
handlerCalled := false
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalled = true
})
srv, err := sc.ToServer(context.Background(), extensions, componenttest.NewNopTelemetrySettings(), handler)
require.NoError(t, err)
// tt
srv.Handler.ServeHTTP(&httptest.ResponseRecorder{}, httptest.NewRequest(http.MethodGet, "/?auth=1", http.NoBody))
// verify
assert.True(t, handlerCalled)
assert.True(t, authCalled)
}
func BenchmarkHTTPRequest(b *testing.B) {
tests := []struct {
name string
forceHTTP1 bool
clientPerThread bool
}{
{
name: "HTTP/2.0, shared client (like load balancer)",
forceHTTP1: false,
clientPerThread: false,
},
{
name: "HTTP/1.1, shared client (like load balancer)",
forceHTTP1: true,
clientPerThread: false,
},
{
name: "HTTP/2.0, client per thread (like single app)",
forceHTTP1: false,
clientPerThread: true,
},
{
name: "HTTP/1.1, client per thread (like single app)",
forceHTTP1: true,
clientPerThread: true,
},
}
tlsServerCreds := configoptional.Some(configtls.ServerConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
CertFile: filepath.Join("testdata", "server.crt"),
KeyFile: filepath.Join("testdata", "server.key"),
},
})
tlsClientCreds := &configtls.ClientConfig{
Config: configtls.Config{
CAFile: filepath.Join("testdata", "ca.crt"),
},
ServerName: "localhost",
}
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
TLS: tlsServerCreds,
}
ln, err := sc.ToListener(context.Background())
require.NoError(b, err)
startServer(b, sc, ln, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, errWrite := fmt.Fprint(w, "tt")
assert.NoError(b, errWrite)
}))
for _, bb := range tests {
cc := &ClientConfig{
Endpoint: "https://" + ln.Addr().String(),
TLS: *tlsClientCreds,
}
b.Run(bb.name, func(b *testing.B) {
var c *http.Client
if !bb.clientPerThread {
c, err = cc.ToClient(context.Background(), nil, nilProvidersSettings)
require.NoError(b, err)
}
b.RunParallel(func(pb *testing.PB) {
if c == nil {
c, err = cc.ToClient(context.Background(), nil, nilProvidersSettings)
require.NoError(b, err)
}
if bb.forceHTTP1 {
c.Transport.(*http.Transport).ForceAttemptHTTP2 = false
}
for pb.Next() {
resp, errResp := c.Get(cc.Endpoint)
require.NoError(b, errResp)
body, errRead := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.NoError(b, errRead)
require.Equal(b, "tt", string(body))
}
c.CloseIdleConnections()
})
// Wait for connections to close before closing server to prevent log spam
<-time.After(10 * time.Millisecond)
})
}
}
func TestDefaultHTTPServerSettings(t *testing.T) {
httpServerSettings := NewDefaultServerConfig()
assert.NotNil(t, httpServerSettings.CORS)
assert.NotNil(t, httpServerSettings.TLS)
assert.Equal(t, 1*time.Minute, httpServerSettings.IdleTimeout)
assert.Equal(t, 30*time.Second, httpServerSettings.WriteTimeout)
assert.Equal(t, time.Duration(0), httpServerSettings.ReadTimeout)
assert.Equal(t, 1*time.Minute, httpServerSettings.ReadHeaderTimeout)
assert.True(t, httpServerSettings.KeepAlivesEnabled) // Default should be true (keep-alives enabled by default)
}
func TestHTTPServerKeepAlives(t *testing.T) {
tests := []struct {
name string
keepAlivesEnabled bool
expectedKeepAlives bool
}{
{
name: "KeepAlives enabled",
keepAlivesEnabled: true,
expectedKeepAlives: true,
},
{
name: "KeepAlives disabled",
keepAlivesEnabled: false,
expectedKeepAlives: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &ServerConfig{
NetAddr: confignet.AddrConfig{
Endpoint: "localhost:0",
Transport: confignet.TransportTypeTCP,
},
KeepAlivesEnabled: tt.keepAlivesEnabled,
}
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)
}))
// Since http.Server.disableKeepAlives is a private field and difficult to test directly,
// we'll verify the configuration was set by testing the server behavior.
// The main verification is that ToServer() succeeds without error when DisableKeepAlives is set.
resp, err := http.Get("http://" + ln.Addr().String())
require.NoError(t, err)
require.NotNil(t, resp)
_ = resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, tt.keepAlivesEnabled, sc.KeepAlivesEnabled)
})
}
}
func TestHTTPServerTelemetry_Tracing(t *testing.T) {
// Create a pattern route. The server name the span after the
// pattern rather than the client-specified path.
mux := http.NewServeMux()
mux.HandleFunc("/b/{bucket}/o/{objectname...}", func(http.ResponseWriter, *http.Request) {})
type testcase struct {
handler http.Handler
httpMethod string
expectedSpanName string
}
for name, tc := range map[string]testcase{
"pattern": {
handler: mux,
httpMethod: "GET",
expectedSpanName: "GET /b/{bucket}/o/{objectname...}",
},
"no_pattern": {
handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
httpMethod: "GET",
expectedSpanName: "GET",
},
"unknown_method": {
handler: mux,
httpMethod: "FOOBAR",
expectedSpanName: "HTTP /b/{bucket}/o/{objectname...}",
},
"lowercase_method": {
handler: mux,
httpMethod: "get",
expectedSpanName: "GET /b/{bucket}/o/{objectname...}",
},
} {
t.Run(name, func(t *testing.T) {
telemetry := componenttest.NewTelemetry()
config := NewDefaultServerConfig()
config.NetAddr.Endpoint = "localhost:0"
srv, err := config.ToServer(
context.Background(),
nil,
telemetry.NewTelemetrySettings(),
tc.handler,
)
require.NoError(t, err)
done := make(chan struct{})
lis, err := config.ToListener(context.Background())
require.NoError(t, err)
go func() {
defer close(done)
_ = srv.Serve(lis)
}()
defer func() {
assert.NoError(t, srv.Close())
<-done
}()
req, err := http.NewRequest(tc.httpMethod, fmt.Sprintf("http://%s/b/bucket123/o/object456/segment", lis.Addr()), http.NoBody)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
spans := telemetry.SpanRecorder.Ended()
require.Len(t, spans, 1)
assert.Equal(t, tc.expectedSpanName, spans[0].Name())
})
}
}
// TestUnmarshalYAMLWithMiddlewares tests that the "middlewares" field is correctly
// parsed from YAML configurations (fixing the bug where "middleware" was used instead)
func TestServerUnmarshalYAMLWithMiddlewares(t *testing.T) {
cm, err := confmaptest.LoadConf(filepath.Join("testdata", "middlewares.yaml"))
require.NoError(t, err)
// Test server configuration
var serverConfig ServerConfig
serverSub, err := cm.Sub("server")
require.NoError(t, err)
require.NoError(t, serverSub.Unmarshal(&serverConfig))
// Validate the server configuration using reflection-based validation
require.NoError(t, confmap.Validate(&serverConfig), "Server configuration should be valid")
assert.Equal(t, "0.0.0.0:4318", serverConfig.NetAddr.Endpoint)
require.Len(t, serverConfig.Middlewares, 2)
assert.Equal(t, component.MustNewID("careful_middleware"), serverConfig.Middlewares[0].ID)
assert.Equal(t, component.MustNewID("support_middleware"), serverConfig.Middlewares[1].ID)
}
// TestUnmarshalYAMLComprehensiveConfig tests the complete configuration example
// to ensure all fields including middlewares are parsed correctly
func TestServerUnmarshalYAMLComprehensiveConfig(t *testing.T) {
cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml"))
require.NoError(t, err)
// Test server configuration
var serverConfig ServerConfig
serverSub, err := cm.Sub("server")
require.NoError(t, err)
require.NoError(t, serverSub.Unmarshal(&serverConfig))
// Validate the server configuration using reflection-based validation
require.NoError(t, confmap.Validate(&serverConfig), "Server configuration should be valid")
// Verify basic fields
assert.Equal(t, "0.0.0.0:4318", serverConfig.NetAddr.Endpoint)
assert.Equal(t, 30*time.Second, serverConfig.ReadTimeout)
assert.Equal(t, 10*time.Second, serverConfig.ReadHeaderTimeout)
assert.Equal(t, 30*time.Second, serverConfig.WriteTimeout)
assert.Equal(t, 120*time.Second, serverConfig.IdleTimeout)
assert.True(t, serverConfig.KeepAlivesEnabled) // Should be true as configured in config.yaml
assert.Equal(t, int64(33554432), serverConfig.MaxRequestBodySize)
assert.True(t, serverConfig.IncludeMetadata)
// Verify TLS configuration
assert.Equal(t, "/path/to/server.crt", serverConfig.TLS.Get().CertFile)
assert.Equal(t, "/path/to/server.key", serverConfig.TLS.Get().KeyFile)
assert.Equal(t, "/path/to/ca.crt", serverConfig.TLS.Get().CAFile)
assert.Equal(t, "/path/to/client-ca.crt", serverConfig.TLS.Get().ClientCAFile)
// Verify CORS configuration
expectedOrigins := []string{"https://example.com", "https://*.test.com"}
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
expectedResponseHeaders := configopaque.MapList{
{Name: "Server", Value: "OpenTelemetry-Collector"},
{Name: "X-Flavor", Value: "apple"},
}
assert.Equal(t, expectedResponseHeaders, serverConfig.ResponseHeaders)
// Verify compression algorithms
expectedAlgorithms := []string{"", "gzip", "zstd", "zlib", "snappy", "deflate"}
assert.Equal(t, expectedAlgorithms, serverConfig.CompressionAlgorithms)
// Verify middlewares
require.Len(t, serverConfig.Middlewares, 3)
assert.Equal(t, component.MustNewID("server_middleware1"), serverConfig.Middlewares[0].ID)
assert.Equal(t, component.MustNewID("server_middleware2"), serverConfig.Middlewares[1].ID)
assert.Equal(t, component.MustNewID("server_middleware3"), serverConfig.Middlewares[2].ID)
}
func startServer(tb testing.TB, sc *ServerConfig, ln net.Listener, h http.Handler) {
s, err := sc.ToServer(tb.Context(), nil, componenttest.NewNopTelemetrySettings(), h)
require.NoError(tb, err)
tb.Cleanup(func() {
require.NoError(tb, s.Close())
})
go func() { _ = s.Serve(ln) }()
}