guzzle/guzzle · error · GuzzleHttp\Exception\InvalidArgumentException
progress client option must be callable
Error message
progress client option must be callable
What it means
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.
Source
Thrown at src/Handler/CurlFactory.php:2680
}
$sslKey = $options['ssl_key'][0];
}
$sslKey = $sslKey ?? $options['ssl_key'];
if (!\is_string($sslKey)) {
throw new InvalidArgumentException('Invalid ssl_key request option');
}
if (self::shouldValidateSslKeyFile($sslKeyType) && !\file_exists($sslKey)) {
throw new InvalidArgumentException(\sprintf('SSL private key not found: %s', Psr7\DiagnosticValue::escape($sslKey)));
}
$conf[\CURLOPT_SSLKEY] = $sslKey;
}
$progress = $options['progress'] ?? null;
if ($progress !== null && !\is_callable($progress)) {
throw new InvalidArgumentException('progress client option must be callable');
}
// The streaming read callback (set by applyBody) aborts the upload on a
// body read failure by returning CURL_READFUNC_ABORT, but PHP ignores
// that integer return before 8.1.17/8.2.4. Install a progress callback
// so older PHP still has a cross-version abort path; the failure is
// classified from the stored request-body exception regardless of errno
// (a truncated request may reach the server first on those versions).
$abortsOnBodyReadFailure = isset($conf[\CURLOPT_READFUNCTION]);
if ($progress !== null || $abortsOnBodyReadFailure) {
/** @var (callable(int, int, int, int): mixed)|null $progress */
$conf[\CURLOPT_NOPROGRESS] = false;
$progressCallback = static function ($resource, $downloadSize, $downloaded, $uploadSize, $uploaded) use ($easy, $progress): int {
// Abort the transfer when the request body read failed (the
// cross-version abort path, since older PHP ignores the read
// callback's return). progressAborted is left unset so the
// failure is classified from the stored request-body exception.View on GitHub (pinned to 9b200fc580)
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.
Example fix
// before ['progress' => [$logger, 'onProgress']] // method renamed to 'progress' // after ['progress' => [$logger, 'progress']]
Defensive patterns
Strategy: type-guard
Validate before calling
if (array_key_exists('progress', $options) && $options['progress'] !== null && !is_callable($options['progress'])) {
throw new InvalidArgumentException('progress client option must be callable');
} Type guard
function isProgressCallable($progress): bool {
return $progress === null || is_callable($progress);
} Prevention
- 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.
When it happens
Trigger: ['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.
Common situations: 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.
Related errors
- auth type must be a string
- auth must contain username and password strings
- sink must be a resource, string, or %s
- %s must be an instance of %s
- handler must be a callable
AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04).
Data as JSON: /data/errors/28874754ddd1c7a5.json.
Report an issue: GitHub.