Repository: open-telemetry/opentelemetry-collector
Pull request #12845: Middleware: HTTP support (part 3/4)
Author: login: jmacd
Created: 2025-04-15T01:00:42Z | Merged: 2025-04-18T20:44:13Z

#### Description

Adds the HTTP middleware support from #12842.

#### Link to tracking issue

Part of #12603.

#### Testing

Yes.

#### Documentation

Added.


Diff:
diff --git a/.chloggen/middleware-http.yaml b/.chloggen/middleware-http.yaml
new file mode 100644
index 00000000000..2b916615cd8
--- /dev/null
+++ b/.chloggen/middleware-http.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. otlpreceiver)
+component: confighttp
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add HTTP middleware support.
+
+# One or more tracking issues or pull requests related to the change
+issues: [12603, 9591, 7441]
+
+# (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]
diff --git a/cmd/builder/internal/builder/main_test.go b/cmd/builder/internal/builder/main_test.go
index 0d49d538b6f..0d6cf0d528e 100644
--- a/cmd/builder/internal/builder/main_test.go
+++ b/cmd/builder/internal/builder/main_test.go
@@ -47,6 +47,7 @@ var replaceModules = []string{
 	"/config/configcompression",
 	"/config/configgrpc",
 	"/config/confighttp",
+	"/config/configmiddleware",
 	"/config/confignet",
 	"/config/configopaque",
 	"/config/configretry",
@@ -79,6 +80,8 @@ var replaceModules = []string{
 	"/extension/extensionauth",
 	"/extension/extensionauth/extensionauthtest",
 	"/extension/extensioncapabilities",
+	"/extension/extensionmiddleware",
+	"/extension/extensionmiddleware/extensionmiddlewaretest",
 	"/extension/extensiontest",
 	"/extension/zpagesextension",
 	"/extension/xextension",
diff --git a/cmd/otelcorecol/builder-config.yaml b/cmd/otelcorecol/builder-config.yaml
index 47585926e72..a0dd3029a1f 100644
--- a/cmd/otelcorecol/builder-config.yaml
+++ b/cmd/otelcorecol/builder-config.yaml
@@ -46,6 +46,7 @@ replaces:
   - go.opentelemetry.io/collector/config/configcompression => ../../config/configcompression
   - go.opentelemetry.io/collector/config/configgrpc => ../../config/configgrpc
   - go.opentelemetry.io/collector/config/confighttp => ../../config/confighttp
+  - go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
   - go.opentelemetry.io/collector/config/confignet => ../../config/confignet
   - go.opentelemetry.io/collector/config/configopaque => ../../config/configopaque
   - go.opentelemetry.io/collector/config/configretry => ../../config/configretry
@@ -79,6 +80,8 @@ replaces:
   - go.opentelemetry.io/collector/extension/extensionauth => ../../extension/extensionauth
   - go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest => ../../extension/extensionauth/extensionauthtest
   - go.opentelemetry.io/collector/extension/extensioncapabilities => ../../extension/extensioncapabilities
+  - go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+  - go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
   - go.opentelemetry.io/collector/extension/extensiontest => ../../extension/extensiontest
   - go.opentelemetry.io/collector/extension/memorylimiterextension => ../../extension/memorylimiterextension
   - go.opentelemetry.io/collector/extension/xextension => ../../extension/xextension
diff --git a/cmd/otelcorecol/go.mod b/cmd/otelcorecol/go.mod
index 05db670b0aa..75241b1b7e1 100644
--- a/cmd/otelcorecol/go.mod
+++ b/cmd/otelcorecol/go.mod
@@ -89,6 +89,7 @@ require (
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configgrpc v0.124.0 // indirect
 	go.opentelemetry.io/collector/config/confighttp v0.124.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/config/confignet v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configopaque v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configretry v1.30.0 // indirect
@@ -107,6 +108,7 @@ require (
 	go.opentelemetry.io/collector/exporter/xexporter v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensiontest v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension/xextension v0.124.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
@@ -186,6 +188,8 @@ replace go.opentelemetry.io/collector/config/configgrpc => ../../config/configgr
 
 replace go.opentelemetry.io/collector/config/confighttp => ../../config/confighttp
 
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
 replace go.opentelemetry.io/collector/config/confignet => ../../config/confignet
 
 replace go.opentelemetry.io/collector/config/configopaque => ../../config/configopaque
@@ -252,6 +256,10 @@ replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest
 
 replace go.opentelemetry.io/collector/extension/extensioncapabilities => ../../extension/extensioncapabilities
 
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
+
 replace go.opentelemetry.io/collector/extension/extensiontest => ../../extension/extensiontest
 
 replace go.opentelemetry.io/collector/extension/memorylimiterextension => ../../extension/memorylimiterextension
diff --git a/config/confighttp/README.md b/config/confighttp/README.md
index 1f1cb1bba27..1ee56939814 100644
--- a/config/confighttp/README.md
+++ b/config/confighttp/README.md
@@ -58,6 +58,7 @@ README](../configtls/README.md).
 - [`http2_ping_timeout`](https://pkg.go.dev/golang.org/x/net/http2#Transport)
 - [`cookies`](https://pkg.go.dev/net/http#CookieJar)
   - [`enabled`] if enabled, the client will store cookies from server responses and reuse them in subsequent requests.
+- [`middlewares`](../configmiddleware/README.md)
 
 Example:
 
@@ -107,6 +108,7 @@ will not be enabled.
 - [`tls`](../configtls/README.md)
 - [`auth`](../configauth/README.md)
   - `request_params`: a list of query parameter names to add to the auth context, along with the HTTP headers
+- [`middlewares`](../configmiddleware/README.md)
 
 You can enable [`attribute processor`][attribute-processor] to append any http header to span's attribute using custom key. You also need to enable the "include_metadata"
 
diff --git a/config/confighttp/client_middleware_test.go b/config/confighttp/client_middleware_test.go
new file mode 100644
index 00000000000..c9427d9fd02
--- /dev/null
+++ b/config/confighttp/client_middleware_test.go
@@ -0,0 +1,255 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	extension.Extension
+	extensionmiddleware.GetHTTPRoundTripperFunc
+}
+
+func newTestClientMiddleware(name string) component.Component {
+	return &testClientMiddleware{
+		Extension: extensionmiddlewaretest.NewNop(),
+		GetHTTPRoundTripperFunc: func(transport http.RoundTripper) (http.RoundTripper, error) {
+			return extensionmiddlewaretest.HTTPClientFunc(
+				func(req *http.Request) (*http.Response, error) {
+					resp, err := transport.RoundTrip(req)
+					if err != nil {
+						return resp, err
+					}
+
+					// Read the original body
+					body, err := io.ReadAll(resp.Body)
+					if err != nil {
+						return resp, err
+					}
+					_ = resp.Body.Close()
+
+					// Create a new body with the appended text
+					newBody := string(body) + "\r\noutput by " + name
+
+					// Replace the response body
+					resp.Body = io.NopCloser(strings.NewReader(newBody))
+					resp.ContentLength = int64(len(newBody))
+
+					return resp, nil
+				}), nil
+		},
+	}
+}
+
+func newTestClientConfig(name string) configmiddleware.Config {
+	return configmiddleware.Config{
+		ID: component.MustNewID(name),
+	}
+}
+
+func TestClientMiddlewares(t *testing.T) {
+	// Create a test server that returns "OK"
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte("OK"))
+	}))
+	defer server.Close()
+
+	// Register two test extensions
+	host := &mockHost{
+		ext: map[component.ID]component.Component{
+			component.MustNewID("test1"): newTestClientMiddleware("test1"),
+			component.MustNewID("test2"): newTestClientMiddleware("test2"),
+		},
+	}
+
+	// Test with different middleware configurations
+	testCases := []struct {
+		name           string
+		middlewares    []configmiddleware.Config
+		expectedOutput string
+	}{
+		{
+			name:           "no_middlewares",
+			middlewares:    nil,
+			expectedOutput: "OK",
+		},
+		{
+			name: "single_middleware",
+			middlewares: []configmiddleware.Config{
+				newTestClientConfig("test1"),
+			},
+			expectedOutput: "OK\r\noutput by test1",
+		},
+		{
+			name: "multiple_middlewares",
+			middlewares: []configmiddleware.Config{
+				newTestClientConfig("test1"),
+				newTestClientConfig("test2"),
+			},
+			expectedOutput: "OK\r\noutput by test2\r\noutput by test1",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create HTTP client config with the test middlewares
+			clientConfig := ClientConfig{
+				Endpoint:    server.URL,
+				Middlewares: tc.middlewares,
+			}
+
+			// Create the client
+			client, err := clientConfig.ToClient(context.Background(), host, componenttest.NewNopTelemetrySettings())
+			require.NoError(t, err)
+
+			// Create a request to the test server
+			req, err := http.NewRequest(http.MethodGet, server.URL, nil)
+			require.NoError(t, err)
+
+			// Send the request
+			resp, err := client.Do(req)
+			require.NoError(t, err)
+			defer resp.Body.Close()
+
+			// Check the response
+			body, err := io.ReadAll(resp.Body)
+			require.NoError(t, err)
+			assert.Equal(t, tc.expectedOutput, string(body))
+		})
+	}
+}
+
+func TestClientMiddlewareErrors(t *testing.T) {
+	// Create a test server that returns "OK"
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte("OK"))
+	}))
+	defer server.Close()
+
+	// Test cases for HTTP client middleware errors
+	httpTests := []struct {
+		name    string
+		host    component.Host
+		config  ClientConfig
+		errText string
+	}{
+		{
+			name: "extension_not_found",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{},
+			},
+			config: ClientConfig{
+				Endpoint: server.URL,
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("nonexistent"),
+					},
+				},
+			},
+			errText: "failed to resolve middleware \"nonexistent\": middleware not found",
+		},
+		{
+			name: "get_round_tripper_fails",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{
+					component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("http middleware error")),
+				},
+			},
+			config: ClientConfig{
+				Endpoint: server.URL,
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("errormw"),
+					},
+				},
+			},
+			errText: "http middleware error",
+		},
+	}
+
+	for _, tc := range httpTests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Trying to create the client should fail
+			_, err := tc.config.ToClient(context.Background(), tc.host, componenttest.NewNopTelemetrySettings())
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tc.errText)
+		})
+	}
+}
+
+// Test failures for gRPC client middlewares by creating a mock implementation
+// that can fail in similar ways to HTTP clients
+func TestGRPCClientMiddlewareErrors(t *testing.T) {
+	// Test cases for gRPC client middleware errors
+	grpcTests := []struct {
+		name    string
+		host    component.Host
+		config  ClientConfig
+		errText string
+	}{
+		{
+			name: "grpc_extension_not_found",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{},
+			},
+			config: ClientConfig{
+				Endpoint: "localhost:1234",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("nonexistent"),
+					},
+				},
+			},
+			errText: "failed to resolve middleware \"nonexistent\": middleware not found",
+		},
+		{
+			name: "grpc_get_client_options_fails",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{
+					component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("grpc middleware error")),
+				},
+			},
+			config: ClientConfig{
+				Endpoint: "localhost:1234",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("errormw"),
+					},
+				},
+			},
+			errText: "grpc middleware error",
+		},
+	}
+
+	for _, tc := range grpcTests {
+		t.Run(tc.name, func(t *testing.T) {
+			// For gRPC, we need to use the configgrpc.ClientConfig structure
+			// We'll test the middleware failure path here using the HTTP client approach,
+			// as the middleware resolution logic is the same
+			_, err := tc.config.ToClient(context.Background(), tc.host, componenttest.NewNopTelemetrySettings())
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tc.errText)
+		})
+	}
+}
diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go
index ae6abc73743..a7c069048d6 100644
--- a/config/confighttp/confighttp.go
+++ b/config/confighttp/confighttp.go
@@ -27,6 +27,7 @@ import (
 	"go.opentelemetry.io/collector/config/configauth"
 	"go.opentelemetry.io/collector/config/configcompression"
 	"go.opentelemetry.io/collector/config/confighttp/internal"
+	"go.opentelemetry.io/collector/config/configmiddleware"
 	"go.opentelemetry.io/collector/config/configopaque"
 	"go.opentelemetry.io/collector/config/configtls"
 	"go.opentelemetry.io/collector/extension/extensionauth"
@@ -111,6 +112,11 @@ type ClientConfig struct {
 	HTTP2PingTimeout time.Duration `mapstructure:"http2_ping_timeout,omitempty"`
 	// Cookies configures the cookie management of the HTTP client.
 	Cookies *CookiesConfig `mapstructure:"cookies,omitempty"`
+
+	// Middlewares are used to add custom functionality to the HTTP client.
+	// Middleware handlers are called in the order they appear in this list,
+	// with the first middleware becoming the outermost handler.
+	Middlewares []configmiddleware.Config `mapstructure:"middleware,omitempty"`
 }
 
 // CookiesConfig defines the configuration of the HTTP client regarding cookies served by the server.
@@ -195,6 +201,22 @@ func (hcs *ClientConfig) ToClient(ctx context.Context, host component.Host, sett
 
 	clientTransport := (http.RoundTripper)(transport)
 
+	// Apply middlewares in reverse order so they execute in
+	// forward order. The first middleware runs after authentication.
+	for i := len(hcs.Middlewares) - 1; i >= 0; i-- {
+		var wrapper func(http.RoundTripper) (http.RoundTripper, error)
+		wrapper, err = hcs.Middlewares[i].GetHTTPClientRoundTripper(ctx, host.GetExtensions())
+		// If we failed to get the middleware
+		if err != nil {
+			return nil, err
+		}
+		clientTransport, err = wrapper(clientTransport)
+		// If we failed to construct a wrapper
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	// The Auth RoundTripper should always be the innermost to ensure that
 	// request signing-based auth mechanisms operate after compression
 	// and header middleware modifies the request
@@ -339,6 +361,11 @@ type ServerConfig struct {
 	// is zero, the value of ReadTimeout is used. If both are
 	// zero, there is no timeout.
 	IdleTimeout time.Duration `mapstructure:"idle_timeout"`
+
+	// Middlewares are used to add custom functionality to the HTTP server.
+	// Middleware handlers are called in the order they appear in this list,
+	// with the first middleware becoming the outermost handler.
+	Middlewares []configmiddleware.Config `mapstructure:"middleware,omitempty"`
 }
 
 // NewDefaultServerConfig returns ServerConfig type object with default values.
@@ -414,7 +441,7 @@ func WithDecoder(key string, dec func(body io.ReadCloser) (io.ReadCloser, error)
 }
 
 // ToServer creates an http.Server from settings object.
-func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) {
+func (hss *ServerConfig) ToServer(ctx context.Context, host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) {
 	serverOpts := &toServerOptions{}
 	serverOpts.Apply(opts...)
 
@@ -426,6 +453,22 @@ func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settin
 		hss.CompressionAlgorithms = defaultCompressionAlgorithms
 	}
 
+	// Apply middlewares in reverse order so they execute in
+	// forward order.  The first middleware runs after
+	// decompression, below, preceded by Auth, CORS, etc.
+	for i := len(hss.Middlewares) - 1; i >= 0; i-- {
+		wrapper, err := hss.Middlewares[i].GetHTTPServerHandler(ctx, host.GetExtensions())
+		// If we failed to get the middleware
+		if err != nil {
+			return nil, err
+		}
+		handler, err = wrapper(handler)
+		// If we failed to construct a wrapper
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	handler = httpContentDecompressor(
 		handler,
 		hss.MaxRequestBodySize,
diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod
index 2b596b18edf..1c738c63353 100644
--- a/config/confighttp/go.mod
+++ b/config/confighttp/go.mod
@@ -13,11 +13,14 @@ require (
 	go.opentelemetry.io/collector/component/componenttest v0.124.0
 	go.opentelemetry.io/collector/config/configauth v0.124.0
 	go.opentelemetry.io/collector/config/configcompression v1.30.0
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000
 	go.opentelemetry.io/collector/config/configopaque v1.30.0
 	go.opentelemetry.io/collector/config/configtls v1.30.0
 	go.opentelemetry.io/collector/extension v1.30.0
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0
 	go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest v0.124.0
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0
+	go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.0.0-00010101000000-000000000000
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
 	go.opentelemetry.io/otel v1.35.0
 	go.uber.org/goleak v1.3.0
@@ -58,6 +61,8 @@ replace go.opentelemetry.io/collector/config/configauth => ../configauth
 
 replace go.opentelemetry.io/collector/config/configcompression => ../configcompression
 
+replace go.opentelemetry.io/collector/config/configmiddleware => ../configmiddleware
+
 replace go.opentelemetry.io/collector/config/configopaque => ../configopaque
 
 replace go.opentelemetry.io/collector/config/configtls => ../configtls
@@ -66,6 +71,8 @@ replace go.opentelemetry.io/collector/extension => ../../extension
 
 replace go.opentelemetry.io/collector/extension/extensionauth => ../../extension/extensionauth
 
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
 replace go.opentelemetry.io/collector/pdata => ../../pdata
 
 replace go.opentelemetry.io/collector/component => ../../component
@@ -83,3 +90,5 @@ replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telem
 replace go.opentelemetry.io/collector/pipeline => ../../pipeline
 
 replace go.opentelemetry.io/collector/featuregate => ../../featuregate
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/config/confighttp/server_middleware_test.go b/config/confighttp/server_middleware_test.go
new file mode 100644
index 00000000000..ca383ccfe38
--- /dev/null
+++ b/config/confighttp/server_middleware_test.go
@@ -0,0 +1,251 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
+)
+
+// testServerMiddleware is a test implementation of configmiddleware.Config
+type testServerMiddleware struct {
+	extension.Extension
+	extensionmiddleware.GetHTTPHandlerFunc
+}
+
+func newTestServerMiddleware(name string) component.Component {
+	return &testServerMiddleware{
+		Extension: extensionmiddlewaretest.NewNop(),
+		GetHTTPHandlerFunc: func(handler http.Handler) (http.Handler, error) {
+			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				// Append middleware name to the URL path
+				r.URL.Path += name + "/"
+
+				// Call the next handler in the chain
+				handler.ServeHTTP(w, r)
+
+				// Add middleware name to the response
+				_, _ = w.Write([]byte("\r\nserved by " + name))
+			}), nil
+		},
+	}
+}
+
+func newTestServerConfig(name string) configmiddleware.Config {
+	return configmiddleware.Config{
+		ID: component.MustNewID(name),
+	}
+}
+
+func TestServerMiddleware(t *testing.T) {
+	// Register two test extensions
+	host := &mockHost{
+		ext: map[component.ID]component.Component{
+			component.MustNewID("test1"): newTestServerMiddleware("test1"),
+			component.MustNewID("test2"): newTestServerMiddleware("test2"),
+		},
+	}
+
+	// Test with different middleware configurations
+	testCases := []struct {
+		name           string
+		middlewares    []configmiddleware.Config
+		expectedOutput string
+	}{
+		{
+			name:           "no_middlewares",
+			middlewares:    nil,
+			expectedOutput: "OK{/}",
+		},
+		{
+			name: "single_middleware",
+			middlewares: []configmiddleware.Config{
+				newTestServerConfig("test1"),
+			},
+			expectedOutput: "OK{/test1/}\r\nserved by test1",
+		},
+		{
+			name: "multiple_middlewares",
+			middlewares: []configmiddleware.Config{
+				newTestServerConfig("test1"),
+				newTestServerConfig("test2"),
+			},
+			expectedOutput: "OK{/test1/test2/}\r\nserved by test2\r\nserved by test1",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create server config with the test middlewares
+			cfg := ServerConfig{
+				Endpoint:    "localhost:0",
+				Middlewares: tc.middlewares,
+			}
+
+			// Create a test handler that responds with the request path
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				_, _ = w.Write([]byte("OK{" + r.URL.Path + "}"))
+			})
+
+			// Create the server
+			srv, err := cfg.ToServer(
+				context.Background(),
+				host,
+				componenttest.NewNopTelemetrySettings(),
+				handler,
+			)
+			require.NoError(t, err)
+
+			// Create a test request
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+
+			// Create a response recorder
+			rec := httptest.NewRecorder()
+
+			// Serve the request
+			srv.Handler.ServeHTTP(rec, req)
+
+			// Get the response
+			resp := rec.Result()
+			defer resp.Body.Close()
+
+			// Check the response
+			body, err := io.ReadAll(resp.Body)
+			require.NoError(t, err)
+
+			assert.Equal(t, tc.expectedOutput, string(body))
+		})
+	}
+}
+
+func TestServerMiddlewareErrors(t *testing.T) {
+	// Create a basic handler for testing
+	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		_, _ = w.Write([]byte("OK"))
+	})
+
+	// Test cases for HTTP server middleware errors
+	httpTests := []struct {
+		name    string
+		host    component.Host
+		config  ServerConfig
+		errText string
+	}{
+		{
+			name: "extension_not_found",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{},
+			},
+			config: ServerConfig{
+				Endpoint: "localhost:0",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("nonexistent"),
+					},
+				},
+			},
+			errText: "failed to resolve middleware \"nonexistent\": middleware not found",
+		},
+		{
+			name: "get_http_handler_fails",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{
+					component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("http middleware error")),
+				},
+			},
+			config: ServerConfig{
+				Endpoint: "localhost:0",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("errormw"),
+					},
+				},
+			},
+			errText: "http middleware error",
+		},
+	}
+
+	for _, tc := range httpTests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Trying to create the server should fail
+			_, err := tc.config.ToServer(
+				context.Background(),
+				tc.host,
+				componenttest.NewNopTelemetrySettings(),
+				handler,
+			)
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tc.errText)
+		})
+	}
+
+	// Test cases for gRPC server middleware errors
+	grpcTests := []struct {
+		name    string
+		host    component.Host
+		config  ServerConfig
+		errText string
+	}{
+		{
+			name: "grpc_extension_not_found",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{},
+			},
+			config: ServerConfig{
+				Endpoint: "localhost:0",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("nonexistent"),
+					},
+				},
+			},
+			errText: "failed to resolve middleware \"nonexistent\": middleware not found",
+		},
+		{
+			name: "get_grpc_handler_fails",
+			host: &mockHost{
+				ext: map[component.ID]component.Component{
+					component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("grpc middleware error")),
+				},
+			},
+			config: ServerConfig{
+				Endpoint: "localhost:0",
+				Middlewares: []configmiddleware.Config{
+					{
+						ID: component.MustNewID("errormw"),
+					},
+				},
+			},
+			errText: "grpc middleware error",
+		},
+	}
+
+	for _, tc := range grpcTests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Trying to create the server should fail
+			_, err := tc.config.ToServer(
+				context.Background(),
+				tc.host,
+				componenttest.NewNopTelemetrySettings(),
+				handler,
+			)
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tc.errText)
+		})
+	}
+}
diff --git a/config/confighttp/xconfighttp/go.mod b/config/confighttp/xconfighttp/go.mod
index 53545c9d538..242306f266d 100644
--- a/config/confighttp/xconfighttp/go.mod
+++ b/config/confighttp/xconfighttp/go.mod
@@ -30,9 +30,11 @@ require (
 	go.opentelemetry.io/collector/component v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configauth v0.124.0 // indirect
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/config/configopaque v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configtls v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
 	go.opentelemetry.io/collector/internal/telemetry v0.124.0 // indirect
 	go.opentelemetry.io/collector/pdata v1.30.0 // indirect
@@ -83,3 +85,9 @@ replace go.opentelemetry.io/collector/pipeline => ../../../pipeline
 replace go.opentelemetry.io/collector/internal/telemetry => ../../../internal/telemetry
 
 replace go.opentelemetry.io/collector/featuregate => ../../../featuregate
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/exporter/otlphttpexporter/go.mod b/exporter/otlphttpexporter/go.mod
index 779acf340a0..8a6fff2129f 100644
--- a/exporter/otlphttpexporter/go.mod
+++ b/exporter/otlphttpexporter/go.mod
@@ -56,11 +56,13 @@ require (
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/collector/client v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configauth v0.124.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.124.0 // indirect
 	go.opentelemetry.io/collector/consumer/consumertest v0.124.0 // indirect
 	go.opentelemetry.io/collector/consumer/xconsumer v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/xextension v0.124.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
 	go.opentelemetry.io/collector/internal/telemetry v0.124.0 // indirect
@@ -161,3 +163,9 @@ replace go.opentelemetry.io/collector/featuregate => ../../featuregate
 replace go.opentelemetry.io/collector/extension/xextension => ../../extension/xextension
 
 replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/extension/extensionmiddleware/client_test.go b/extension/extensionmiddleware/client_test.go
index c525ffa3b76..8d4eadcade2 100644
--- a/extension/extensionmiddleware/client_test.go
+++ b/extension/extensionmiddleware/client_test.go
@@ -32,19 +32,6 @@ func TestGetHTTPRoundTripperFunc(t *testing.T) {
 		require.Equal(t, baseRT, rt)
 	})
 
-	t.Run("wrapping function", func(t *testing.T) {
-		// Create a custom round tripper wrapper for testing
-		customRT := &testRoundTripper{base: baseRT}
-
-		wrapperFunc := GetHTTPRoundTripperFunc(func(base http.RoundTripper) (http.RoundTripper, error) {
-			return &testRoundTripper{base: base}, nil
-		})
-
-		rt, err := wrapperFunc.GetHTTPRoundTripper(baseRT)
-		require.NoError(t, err)
-		require.IsType(t, customRT, rt, "wrapper function should return the custom round tripper")
-	})
-
 	t.Run("error function", func(t *testing.T) {
 		expectedErr := errors.New("round tripper error")
 		errorFunc := GetHTTPRoundTripperFunc(func(_ http.RoundTripper) (http.RoundTripper, error) {
@@ -58,16 +45,6 @@ func TestGetHTTPRoundTripperFunc(t *testing.T) {
 	})
 }
 
-// testRoundTripper is a simple round tripper implementation for testing
-type testRoundTripper struct {
-	base http.RoundTripper
-}
-
-func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	// Just delegate to the base round tripper
-	return t.base.RoundTrip(req)
-}
-
 func TestGetGRPCClientOptionsFunc(t *testing.T) {
 	t.Run("nil function", func(t *testing.T) {
 		var nilFunc GetGRPCClientOptionsFunc
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/nop.go b/extension/extensionmiddleware/extensionmiddlewaretest/nop.go
index 7d8415577cf..b15addccf2d 100644
--- a/extension/extensionmiddleware/extensionmiddlewaretest/nop.go
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/nop.go
@@ -4,6 +4,8 @@
 package extensionmiddlewaretest // import "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
 
 import (
+	"net/http"
+
 	"go.opentelemetry.io/collector/extension"
 )
 
@@ -14,3 +16,12 @@ import (
 func NewNop() extension.Extension {
 	return &baseExtension{}
 }
+
+// HTTPClientFunc implements an HTTP client middleware function.  This
+// is the equivalent of net/http.HandlerFunc for creating a
+// net/http.RoundTripper from a function.
+type HTTPClientFunc func(*http.Request) (*http.Response, error)
+
+func (f HTTPClientFunc) RoundTrip(req *http.Request) (*http.Response, error) {
+	return f(req)
+}
diff --git a/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go b/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go
index 4b136369f75..8c92eca20fe 100644
--- a/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go
+++ b/extension/extensionmiddleware/extensionmiddlewaretest/nop_test.go
@@ -4,6 +4,7 @@
 package extensionmiddlewaretest
 
 import (
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -42,3 +43,19 @@ func TestNopServer(t *testing.T) {
 	require.NoError(t, err)
 	require.Nil(t, grpcOpts)
 }
+
+func TestHTTPClientFunc(t *testing.T) {
+	called := false
+	req := &http.Request{}
+	resp := &http.Response{}
+
+	f := HTTPClientFunc(func(r *http.Request) (*http.Response, error) {
+		require.Equal(t, r, req)
+		called = true
+		return resp, nil
+	})
+
+	result, _ := f.RoundTrip(req)
+	require.True(t, called)
+	require.Equal(t, resp, result)
+}
diff --git a/extension/zpagesextension/go.mod b/extension/zpagesextension/go.mod
index 9ee9b204157..8858109b5c4 100644
--- a/extension/zpagesextension/go.mod
+++ b/extension/zpagesextension/go.mod
@@ -43,9 +43,11 @@ require (
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/collector/client v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/config/configopaque v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configtls v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
 	go.opentelemetry.io/collector/internal/telemetry v0.124.0 // indirect
 	go.opentelemetry.io/collector/pdata v1.30.0 // indirect
@@ -111,3 +113,9 @@ replace go.opentelemetry.io/collector/featuregate => ../../featuregate
 replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest => ../../extension/extensionauth/extensionauthtest
 
 replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../extensionmiddleware
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extensionmiddleware/extensionmiddlewaretest
diff --git a/internal/e2e/go.mod b/internal/e2e/go.mod
index a2aa3087a0d..006d2ff67b9 100644
--- a/internal/e2e/go.mod
+++ b/internal/e2e/go.mod
@@ -83,6 +83,7 @@ require (
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/collector/client v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/connector/xconnector v0.124.0 // indirect
 	go.opentelemetry.io/collector/consumer/consumererror v0.124.0 // indirect
 	go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.124.0 // indirect
@@ -91,6 +92,7 @@ require (
 	go.opentelemetry.io/collector/exporter/xexporter v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/extension/extensiontest v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension/xextension v0.124.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
@@ -261,3 +263,9 @@ replace go.opentelemetry.io/collector/confmap/provider/yamlprovider => ../../con
 replace go.opentelemetry.io/collector/confmap/provider/fileprovider => ../../confmap/provider/fileprovider
 
 replace go.opentelemetry.io/collector/service/hostcapabilities => ../../service/hostcapabilities
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
diff --git a/otelcol/go.mod b/otelcol/go.mod
index 1815d666143..d3aafc87a7d 100644
--- a/otelcol/go.mod
+++ b/otelcol/go.mod
@@ -220,3 +220,9 @@ replace go.opentelemetry.io/collector/confmap/provider/fileprovider => ../confma
 replace go.opentelemetry.io/collector/confmap/provider/yamlprovider => ../confmap/provider/yamlprovider
 
 replace go.opentelemetry.io/collector/internal/telemetry => ../internal/telemetry
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/otelcol/otelcoltest/go.mod b/otelcol/otelcoltest/go.mod
index f8a4d687273..24c4b1ab08f 100644
--- a/otelcol/otelcoltest/go.mod
+++ b/otelcol/otelcoltest/go.mod
@@ -229,3 +229,9 @@ replace go.opentelemetry.io/collector/processor/processortest => ../../processor
 replace go.opentelemetry.io/collector/pdata/testdata => ../../pdata/testdata
 
 replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
diff --git a/receiver/otlpreceiver/go.mod b/receiver/otlpreceiver/go.mod
index 1cb923fe48e..f4538d79c44 100644
--- a/receiver/otlpreceiver/go.mod
+++ b/receiver/otlpreceiver/go.mod
@@ -65,7 +65,9 @@ require (
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/collector/client v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
 	go.opentelemetry.io/collector/pipeline v0.124.0 // indirect
 	go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect
@@ -151,3 +153,9 @@ retract (
 replace go.opentelemetry.io/collector/featuregate => ../../featuregate
 
 replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest => ../../extension/extensionauth/extensionauthtest
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/service/go.mod b/service/go.mod
index 6ab66b2ee64..65549bc6f3d 100644
--- a/service/go.mod
+++ b/service/go.mod
@@ -103,10 +103,12 @@ require (
 	go.opentelemetry.io/collector/client v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configauth v0.124.0 // indirect
 	go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
+	go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
 	go.opentelemetry.io/collector/config/configopaque v1.30.0 // indirect
 	go.opentelemetry.io/collector/config/configtls v1.30.0 // indirect
 	go.opentelemetry.io/collector/consumer/consumererror v0.124.0 // indirect
 	go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
+	go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
 	go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
 	go.opentelemetry.io/contrib/zpages v0.60.0 // indirect
@@ -234,3 +236,9 @@ replace go.opentelemetry.io/collector/otelcol => ../otelcol
 replace go.opentelemetry.io/collector/confmap/provider/yamlprovider => ../confmap/provider/yamlprovider
 
 replace go.opentelemetry.io/collector/confmap/provider/fileprovider => ../confmap/provider/fileprovider
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../extension/extensionmiddleware
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extension/extensionmiddleware/extensionmiddlewaretest
diff --git a/service/hostcapabilities/go.mod b/service/hostcapabilities/go.mod
index 02833db0656..9e33baab67e 100644
--- a/service/hostcapabilities/go.mod
+++ b/service/hostcapabilities/go.mod
@@ -89,3 +89,9 @@ replace go.opentelemetry.io/collector/otelcol => ../../otelcol
 replace go.opentelemetry.io/collector/confmap/provider/fileprovider => ../../confmap/provider/fileprovider
 
 replace go.opentelemetry.io/collector/confmap/provider/yamlprovider => ../../confmap/provider/yamlprovider
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
+
+replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
+
+replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware


Reviews:
user: login: bogdandrutu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T16:50:42Z
user: login: bogdandrutu
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T16:52:50Z
user: login: jmacd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T01:21:34Z
user: login: jmacd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T16:40:17Z
user: login: bogdandrutu
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T20:21:07Z

Review comments:
diff_hunk: @@ -0,0 +1,159 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	name string
+}
+
+func (*testClientMiddleware) Start(_ context.Context, _ component.Host) error {
+	return nil
+}
+
+func (*testClientMiddleware) Shutdown(_ context.Context) error {
+	return nil
+}
path: config/confighttp/client_middleware_test.go
user: login: bogdandrutu
body: same comment about embedding the component funcs.
created_at: 2025-04-17T16:50:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,159 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	name string
+}
+
+func (*testClientMiddleware) Start(_ context.Context, _ component.Host) error {
+	return nil
+}
+
+func (*testClientMiddleware) Shutdown(_ context.Context) error {
+	return nil
+}
+
+var _ extensionmiddleware.HTTPClient = &testClientMiddleware{}
+
+// GetHTTPRoundTripper creates a round tripper that appends text to the response body
+func (m *testClientMiddleware) GetHTTPRoundTripper(transport http.RoundTripper) (http.RoundTripper, error) {
path: config/confighttp/client_middleware_test.go
user: login: bogdandrutu
body: can use the new helper func.
created_at: 2025-04-17T16:51:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,159 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	name string
+}
+
+func (*testClientMiddleware) Start(_ context.Context, _ component.Host) error {
+	return nil
+}
+
+func (*testClientMiddleware) Shutdown(_ context.Context) error {
+	return nil
+}
+
+var _ extensionmiddleware.HTTPClient = &testClientMiddleware{}
+
+// GetHTTPRoundTripper creates a round tripper that appends text to the response body
+func (m *testClientMiddleware) GetHTTPRoundTripper(transport http.RoundTripper) (http.RoundTripper, error) {
+	return &bodyAppenderRoundTripper{
+		name:      m.name,
+		transport: transport,
+	}, nil
+}
+
+// bodyAppenderRoundTripper is a round tripper that appends text to the response body
+type bodyAppenderRoundTripper struct {
+	name      string
+	transport http.RoundTripper
+}
+
+// RoundTrip appends text to the response body
+func (rt *bodyAppenderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	resp, err := rt.transport.RoundTrip(req)
+	if err != nil {
+		return resp, err
+	}
+
+	// Read the original body
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return resp, err
+	}
+	_ = resp.Body.Close()
+
+	// Create a new body with the appended text
+	newBody := string(body) + "\r\noutput by " + rt.name
+
+	// Replace the response body
+	resp.Body = io.NopCloser(strings.NewReader(newBody))
+	resp.ContentLength = int64(len(newBody))
+
+	return resp, nil
+}
+
+func newTestClientMiddleware(name string) component.Component {
+	return &testClientMiddleware{
+		name: name,
+	}
+}
+
+func newTestClientConfig(name string) configmiddleware.Middleware {
+	return configmiddleware.Middleware{
+		MiddlewareID: component.MustNewID(name),
path: config/confighttp/client_middleware_test.go
user: login: bogdandrutu
body: I know I did not catch in the 2nd PR. Can we `MiddlewareID` -> `ID`?
created_at: 2025-04-17T16:51:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"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/configmiddleware"
+)
+
+// testServerMiddleware is a test implementation of configmiddleware.Middleware
path: config/confighttp/server_middleware_test.go
user: login: bogdandrutu
body: Same comments to this file as well.
created_at: 2025-04-17T16:52:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,159 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	name string
+}
+
+func (*testClientMiddleware) Start(_ context.Context, _ component.Host) error {
+	return nil
+}
+
+func (*testClientMiddleware) Shutdown(_ context.Context) error {
+	return nil
+}
+
+var _ extensionmiddleware.HTTPClient = &testClientMiddleware{}
+
+// GetHTTPRoundTripper creates a round tripper that appends text to the response body
+func (m *testClientMiddleware) GetHTTPRoundTripper(transport http.RoundTripper) (http.RoundTripper, error) {
+	return &bodyAppenderRoundTripper{
+		name:      m.name,
+		transport: transport,
+	}, nil
+}
+
+// bodyAppenderRoundTripper is a round tripper that appends text to the response body
+type bodyAppenderRoundTripper struct {
+	name      string
+	transport http.RoundTripper
+}
+
+// RoundTrip appends text to the response body
+func (rt *bodyAppenderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	resp, err := rt.transport.RoundTrip(req)
+	if err != nil {
+		return resp, err
+	}
+
+	// Read the original body
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return resp, err
+	}
+	_ = resp.Body.Close()
+
+	// Create a new body with the appended text
+	newBody := string(body) + "\r\noutput by " + rt.name
+
+	// Replace the response body
+	resp.Body = io.NopCloser(strings.NewReader(newBody))
+	resp.ContentLength = int64(len(newBody))
+
+	return resp, nil
+}
+
+func newTestClientMiddleware(name string) component.Component {
+	return &testClientMiddleware{
+		name: name,
+	}
+}
+
+func newTestClientConfig(name string) configmiddleware.Middleware {
+	return configmiddleware.Middleware{
+		MiddlewareID: component.MustNewID(name),
path: config/confighttp/client_middleware_test.go
user: login: jmacd
body: I think we did. I see 
type Config struct {
	// ID specifies the name of the extension to use.
	ID component.ID `mapstructure:"id,omitempty"`
}

created_at: 2025-04-18T01:21:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package confighttp
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"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/configmiddleware"
+	"go.opentelemetry.io/collector/extension/extensionmiddleware"
+)
+
+// testClientMiddleware is a test middleware that appends a string to the response body
+type testClientMiddleware struct {
+	name string
+}
+
+func (*testClientMiddleware) Start(_ context.Context, _ component.Host) error {
+	return nil
+}
+
+func (*testClientMiddleware) Shutdown(_ context.Context) error {
+	return nil
+}
path: config/confighttp/client_middleware_test.go
user: login: jmacd
body: Fixed. Note that I added a new test helper HTTPClientFunc which is the equivalent of net/http's HTTPHandlerFunc.
created_at: 2025-04-18T16:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,3 +16,12 @@ import (
 func NewNop() extension.Extension {
 	return &baseExtension{}
 }
+
+// HTTPClientFunc implements an HTTP client middleware function.  This
+// is the equivalent of net/http.HandlerFunc for creating a
+// net/http.RoundTripper from a function.
+type HTTPClientFunc func(*http.Request) (*http.Response, error)
path: extension/extensionmiddleware/extensionmiddlewaretest/nop.go
user: login: bogdandrutu
body: RoundTripperFunc to be consistent with `http.HandlerFunc`?
created_at: 2025-04-18T20:20:41Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-18T18:45:46Z
body: ## [Codecov](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector/pull/12845?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.54%. Comparing base [(`9a54388`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector/commit/9a54388de63604d0a5f30d8805580dbe4a0f0ed9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) to head [(`abc302d`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector/commit/abc302dcd9d39259d0b102991b3644060571b22b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main   #12845   +/-   ##
=======================================
  Coverage   91.54%   91.54%           
=======================================
  Files         499      499           
  Lines       27052    27076   +24     
=======================================
+ Hits        24764    24788   +24     
  Misses       1809     1809           
  Partials      479      479           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector/pull/12845?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE