Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error handling and response expectation options to BackgroundSync #242

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions assets/src/backgroundsync-form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion src/CachingStrategy/BackgroundSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions src/Dto/BackgroundSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>
*/
#[SerializedName('expected_status_codes')]
public array $expectedStatusCodes = [];

public string $method;

#[SerializedName('max_retention_time')]
Expand Down
24 changes: 24 additions & 0 deletions src/Resources/config/definition/service_worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
->treatTrueLike([])
->info('The background sync configuration.')
->arrayPrototype()
->addDefaultsIfNotSet()
->children()
->scalarNode('queue_name')
->isRequired()
Expand All @@ -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.')
Expand Down
87 changes: 81 additions & 6 deletions src/WorkboxPlugin/BackgroundSyncPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@

namespace SpomkyLabs\PwaBundle\WorkboxPlugin;

use function count;

final readonly class BackgroundSyncPlugin implements CachePluginInterface, HasDebugInterface
{
private const NAME = 'BackgroundSyncPlugin';

/**
* @param array<int> $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 = [],
) {
}

Expand Down Expand Up @@ -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 = <<<BACKGROUND_SYNC_RULE_STRATEGY
new workbox.backgroundSync.BackgroundSyncPlugin('{$this->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<int> $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
Expand All @@ -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;}},";
}
}