Skip to content

Commit 99d6276

Browse files
authored
Merge pull request #55713 from nextcloud/feat/taskprocessing/user-facing-error-message
feat(TaskProcessing): user-facing error messages
2 parents 4ecd217 + b5f057c commit 99d6276

File tree

14 files changed

+289
-21
lines changed

14 files changed

+289
-21
lines changed

core/Controller/TaskProcessingApiController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,17 +460,18 @@ public function setProgress(int $taskId, float $progress): DataResponse {
460460
* @param int $taskId The id of the task
461461
* @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
462462
* @param string|null $errorMessage An error message if the task failed
463+
* @param string|null $userFacingErrorMessage An error message that will be shown to the user
463464
* @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
464465
*
465466
* 200: Result updated successfully
466467
* 404: Task not found
467468
*/
468469
#[ExAppRequired]
469470
#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
470-
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
471+
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null, ?string $userFacingErrorMessage = null): DataResponse {
471472
try {
472473
// set result
473-
$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true);
474+
$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, isUsingFileIds: true, userFacingError: $userFacingErrorMessage);
474475
$task = $this->taskProcessingManager->getTask($taskId);
475476

476477
/** @var CoreTaskProcessingTask $json */
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Migrations;
10+
11+
use Closure;
12+
use OCP\DB\ISchemaWrapper;
13+
use OCP\DB\Types;
14+
use OCP\Migration\Attributes\AddColumn;
15+
use OCP\Migration\Attributes\ColumnType;
16+
use OCP\Migration\IOutput;
17+
use OCP\Migration\SimpleMigrationStep;
18+
19+
/**
20+
*
21+
*/
22+
#[AddColumn(table: 'taskprocessing_tasks', name: 'user_facing_error_message', type: ColumnType::STRING)]
23+
class Version33000Date20251013110519 extends SimpleMigrationStep {
24+
25+
/**
26+
* @param IOutput $output
27+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
28+
* @param array $options
29+
* @return null|ISchemaWrapper
30+
*/
31+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
32+
/** @var ISchemaWrapper $schema */
33+
$schema = $schemaClosure();
34+
35+
if ($schema->hasTable('taskprocessing_tasks')) {
36+
$table = $schema->getTable('taskprocessing_tasks');
37+
if (!$table->hasColumn('user_facing_error_message')) {
38+
$table->addColumn('user_facing_error_message', Types::STRING, [
39+
'notnull' => false,
40+
'length' => 4000,
41+
]);
42+
return $schema;
43+
}
44+
}
45+
46+
return null;
47+
}
48+
}

core/openapi-ex_app.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,12 @@
960960
"nullable": true,
961961
"default": null,
962962
"description": "An error message if the task failed"
963+
},
964+
"userFacingErrorMessage": {
965+
"type": "string",
966+
"nullable": true,
967+
"default": null,
968+
"description": "An error message that will be shown to the user"
963969
}
964970
}
965971
}

core/openapi-full.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11068,6 +11068,12 @@
1106811068
"nullable": true,
1106911069
"default": null,
1107011070
"description": "An error message if the task failed"
11071+
},
11072+
"userFacingErrorMessage": {
11073+
"type": "string",
11074+
"nullable": true,
11075+
"default": null,
11076+
"description": "An error message that will be shown to the user"
1107111077
}
1107211078
}
1107311079
}

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@
868868
'OCP\\TaskProcessing\\Exception\\PreConditionNotMetException' => $baseDir . '/lib/public/TaskProcessing/Exception/PreConditionNotMetException.php',
869869
'OCP\\TaskProcessing\\Exception\\ProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/ProcessingException.php',
870870
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => $baseDir . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
871+
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
871872
'OCP\\TaskProcessing\\Exception\\ValidationException' => $baseDir . '/lib/public/TaskProcessing/Exception/ValidationException.php',
872873
'OCP\\TaskProcessing\\IInternalTaskType' => $baseDir . '/lib/public/TaskProcessing/IInternalTaskType.php',
873874
'OCP\\TaskProcessing\\IManager' => $baseDir . '/lib/public/TaskProcessing/IManager.php',
@@ -1530,6 +1531,7 @@
15301531
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
15311532
'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
15321533
'OC\\Core\\Migrations\\Version33000Date20250819110529' => $baseDir . '/core/Migrations/Version33000Date20250819110529.php',
1534+
'OC\\Core\\Migrations\\Version33000Date20251013110519' => $baseDir . '/core/Migrations/Version33000Date20251013110519.php',
15331535
'OC\\Core\\Migrations\\Version33000Date20251023110529' => $baseDir . '/core/Migrations/Version33000Date20251023110529.php',
15341536
'OC\\Core\\Migrations\\Version33000Date20251023120529' => $baseDir . '/core/Migrations/Version33000Date20251023120529.php',
15351537
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',

lib/composer/composer/autoload_static.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
2121
array (
2222
'NCU\\' => 4,
2323
),
24+
'B' =>
25+
array (
26+
'Bamarni\\Composer\\Bin\\' => 21,
27+
),
2428
);
2529

2630
public static $prefixDirsPsr4 = array (
@@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
4044
array (
4145
0 => __DIR__ . '/../../..' . '/lib/unstable',
4246
),
47+
'Bamarni\\Composer\\Bin\\' =>
48+
array (
49+
0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
50+
),
4351
);
4452

4553
public static $fallbackDirsPsr4 = array (
@@ -909,6 +917,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
909917
'OCP\\TaskProcessing\\Exception\\PreConditionNotMetException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/PreConditionNotMetException.php',
910918
'OCP\\TaskProcessing\\Exception\\ProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ProcessingException.php',
911919
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
920+
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
912921
'OCP\\TaskProcessing\\Exception\\ValidationException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ValidationException.php',
913922
'OCP\\TaskProcessing\\IInternalTaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IInternalTaskType.php',
914923
'OCP\\TaskProcessing\\IManager' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IManager.php',
@@ -1571,6 +1580,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
15711580
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
15721581
'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
15731582
'OC\\Core\\Migrations\\Version33000Date20250819110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20250819110529.php',
1583+
'OC\\Core\\Migrations\\Version33000Date20251013110519' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251013110519.php',
15741584
'OC\\Core\\Migrations\\Version33000Date20251023110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023110529.php',
15751585
'OC\\Core\\Migrations\\Version33000Date20251023120529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023120529.php',
15761586
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',

lib/private/TaskProcessing/Db/Task.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
* @method int getEndedAt()
4848
* @method setAllowCleanup(int $allowCleanup)
4949
* @method int getAllowCleanup()
50+
* @method setUserFacingErrorMessage(null|string $message)
51+
* @method null|string getUserFacingErrorMessage()
5052
*/
5153
class Task extends Entity {
5254
protected $lastUpdated;
@@ -66,16 +68,17 @@ class Task extends Entity {
6668
protected $startedAt;
6769
protected $endedAt;
6870
protected $allowCleanup;
71+
protected $userFacingErrorMessage;
6972

7073
/**
7174
* @var string[]
7275
*/
73-
public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup'];
76+
public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message'];
7477

7578
/**
7679
* @var string[]
7780
*/
78-
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup'];
81+
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage'];
7982

8083

8184
public function __construct() {
@@ -98,6 +101,7 @@ public function __construct() {
98101
$this->addType('startedAt', 'integer');
99102
$this->addType('endedAt', 'integer');
100103
$this->addType('allowCleanup', 'integer');
104+
$this->addType('userFacingErrorMessage', 'string');
101105
}
102106

103107
public function toRow(): array {
@@ -127,6 +131,7 @@ public static function fromPublicTask(OCPTask $task): self {
127131
'startedAt' => $task->getStartedAt(),
128132
'endedAt' => $task->getEndedAt(),
129133
'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
134+
'userFacingErrorMessage' => $task->getUserFacingErrorMessage(),
130135
]);
131136
return $taskEntity;
132137
}
@@ -150,6 +155,7 @@ public function toPublicTask(): OCPTask {
150155
$task->setStartedAt($this->getStartedAt());
151156
$task->setEndedAt($this->getEndedAt());
152157
$task->setAllowCleanup($this->getAllowCleanup() !== 0);
158+
$task->setUserFacingErrorMessage($this->getUserFacingErrorMessage());
153159
return $task;
154160
}
155161
}

lib/private/TaskProcessing/Manager.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use OCP\TaskProcessing\Exception\NotFoundException;
5151
use OCP\TaskProcessing\Exception\ProcessingException;
5252
use OCP\TaskProcessing\Exception\UnauthorizedException;
53+
use OCP\TaskProcessing\Exception\UserFacingProcessingException;
5354
use OCP\TaskProcessing\Exception\ValidationException;
5455
use OCP\TaskProcessing\IInternalTaskType;
5556
use OCP\TaskProcessing\IManager;
@@ -211,7 +212,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
211212
try {
212213
return ['output' => $this->provider->process($input['input'])];
213214
} catch (\RuntimeException $e) {
214-
throw new ProcessingException($e->getMessage(), 0, $e);
215+
throw new ProcessingException($e->getMessage(), previous: $e);
215216
}
216217
}
217218

@@ -362,7 +363,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
362363
try {
363364
$this->provider->generate($input['input'], $resources);
364365
} catch (\RuntimeException $e) {
365-
throw new ProcessingException($e->getMessage(), 0, $e);
366+
throw new ProcessingException($e->getMessage(), previous: $e);
366367
}
367368
for ($i = 0; $i < $input['numberOfImages']; $i++) {
368369
if (is_resource($resources[$i])) {
@@ -480,7 +481,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
480481
try {
481482
$result = $this->provider->transcribeFile($input['input']);
482483
} catch (\RuntimeException $e) {
483-
throw new ProcessingException($e->getMessage(), 0, $e);
484+
throw new ProcessingException($e->getMessage(), previous: $e);
484485
}
485486
return ['output' => $result];
486487
}
@@ -1041,7 +1042,8 @@ public function processTask(Task $task, ISynchronousProvider $provider): bool {
10411042
$output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
10421043
} catch (ProcessingException $e) {
10431044
$this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1044-
$this->setTaskResult($task->getId(), $e->getMessage(), null);
1045+
$userFacingErrorMessage = $e instanceof UserFacingProcessingException ? $e->getUserFacingMessage() : null;
1046+
$this->setTaskResult($task->getId(), $e->getMessage(), null, userFacingError: $userFacingErrorMessage);
10451047
return false;
10461048
} catch (\Throwable $e) {
10471049
$this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
@@ -1112,7 +1114,7 @@ public function setTaskProgress(int $id, float $progress): bool {
11121114
return true;
11131115
}
11141116

1115-
public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
1117+
public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void {
11161118
// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
11171119
$task = $this->getTask($id);
11181120
if ($task->getStatus() === Task::STATUS_CANCELLED) {
@@ -1122,8 +1124,12 @@ public function setTaskResult(int $id, ?string $error, ?array $result, bool $isU
11221124
if ($error !== null) {
11231125
$task->setStatus(Task::STATUS_FAILED);
11241126
$task->setEndedAt(time());
1125-
// truncate error message to 1000 characters
1126-
$task->setErrorMessage(mb_substr($error, 0, 1000));
1127+
// truncate error message to 4000 characters
1128+
$task->setErrorMessage(substr($error, 0, 4000));
1129+
// truncate error message to 4000 characters
1130+
if ($userFacingError !== null) {
1131+
$task->setUserFacingErrorMessage(substr($userFacingError, 0, 4000));
1132+
}
11271133
$this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
11281134
} elseif ($result !== null) {
11291135
$taskTypes = $this->getAvailableTaskTypes();

lib/public/TaskProcessing/Exception/ProcessingException.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@
1010

1111
namespace OCP\TaskProcessing\Exception;
1212

13+
use OCP\AppFramework\Attribute\Consumable;
14+
1315
/**
1416
* Exception thrown during processing of a task
1517
* by a synchronous provider
18+
*
1619
* @since 30.0.0
1720
*/
21+
#[Consumable(since: '30.0.0')]
1822
class ProcessingException extends \RuntimeException {
1923
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
11+
namespace OCP\TaskProcessing\Exception;
12+
13+
use OCP\AppFramework\Attribute\Consumable;
14+
15+
/**
16+
* Exception thrown during processing of a task
17+
* by a synchronous provider with the possibility to set a user-facing
18+
* error message
19+
*
20+
* @since 33.0.0
21+
*/
22+
#[Consumable(since: '33.0.0')]
23+
class UserFacingProcessingException extends ProcessingException {
24+
25+
/**
26+
* @param string $message
27+
* @param int $code
28+
* @param \Throwable|null $previous
29+
* @param string|null $userFacingMessage
30+
* @since 33.0.0
31+
*/
32+
public function __construct(
33+
string $message = '',
34+
int $code = 0,
35+
?\Throwable $previous = null,
36+
private ?string $userFacingMessage = null,
37+
) {
38+
parent::__construct($message, $code, $previous);
39+
}
40+
41+
/**
42+
* @since 33.0.0
43+
*/
44+
public function getUserFacingMessage(): ?string {
45+
return $this->userFacingMessage;
46+
}
47+
48+
/**
49+
* @param null|string $userFacingMessage Must be already translated into the language of the user
50+
* @since 33.0.0
51+
*/
52+
public function setUserFacingMessage(?string $userFacingMessage): void {
53+
$this->userFacingMessage = $userFacingMessage;
54+
}
55+
}

0 commit comments

Comments
 (0)