Skip to content

Commit b650785

Browse files
[12.x] Allow down command --retry option to accept datetime values (#58509)
* Enhance `down` command to support datetime for Retry-After header/option * formatting * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent b877493 commit b650785

File tree

2 files changed

+80
-4
lines changed

2 files changed

+80
-4
lines changed

src/Illuminate/Foundation/Console/DownCommand.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace Illuminate\Foundation\Console;
44

55
use App\Http\Middleware\PreventRequestsDuringMaintenance;
6+
use DateTimeInterface;
67
use Exception;
78
use Illuminate\Console\Command;
89
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
910
use Illuminate\Foundation\Exceptions\RegisterErrorViewPaths;
11+
use Illuminate\Support\Carbon;
1012
use Illuminate\Support\Str;
1113
use Symfony\Component\Console\Attribute\AsCommand;
1214
use Throwable;
@@ -21,7 +23,7 @@ class DownCommand extends Command
2123
*/
2224
protected $signature = 'down {--redirect= : The path that users should be redirected to}
2325
{--render= : The view that should be prerendered for display during maintenance mode}
24-
{--retry= : The number of seconds after which the request may be retried}
26+
{--retry= : The number of seconds or the datetime after which the request may be retried}
2527
{--refresh= : The number of seconds after which the browser may refresh}
2628
{--secret= : The secret phrase that may be used to bypass maintenance mode}
2729
{--with-secret : Generate a random secret phrase that may be used to bypass maintenance mode}
@@ -135,15 +137,29 @@ protected function prerenderView()
135137
}
136138

137139
/**
138-
* Get the number of seconds the client should wait before retrying their request.
140+
* Get the number of seconds or date / time the client should wait before retrying their request.
139141
*
140-
* @return int|null
142+
* @return int|string|null
141143
*/
142144
protected function getRetryTime()
143145
{
144146
$retry = $this->option('retry');
145147

146-
return is_numeric($retry) && $retry > 0 ? (int) $retry : null;
148+
if (is_numeric($retry) && $retry > 0) {
149+
return (int) $retry;
150+
}
151+
152+
if (is_string($retry) && ! empty($retry)) {
153+
try {
154+
$date = Carbon::parse($retry);
155+
156+
return $date->format(DateTimeInterface::RFC7231);
157+
} catch (Exception) {
158+
return null;
159+
}
160+
}
161+
162+
return null;
147163
}
148164

149165
/**

tests/Integration/Foundation/MaintenanceModeTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Tests\Integration\Foundation;
44

5+
use DateTimeInterface;
56
use Illuminate\Foundation\Console\DownCommand;
67
use Illuminate\Foundation\Console\UpCommand;
78
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
@@ -12,6 +13,7 @@
1213
use Illuminate\Support\Facades\Event;
1314
use Illuminate\Support\Facades\Route;
1415
use Orchestra\Testbench\TestCase;
16+
use PHPUnit\Framework\Attributes\DataProvider;
1517
use Symfony\Component\HttpFoundation\Cookie;
1618

1719
class MaintenanceModeTest extends TestCase
@@ -192,4 +194,62 @@ public function testDispatchEventWhenMaintenanceModeIsDisabled()
192194
$this->artisan(UpCommand::class);
193195
Event::assertDispatched(MaintenanceModeDisabled::class);
194196
}
197+
198+
#[DataProvider('retryAfterDatetimeProvider')]
199+
public function testMaintenanceModeRetryCanAcceptDatetime(string $datetime): void
200+
{
201+
$this->artisan(DownCommand::class, ['--retry' => $datetime]);
202+
203+
$data = json_decode(file_get_contents(storage_path('framework/down')), true);
204+
205+
$expectedDate = Carbon::parse($datetime)->format(DateTimeInterface::RFC7231);
206+
$this->assertSame($expectedDate, $data['retry']);
207+
}
208+
209+
public static function retryAfterDatetimeProvider(): array
210+
{
211+
return [
212+
'ISO 8601 format' => [date('Y-m-d H:i:s', strtotime('+1 week'))],
213+
'natural language' => ['tomorrow 14:00'],
214+
'relative time' => ['+2 hours'],
215+
];
216+
}
217+
218+
public function testMaintenanceModeRetryWithHttpDateHeader(): void
219+
{
220+
$retryDate = Carbon::now()->addWeek();
221+
$expectedHeader = $retryDate->format(DateTimeInterface::RFC7231);
222+
223+
file_put_contents(storage_path('framework/down'), json_encode([
224+
'retry' => $expectedHeader,
225+
]));
226+
227+
Route::get('/foo', fn () => 'Hello World')->middleware(PreventRequestsDuringMaintenance::class);
228+
229+
$response = $this->get('/foo');
230+
231+
$response->assertStatus(503);
232+
$response->assertHeader('Retry-After', $expectedHeader);
233+
}
234+
235+
public function testMaintenanceModeRetryWithInvalidDatetimeReturnsNull(): void
236+
{
237+
$this->artisan(DownCommand::class, ['--retry' => 'not-a-valid-date']);
238+
239+
$data = json_decode(file_get_contents(storage_path('framework/down')), true);
240+
241+
$this->assertNull($data['retry']);
242+
}
243+
244+
public function testMaintenanceModeRetryWithAtTimestampNotation(): void
245+
{
246+
$futureTimestamp = time() + 3600;
247+
248+
$this->artisan(DownCommand::class, ['--retry' => '@'.$futureTimestamp]);
249+
250+
$data = json_decode(file_get_contents(storage_path('framework/down')), true);
251+
252+
$expectedDate = Carbon::createFromTimestamp($futureTimestamp)->format(DateTimeInterface::RFC7231);
253+
$this->assertSame($expectedDate, $data['retry']);
254+
}
195255
}

0 commit comments

Comments
 (0)