Repository: symfony/symfony
File: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php (PHP)

Code before:

        $container
            ->getDefinition('http_client.uri_template')
            ->setArgument(2, $defaultUriTemplateVars);

        foreach ($config['scoped_clients'] as $name => $scopeConfig) {
            if ($container->has($name)) {
                throw new InvalidArgumentException(\sprintf('Invalid scope name: "%s" is reserved.', $name));
            }

            $scope = $scopeConfig['scope'] ?? null;
            unset($scopeConfig['scope']);
            $cachingOptions = $scopeConfig['caching'] ?? ['enabled' => false];
            unset($scopeConfig['caching']);
            $rateLimiter = $scopeConfig['rate_limiter'] ?? null;
            unset($scopeConfig['rate_limiter']);
            $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false];
            unset($scopeConfig['retry_failed']);

            // This "transport" service is decorated in the following order:
            // 1. ThrottlingHttpClient (30) -> throttles requests
            // 2. CachingHttpClient (25) -> caches responses
            // 3. UriTemplateHttpClient (20) -> expands URI templates
            // 4. ScopingHttpClient (15) -> resolves relative URLs and applies scope configuration
            // 5. RetryableHttpClient (10) -> retries requests
            // 6. TraceableHttpClient (5) -> traces requests
            $container->register($name, HttpClientInterface::class)
                ->setFactory('current')
                ->setArguments([[new Reference('http_client.transport')]])
                ->addTag('http_client.client')
            ;

            $scopingDefinition = $container->register($name.'.scoping', ScopingHttpClient::class)
                ->setDecoratedService($name, null, 15)
                ->addTag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']);

            if (null === $scope) {
                $baseUri = $scopeConfig['base_uri'];
                unset($scopeConfig['base_uri']);

                $scopingDefinition
                    ->setFactory([ScopingHttpClient::class, 'forBaseUri'])
                    ->setArguments([new Reference('.inner'), $baseUri, $scopeConfig]);
            } else {
                $scopingDefinition
                    ->setArguments([new Reference('.inner'), [$scope => $scopeConfig], $scope]);
            }

            if ($this->readConfigEnabled('http_client.scoped_clients.'.$name.'.caching', $container, $cachingOptions)) {
                $this->registerCachingHttpClient($cachingOptions, $scopeConfig, $name, $container);
            }

Review comment (bug):
Looks like I completely messed up the decoration order
I think the left side numbering is the right one:
- first we want to throttle
- then resolve URI
- pass resolved URIs to scoping
- cache
- retry failures
- trace real requests with everything resolved

Code after:

        $container
            ->getDefinition('http_client.uri_template')
            ->setArgument(2, $defaultUriTemplateVars);

        foreach ($config['scoped_clients'] as $name => $scopeConfig) {
            if ($container->has($name)) {
                throw new InvalidArgumentException(\sprintf('Invalid scope name: "%s" is reserved.', $name));
            }

            $scope = $scopeConfig['scope'] ?? null;
            unset($scopeConfig['scope']);
            $cachingOptions = $scopeConfig['caching'] ?? ['enabled' => false];
            unset($scopeConfig['caching']);
            $rateLimiter = $scopeConfig['rate_limiter'] ?? null;
            unset($scopeConfig['rate_limiter']);
            $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false];
            unset($scopeConfig['retry_failed']);

            // This "transport" service is decorated in the following order:
            // 1. ThrottlingHttpClient (5) -> throttles requests
            // 2. UriTemplateHttpClient (10) -> expands URI templates
            // 3. ScopingHttpClient (15) -> resolves relative URLs and applies scope configuration
            // 4. CachingHttpClient (20) -> caches responses
            // 5. RetryableHttpClient (25) -> retries requests
            // 6. TraceableHttpClient (100) -> traces requests
            $container->register($name, HttpClientInterface::class)
                ->setFactory('current')
                ->setArguments([[new Reference('http_client.transport')]])
                ->addTag('http_client.client')
            ;

            $scopingDefinition = $container->register($name.'.scoping', ScopingHttpClient::class)
                ->setDecoratedService($name, null, 15)
                ->addTag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']);

            if (null === $scope) {
                $baseUri = $scopeConfig['base_uri'];
                unset($scopeConfig['base_uri']);

                $scopingDefinition
                    ->setFactory([ScopingHttpClient::class, 'forBaseUri'])
                    ->setArguments([new Reference('.inner'), $baseUri, $scopeConfig]);
            } else {
                $scopingDefinition
                    ->setArguments([new Reference('.inner'), [$scope => $scopeConfig], $scope]);
            }

            if ($this->readConfigEnabled('http_client.scoped_clients.'.$name.'.caching', $container, $cachingOptions)) {
                $this->registerCachingHttpClient($cachingOptions, $scopeConfig, $name, $container);
            }