{"id":"28874754ddd1c7a5","repo":"guzzle/guzzle","slug":"progress-client-option-must-be-callable","errorCode":null,"errorMessage":"progress client option must be callable","messagePattern":"progress client option must be callable","errorType":"validation","errorClass":"GuzzleHttp\\Exception\\InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"src/Handler/CurlFactory.php","lineNumber":2680,"sourceCode":"                }\n                $sslKey = $options['ssl_key'][0];\n            }\n\n            $sslKey = $sslKey ?? $options['ssl_key'];\n\n            if (!\\is_string($sslKey)) {\n                throw new InvalidArgumentException('Invalid ssl_key request option');\n            }\n\n            if (self::shouldValidateSslKeyFile($sslKeyType) && !\\file_exists($sslKey)) {\n                throw new InvalidArgumentException(\\sprintf('SSL private key not found: %s', Psr7\\DiagnosticValue::escape($sslKey)));\n            }\n            $conf[\\CURLOPT_SSLKEY] = $sslKey;\n        }\n\n        $progress = $options['progress'] ?? null;\n        if ($progress !== null && !\\is_callable($progress)) {\n            throw new InvalidArgumentException('progress client option must be callable');\n        }\n\n        // The streaming read callback (set by applyBody) aborts the upload on a\n        // body read failure by returning CURL_READFUNC_ABORT, but PHP ignores\n        // that integer return before 8.1.17/8.2.4. Install a progress callback\n        // so older PHP still has a cross-version abort path; the failure is\n        // classified from the stored request-body exception regardless of errno\n        // (a truncated request may reach the server first on those versions).\n        $abortsOnBodyReadFailure = isset($conf[\\CURLOPT_READFUNCTION]);\n\n        if ($progress !== null || $abortsOnBodyReadFailure) {\n            /** @var (callable(int, int, int, int): mixed)|null $progress */\n            $conf[\\CURLOPT_NOPROGRESS] = false;\n            $progressCallback = static function ($resource, $downloadSize, $downloaded, $uploadSize, $uploaded) use ($easy, $progress): int {\n                // Abort the transfer when the request body read failed (the\n                // cross-version abort path, since older PHP ignores the read\n                // callback's return). progressAborted is left unset so the\n                // failure is classified from the stored request-body exception.","sourceCodeStart":2662,"sourceCodeEnd":2698,"githubUrl":"https://github.com/guzzle/guzzle/blob/9b200fc5805036b331d6031199880dadecae0275/src/Handler/CurlFactory.php#L2662-L2698","documentation":"Thrown when the 'progress' client/request option is set to a non-null value that is not callable (does not satisfy is_callable). Guzzle forwards progress to a cURL transfer callback; a non-callable value cannot be invoked and is rejected up front.","triggerScenarios":"['progress' => 'someFunc'] where the function does not exist; ['progress' => [$obj, 'method']] on a non-existent or non-public method; ['progress' => 5]; passing a class name instead of an instance.","commonSituations":"Refactoring renaming a method without updating the progress option; passing a static method string without the class context; serialization/unserialization leaving the target object detached.","solutions":["Pass a valid callable: a closure, a function name string that exists, or a valid [$object, 'method'] array.","Remove the 'progress' option if you no longer need it.","If the callable target is an object method, verify the method exists and is public."],"exampleFix":"// before\n['progress' => [$logger, 'onProgress']]  // method renamed to 'progress'\n// after\n['progress' => [$logger, 'progress']]","handlingStrategy":"type-guard","validationCode":"if (array_key_exists('progress', $options) && $options['progress'] !== null && !is_callable($options['progress'])) {\n    throw new InvalidArgumentException('progress client option must be callable');\n}","typeGuard":"function isProgressCallable($progress): bool {\n    return $progress === null || is_callable($progress);\n}","tryCatchPattern":null,"preventionTips":["Use a closure for progress callbacks so the callable survives refactors.","Verify method existence and visibility before wiring an object-method callback.","Remove the 'progress' option if no longer needed."],"tags":["configuration","callback","validation"],"analyzedSha":"9b200fc5805036b331d6031199880dadecae0275","analyzedAt":"2026-08-04T21:24:26.648Z","schemaVersion":2}