fix(media): treat any download failure (timeout/DNS/refused) as a failed fetch
MediaAttacher::download() only caught RuntimeException, but a connection-level failure throws Illuminate\Http\Client\ConnectionException (extends Exception, not RuntimeException) — e.g. a slow/unreachable proxy hitting the 20s timeout. On the API hosting path that propagated as a 500 (not the promised 422), skipped the batch rollback (orphaning an already-hosted item), and leaked the temp file. Catch Throwable so any fetch failure returns null → the caller rejects cleanly with 422 and rolls back. Also hardens the existing MCP/REST attach-from-url paths (a timeout there now reports a failed URL instead of 500).
This commit is contained in:
parent
a6bb39cc1a
commit
a63e9ea778
2 changed files with 34 additions and 1 deletions
|
|
@ -10,6 +10,7 @@
|
|||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Downloads public URLs and attaches them as media to a post — used by
|
||||
|
|
@ -153,7 +154,8 @@ private function download(string $url): ?array
|
|||
},
|
||||
])
|
||||
->get($url);
|
||||
} catch (RuntimeException) {
|
||||
} catch (Throwable) {
|
||||
// Any fetch failure (timeout, DNS, refused, oversize) is a failed download.
|
||||
@unlink($temp);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -299,6 +300,36 @@
|
|||
expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('rejects and rolls back when a media url connection fails (timeout/dns)', function () {
|
||||
$this->socialAccount->update(['is_active' => true]);
|
||||
|
||||
Http::fake([
|
||||
'cdn.example.com/good.jpg' => Http::response(
|
||||
file_get_contents(__DIR__.'/../../fixtures/1x1.png'),
|
||||
200,
|
||||
['Content-Type' => 'image/png'],
|
||||
),
|
||||
'cdn.example.com/timeout.jpg' => fn () => throw new ConnectionException('Connection timed out'),
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'content' => 'Timeout media post',
|
||||
'media' => [
|
||||
['url' => 'https://cdn.example.com/good.jpg'],
|
||||
['url' => 'https://cdn.example.com/timeout.jpg'],
|
||||
],
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
||||
],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['media']);
|
||||
|
||||
expect(Post::where('content', 'Timeout media post')->exists())->toBeFalse();
|
||||
expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('rejects creating a post when an external media url is not a supported type', function () {
|
||||
$this->socialAccount->update(['is_active' => true]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue