sourceUrlForWorker($job); try { $response = $this->http($timeout) ->post($this->workerEndpoint($workerUrl, '/v1/upscale'), [ 'job_id' => (int) $job->id, 'source_url' => $sourceUrl, 'scale' => (int) $job->scale, 'mode' => (string) $job->mode, 'output_format' => 'webp', ]); } catch (ConnectionException $exception) { throw $this->wrapHttpException($exception, $job, 'upscale'); } $payload = $this->decodeWorkerPayload($response); [$binary, $cleanupFilename] = $this->resolveWorkerOutputBinary($payload, $workerUrl, $token, $timeout, $job); $validated = $this->validateOutputBinary($binary); $stored = $this->storage->putOutputBinary($job, $binary, $validated['mime']); if ($cleanupFilename !== null) { $this->deleteWorkerResult($workerUrl, $cleanupFilename, $token, $timeout, $job); } $metadata = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : []; $metadata['source_transport'] = str_contains($sourceUrl, '/internal/enhance/source/') ? 'signed-route' : 'temporary-url'; return new EnhanceProcessorResult( disk: $stored['disk'], path: $stored['path'], width: (int) $validated['width'], height: (int) $validated['height'], filesize: (int) $validated['filesize'], mime: (string) $validated['mime'], metadata: $metadata, ); } private function http(int $timeout): PendingRequest { return Http::timeout($timeout) ->acceptJson() ->asJson() ->withToken((string) config('enhance.external_worker.token')); } private function decodeWorkerPayload(Response $response): array { if (! $response->successful()) { $payload = $response->json(); throw new RuntimeException( $response->status() >= 500 ? 'Worker is unavailable.' : $this->normalizeWorkerError(is_array($payload) ? ($payload['error'] ?? null) : null, 'Worker rejected the image.'), ); } $payload = $response->json(); if (! is_array($payload) || ! ($payload['success'] ?? false)) { throw new RuntimeException( $this->normalizeWorkerError(is_array($payload) ? ($payload['error'] ?? null) : null, 'Worker returned an invalid response.'), ); } return $payload; } private function resolveWorkerOutputBinary(array $payload, string $workerUrl, string $token, int $timeout, EnhanceJob $job): array { $base64 = trim((string) ($payload['output_base64'] ?? '')); if ($base64 !== '') { $binary = base64_decode($base64, true); if (! is_string($binary) || $binary === '') { throw new RuntimeException('Worker returned an invalid response.'); } return [$binary, null]; } $outputUrl = trim((string) ($payload['output_url'] ?? '')); if ($outputUrl === '') { throw new RuntimeException('Worker returned an invalid response.'); } $safeOutputUrl = $this->normalizeWorkerOutputUrl($workerUrl, $outputUrl); try { $outputResponse = Http::timeout($timeout) ->withToken($token) ->get($safeOutputUrl); } catch (ConnectionException $exception) { throw $this->wrapHttpException($exception, $job, 'download'); } if (! $outputResponse->successful()) { throw new RuntimeException('Worker returned an invalid response.'); } $binary = $outputResponse->body(); if ($binary === '') { throw new RuntimeException('Worker returned an invalid response.'); } $path = trim((string) parse_url($safeOutputUrl, PHP_URL_PATH)); $filename = basename($path); return [$binary, $filename !== '' ? $filename : null]; } private function validateOutputBinary(string $binary): array { $maxBytes = max(1, (int) config('enhance.external_worker.max_download_mb', 60)) * 1024 * 1024; if (strlen($binary) > $maxBytes) { throw new RuntimeException('The upscaled output exceeded the maximum allowed size.'); } $dimensions = @getimagesizefromstring($binary); if (! is_array($dimensions)) { throw new RuntimeException('Worker returned an invalid response.'); } $width = (int) ($dimensions[0] ?? 0); $height = (int) ($dimensions[1] ?? 0); $maxWidth = max(1, (int) config('enhance.max_output_width', 8192)); $maxHeight = max(1, (int) config('enhance.max_output_height', 8192)); if ($width < 1 || $height < 1 || $width > $maxWidth || $height > $maxHeight) { throw new RuntimeException('Worker returned an invalid response.'); } $mime = strtolower((string) ((new \finfo(FILEINFO_MIME_TYPE))->buffer($binary) ?: '')); if (! in_array($mime, (array) config('enhance.allowed_mimes', []), true)) { throw new RuntimeException('Worker returned an invalid response.'); } return [ 'width' => $width, 'height' => $height, 'filesize' => strlen($binary), 'mime' => $mime, ]; } private function sourceUrlForWorker(EnhanceJob $job): string { $disk = Storage::disk($job->source_disk ?: $this->storage->diskName()); $path = ltrim(trim((string) $job->source_path), '/'); if ($path === '') { throw new RuntimeException('The source file could not be downloaded by the worker.'); } try { if (method_exists($disk, 'providesTemporaryUrls') && $disk->providesTemporaryUrls()) { return $disk->temporaryUrl($path, now()->addMinutes(15)); } } catch (Throwable) { } return URL::temporarySignedRoute( 'enhance.source.download', now()->addMinutes(15), ['enhanceJob' => $job->id], ); } private function normalizeWorkerOutputUrl(string $workerUrl, string $outputUrl): string { if (str_starts_with($outputUrl, '/')) { return rtrim($workerUrl, '/') . $outputUrl; } $workerParts = parse_url($workerUrl); $outputParts = parse_url($outputUrl); if (! is_array($workerParts) || ! is_array($outputParts)) { throw new RuntimeException('Worker returned an invalid response.'); } $sameHost = ($workerParts['scheme'] ?? null) === ($outputParts['scheme'] ?? null) && ($workerParts['host'] ?? null) === ($outputParts['host'] ?? null) && (($workerParts['port'] ?? null) === ($outputParts['port'] ?? null)); if (! $sameHost) { throw new RuntimeException('Worker returned an invalid response.'); } return $outputUrl; } private function deleteWorkerResult(string $workerUrl, string $filename, string $token, int $timeout, EnhanceJob $job): void { $safeFilename = basename($filename); if ($safeFilename === '') { return; } try { Http::timeout(min($timeout, 30)) ->acceptJson() ->withToken($token) ->delete($this->workerEndpoint($workerUrl, '/v1/results/' . rawurlencode($safeFilename))); } catch (ConnectionException $exception) { Log::warning('enhance.external_worker.cleanup_failed', [ 'enhance_job_id' => $job->id, 'message' => $exception->getMessage(), ]); } } private function workerEndpoint(string $workerUrl, string $path): string { return rtrim($workerUrl, '/') . $path; } private function normalizeWorkerError(mixed $error, string $fallback): string { $message = trim((string) $error); if (in_array($message, self::SAFE_WORKER_ERRORS, true)) { return $message; } return $fallback; } private function wrapHttpException(ConnectionException $exception, EnhanceJob $job, string $stage): RuntimeException { $message = str_contains(strtolower($exception->getMessage()), 'timed out') ? 'The enhance worker timed out while processing this image.' : 'Worker is unavailable.'; Log::warning('enhance.external_worker.http_failed', [ 'enhance_job_id' => $job->id, 'stage' => $stage, 'message' => $exception->getMessage(), ]); return new RuntimeException($message, 0, $exception); } }