{"record":{"id":"2a854c39738a765f","repo":"symfony/http-kernel","slug":"the-value-of-the-key-option-of-the-s-attribute-must-evaluate","errorCode":null,"errorMessage":"The value of the \"$key\" option of the \"%s\" attribute must evaluate to a string, \"%s\" given.","messagePattern":"The value of the \"\\$key\" option of the \"(.+?)\" attribute must evaluate to a string, \"(.+?)\" given\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"EventListener/RateLimitAttributeListener.php","lineNumber":63,"sourceCode":"     * @param ControllerAttributeEvent<RateLimit, ControllerArgumentsEvent> $event\n     */\n    public function onKernelControllerAttribute(ControllerAttributeEvent $event, ?string $eventName = null, ?EventDispatcherInterface $dispatcher = null): void\n    {\n        $request = $event->kernelEvent->getRequest();\n        $attribute = $event->attribute;\n\n        if ($attribute->methods && !\\in_array($request->getMethod(), $attribute->methods, true)) {\n            return;\n        }\n\n        if (!$this->limiters->has($attribute->limiter)) {\n            throw new \\InvalidArgumentException(\\sprintf('Rate limiter \"%s\" does not exist. Did you forget to configure it? Available limiters: \"%s\".', $attribute->limiter, implode('\", \"', array_keys($this->limiters->getProvidedServices()))));\n        }\n\n        if (null === $attribute->key) {\n            $key = ($request->getClientIp() ?? 'unknown').'~'.$request->getMethod().'~'.$request->getPathInfo();\n        } elseif (!\\is_string($key = $event->evaluate($attribute->key))) {\n            throw new \\TypeError(\\sprintf('The value of the \"$key\" option of the \"%s\" attribute must evaluate to a string, \"%s\" given.', RateLimit::class, get_debug_type($key)));\n        }\n\n        $rateLimit = $this->limiters->get($attribute->limiter)->create($key)->consume($attribute->tokens);\n\n        $candidate = $attribute->exposeHeaders && null !== $rateLimit->getResetAt()\n            ? new AppliedRateLimit($rateLimit, $attribute->tokens)\n            : null;\n\n        if (!$rateLimit->isAccepted()) {\n            $request->attributes->set(self::RATE_LIMIT_ATTRIBUTE, $candidate);\n\n            if ($dispatcher && class_exists(RateLimitExceededEvent::class)) {\n                $dispatcher->dispatch(new RateLimitExceededEvent($rateLimit, $attribute->limiter, $key));\n            }\n\n            throw new TooManyRequestsHttpException(max(0, $rateLimit->getRetryAfter()->getTimestamp() - time()));\n        }\n","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/symfony/http-kernel/blob/aa3a39d7286a62cdfea98f0e69c651a3da6e36cf/EventListener/RateLimitAttributeListener.php#L45-L81","documentation":"The #[RateLimit] key option may be a string expression evaluated via the ControllerArgumentsEvent evaluator; its result is used as the limiter bucket key. If the evaluation yields a non-string, a TypeError is thrown naming the actual type.","triggerScenarios":"Annotating with #[RateLimit(key: 'expr...')] where the expression evaluates to an int, array, or null instead of a string, during onKernelControllerAttribute.","commonSituations":"Using key: 'request.headers.get(\"X-Api-Key\")' which returns null when the header is missing; an expression returning a numeric user id rather than casting it to string.","solutions":["Make the expression return a string, e.g. key: \"request.headers.get('X-Api-Key', 'anonymous')\".","Cast inside the expression or use a fallback: key: \"user.getId() ~ ''\" is discouraged; prefer explicit string functions.","Ensure default values are strings when header/attribute may be absent."],"exampleFix":"// before\n#[RateLimit(limiter: 'basic', key: 'request.headers.get(\"X-Api-Key\")')]\n// after\n#[RateLimit(limiter: 'basic', key: \"request.headers.get('X-Api-Key', 'anonymous')\")]","handlingStrategy":"type-guard","validationCode":"$key = $attribute->key;\nif ($key !== null && !is_string($key)) { /* expression must be string-typed source */ }","typeGuard":"function isValidRateLimitKey(mixed $evaluated): bool { return is_string($evaluated); }","tryCatchPattern":"try { $listener->onKernelControllerAttribute($event, 'kernel.controller', ...); } catch (\\TypeError $e) { /* fall back to IP-based key */ }","preventionTips":["Provide non-null defaults in key expressions (headers.get with default)","Cast numeric ids to string in expressions","Cover key expressions with unit tests asserting string results"],"tags":["type-mismatch","rate-limiting","expression","symfony"],"backgroundTag":"type-mismatch","analyzedSha":"aa3a39d7286a62cdfea98f0e69c651a3da6e36cf","analyzedAt":"2026-09-13T18:03:36.509Z","contentChangedAt":"2026-09-13T18:03:36.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}