guzzle/guzzle · error · GuzzleHttp\Exception\RequestException
Directory %s does not exist for sink value of %s
Error message
Directory %s does not exist for sink value of %s
What it means
Thrown when the 'sink' option is a string file path but the parent directory (dirname($sink)) does not exist. Guzzle validates the directory up front so the failure happens before cURL writes, rather than mid-transfer. Both the missing directory and the sink value are escaped via DiagnosticValue::escape. Use a resource or StreamInterface as the sink if you do not want a filesystem path.
Source
Thrown at src/Handler/CurlFactory.php:2519
}
}
$streamFactory = self::requireStreamFactory($options[RequestOptions::STREAM_FACTORY] ?? new HttpFactory());
$hasSink = isset($options['sink']);
if (!$hasSink) {
// Use a default temp stream if no sink was set.
$options['sink'] = Psr7\Utils::tryFopen('php://temp', 'w+');
}
$sink = $options['sink'];
if ($hasSink && \is_resource($sink)) {
$sink = self::streamForResourceSink(Psr7\Utils::streamFor($sink));
} elseif (\is_resource($sink)) {
$sink = $streamFactory->createStreamFromResource($sink);
} elseif (!\is_string($sink)) {
$sink = Psr7\Utils::streamFor($sink);
} elseif (!\is_dir(\dirname($sink))) {
// Ensure that the directory exists before failing in curl.
throw new RequestException(\sprintf('Directory %s does not exist for sink value of %s', Psr7\DiagnosticValue::escape(\dirname($sink)), Psr7\DiagnosticValue::escape($sink)), $easy->request);
} else {
$sink = new LazyOpenStream($sink, 'w+');
}
$easy->sink = $sink;
$conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, string $write) use ($easy, $sink): int {
$length = \strlen($write);
try {
$newResponseBodyBytes = TransferByteCounter::add(
$easy->responseBodyBytes,
$length,
'Response body exceeds the maximum integer size supported on this platform'
);
} catch (\OverflowException $e) {
$easy->responseBodySizeException = $e;
return 0;
}View on GitHub (pinned to 9b200fc580)
Solutions
- Create the destination directory before the request: mkdir(dirname($sink), 0775, true).
- Pass an already-open resource or a StreamInterface as 'sink' to bypass filesystem path validation.
- Validate/normalize the configured sink path at application startup and create its parent dir.
Example fix
// before
$client->request('GET', $url, ['sink' => '/var/downloads/report.pdf']);
// after
@mkdir('/var/downloads', 0775, true);
$client->request('GET', $url, ['sink' => '/var/downloads/report.pdf']); Defensive patterns
Strategy: validation
Validate before calling
if (isset($options['sink']) && is_string($options['sink']) && !is_dir(dirname($options['sink']))) {
@mkdir(dirname($options['sink']), 0775, true);
} Type guard
function sinkDirectoryExists($sink): bool {
return !is_string($sink) || is_dir(dirname($sink));
} Prevention
- mkdir the parent directory (recursively) before sending the download request.
- Pass a resource or StreamInterface sink to avoid filesystem path checks.
- Validate configured sink paths at app bootstrap.
When it happens
Trigger: Setting ['sink' => '/tmp/downloads/file.bin'] where /tmp/downloads does not exist; using a configurable output path whose directory was never created; relative sink path expecting a cwd that is not active.
Common situations: Download-to-file features where the user-supplied output directory was not mkdir'd; containerized deployments missing a volume mount path; path templating that produces an unexpected directory.
Related errors
- sink must be a resource, string, or %s
- auth type must be a string
- auth must contain username and password strings
- Failed to write the response body
- %s must be an instance of %s
AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04).
Data as JSON: /data/errors/b3d9e9195c65e227.json.
Report an issue: GitHub.