From 60444f3a5e1d329319b6557a794dd0c047c83ee4 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 5 Oct 2024 14:57:37 +0200 Subject: [PATCH] Add error handling and response expectation options to BackgroundSync (#242) Added configurations for handling 4xx and 5xx errors, expected status codes, and redirect responses in the BackgroundSync functionality. Updated the associated DTO and classes to support these new options, enhancing the robustness and flexibility of background synchronization. --- assets/src/backgroundsync-form_controller.js | 26 +++--- src/CachingStrategy/BackgroundSync.php | 6 +- src/Dto/BackgroundSync.php | 15 ++++ .../config/definition/service_worker.php | 24 +++++ src/WorkboxPlugin/BackgroundSyncPlugin.php | 87 +++++++++++++++++-- 5 files changed, 141 insertions(+), 17 deletions(-) diff --git a/assets/src/backgroundsync-form_controller.js b/assets/src/backgroundsync-form_controller.js index ac47b58..28a9238 100644 --- a/assets/src/backgroundsync-form_controller.js +++ b/assets/src/backgroundsync-form_controller.js @@ -31,16 +31,22 @@ export default class extends Controller { try { const params = this.paramsValue; params.headers = this.headersValue; - if (form.enctype === 'multipart/form-data') { - params.body = new FormData(form); - } else if (form.enctype === 'application/json') { - params.body = JSON.stringify(Object.fromEntries(new FormData(form))); - } else if (form.enctype === 'application/x-www-form-urlencoded') { - params.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - params.body = new URLSearchParams(new FormData(form)); - } else { - // Unsupported form enctype - return; + switch (form.enctype) { + case 'multipart/form-data': + params.headers['Content-Type'] = 'multipart/form-data'; + params.body = new FormData(form); + break; + case 'application/json': + params.headers['Content-Type'] = 'application/json'; + params.body = JSON.stringify(Object.fromEntries(new FormData(form))); + break; + case 'application/x-www-form-urlencoded': + params.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + params.body = (new URLSearchParams(new FormData(form))).toString(); + break; + default: + console.error('Unknown form enctype'); + return; } params.method = form.method.toUpperCase(); const response = await fetch(url, params); diff --git a/src/CachingStrategy/BackgroundSync.php b/src/CachingStrategy/BackgroundSync.php index a62ae84..40bf521 100644 --- a/src/CachingStrategy/BackgroundSync.php +++ b/src/CachingStrategy/BackgroundSync.php @@ -51,7 +51,11 @@ public function getCacheStrategies(): array $sync->queueName, $sync->maxRetentionTime, $sync->forceSyncFallback, - $sync->broadcastChannel + $sync->broadcastChannel, + $sync->errorOn4xx, + $sync->errorOn5xx, + $sync->expectRedirect, + $sync->expectedStatusCodes, ), ) ->withMethod($sync->method); diff --git a/src/Dto/BackgroundSync.php b/src/Dto/BackgroundSync.php index 0944a93..0451a50 100644 --- a/src/Dto/BackgroundSync.php +++ b/src/Dto/BackgroundSync.php @@ -14,6 +14,21 @@ final class BackgroundSync extends Cache #[SerializedName('match_callback')] public string $matchCallback; + #[SerializedName('error_on_4xx')] + public bool $errorOn4xx = false; + + #[SerializedName('error_on_5xx')] + public bool $errorOn5xx = true; + + #[SerializedName('expect_redirect')] + public bool $expectRedirect = false; + + /** + * @var array + */ + #[SerializedName('expected_status_codes')] + public array $expectedStatusCodes = []; + public string $method; #[SerializedName('max_retention_time')] diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index 2e8e769..b28e3e5 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -393,6 +393,7 @@ ->treatTrueLike([]) ->info('The background sync configuration.') ->arrayPrototype() + ->addDefaultsIfNotSet() ->children() ->scalarNode('queue_name') ->isRequired() @@ -404,6 +405,29 @@ ->info('The regex or callback function to match the URLs.') ->example(['/\/api\//']) ->end() + ->booleanNode('error_on_4xx') + ->defaultTrue() + ->info('Whether to retry the request on 4xx errors.') + ->end() + ->booleanNode('error_on_5xx') + ->defaultTrue() + ->info('Whether to retry the request on 5xx errors.') + ->end() + ->arrayNode('expected_status_codes') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->info( + 'The expected success status codes. If the response status code is not in the list, the request will be retried.' + ) + ->integerPrototype()->end() + ->end() + ->booleanNode('expect_redirect') + ->defaultFalse() + ->info( + 'Whether to expect a redirect (JS response type should be "opaqueredirect" or the "redirected" property is "true").' + ) + ->end() ->scalarNode('method') ->defaultValue('POST') ->info('The HTTP method.') diff --git a/src/WorkboxPlugin/BackgroundSyncPlugin.php b/src/WorkboxPlugin/BackgroundSyncPlugin.php index c1c9bea..f34b7ab 100644 --- a/src/WorkboxPlugin/BackgroundSyncPlugin.php +++ b/src/WorkboxPlugin/BackgroundSyncPlugin.php @@ -4,15 +4,24 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function count; + final readonly class BackgroundSyncPlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'BackgroundSyncPlugin'; + /** + * @param array $expectedStatusCodes + */ public function __construct( public string $queueName, public bool $forceSyncFallback, public null|string $broadcastChannel, public int $maxRetentionTime, + public bool $errorOn4xx = false, + public bool $errorOn5xx = true, + public bool $expectRedirect = false, + public array $expectedStatusCodes = [], ) { } @@ -42,23 +51,43 @@ public function render(int $jsonOptions = 0): string BROADCAST_CHANNEL; } + $errorOn4xx = $this->getErrorOn4xx(); + $errorOn5xx = $this->getErrorOn5xx(); + $expectRedirect = $this->getExpectRedirect(); + $expectedStatusCodes = $this->getExpectedSuccessStatusCodes(); + $declaration = <<queueName}',{ - "maxRetentionTime": {$this->maxRetentionTime}, - "forceSyncFallback": {$forceSyncFallback}{$broadcastChannelSection} -}) + + {$errorOn4xx}{$errorOn5xx}{$expectRedirect}{$expectedStatusCodes}new workbox.backgroundSync.BackgroundSyncPlugin('{$this->queueName}',{"maxRetentionTime": {$this->maxRetentionTime}, "forceSyncFallback": {$forceSyncFallback}{$broadcastChannelSection}}) + BACKGROUND_SYNC_RULE_STRATEGY; return trim($declaration); } + /** + * @param array $expectedStatusCodes + */ public static function create( string $queueName, int $maxRetentionTime, bool $forceSyncFallback, - null|string $broadcastChannel + null|string $broadcastChannel, + bool $errorOn4xx = false, + bool $errorOn5xx = true, + bool $expectRedirect = false, + array $expectedStatusCodes = [], ): static { - return new self($queueName, $forceSyncFallback, $broadcastChannel, $maxRetentionTime); + return new self( + $queueName, + $forceSyncFallback, + $broadcastChannel, + $maxRetentionTime, + $errorOn4xx, + $errorOn5xx, + $expectRedirect, + $expectedStatusCodes, + ); } public function getDebug(): array @@ -68,6 +97,52 @@ public function getDebug(): array 'forceSyncFallback' => $this->forceSyncFallback, 'broadcastChannel' => $this->broadcastChannel, 'maxRetentionTime' => $this->maxRetentionTime, + 'errorOn4xx' => $this->errorOn4xx, + 'errorOn5xx' => $this->errorOn5xx, + 'expectRedirect' => $this->expectRedirect, + 'expectedSuccessStatusCodes' => $this->expectedStatusCodes, ]; } + + private function getErrorOn4xx(): string + { + if ($this->errorOn5xx === false) { + return ''; + } + + return $this->getErrorOn(400); + } + + private function getErrorOn5xx(): string + { + if ($this->errorOn5xx === false) { + return ''; + } + + return $this->getErrorOn(500); + } + + private function getErrorOn(int $statusCode): string + { + return "{fetchDidSucceed: ({response}) => {if (response.status >= {$statusCode}) {throw new Error('Server error.');}return response;}},"; + } + + private function getExpectedSuccessStatusCodes(): string + { + if (count($this->expectedStatusCodes) === 0) { + return ''; + } + $codes = implode(',', $this->expectedStatusCodes); + + return "{fetchDidSucceed: ({response}) => {if (! [{$codes}].includes(response.status)) {throw new Error('Unexpected response status code. Expected one of [{$codes}]. Got ' + response.status);}return response;}},"; + } + + private function getExpectRedirect(): string + { + if ($this->expectRedirect === false) { + return ''; + } + + return "{fetchDidSucceed: ({response}) => {if (response.type !== 'opaqueredirect' || response.redirect !== true) {throw new Error('Expected a redirect response.');}return response;}},"; + } }