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

  1. Create the destination directory before the request: mkdir(dirname($sink), 0775, true).
  2. Pass an already-open resource or a StreamInterface as 'sink' to bypass filesystem path validation.
  3. 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

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


AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04). Data as JSON: /data/errors/b3d9e9195c65e227.json. Report an issue: GitHub.