diff --git a/docs/usage.md b/docs/usage.md index 504aae9ef..ab71f16a1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -419,6 +419,7 @@ The following workflow action types are available: - The URL to send the request to - The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below. +- Encoding for the request body, either JSON or form data - The request headers as key-value pairs #### Workflow placeholders diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html index 5184dcd10..add7878f4 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -338,7 +338,10 @@
- +
+ + +
@if (formGroup.get('webhook').value['use_params']) { } @else { diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index 3abdee09a..0908b69c0 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -471,6 +471,7 @@ export class WorkflowEditDialogComponent id: new FormControl(action.webhook?.id), url: new FormControl(action.webhook?.url), use_params: new FormControl(action.webhook?.use_params), + as_json: new FormControl(action.webhook?.as_json), params: new FormControl(action.webhook?.params), body: new FormControl(action.webhook?.body), headers: new FormControl(action.webhook?.headers), @@ -588,6 +589,7 @@ export class WorkflowEditDialogComponent id: null, url: null, use_params: true, + as_json: false, params: null, body: null, headers: null, diff --git a/src-ui/src/app/data/workflow-action.ts b/src-ui/src/app/data/workflow-action.ts index b802d47b4..0d8316ecb 100644 --- a/src-ui/src/app/data/workflow-action.ts +++ b/src-ui/src/app/data/workflow-action.ts @@ -22,6 +22,8 @@ export interface WorkflowActionWebhook extends ObjectWithId { use_params?: boolean + as_json?: boolean + params?: object body?: string diff --git a/src/documents/migrations/1061_workflowactionwebhook_as_json.py b/src/documents/migrations/1061_workflowactionwebhook_as_json.py new file mode 100644 index 000000000..f1945cfc1 --- /dev/null +++ b/src/documents/migrations/1061_workflowactionwebhook_as_json.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-01-18 19:35 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1060_alter_customfieldinstance_value_select"), + ] + + operations = [ + migrations.AddField( + model_name="workflowactionwebhook", + name="as_json", + field=models.BooleanField(default=False, verbose_name="send as JSON"), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 88265a7da..79856b837 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1209,6 +1209,11 @@ class WorkflowActionWebhook(models.Model): verbose_name=_("use parameters"), ) + as_json = models.BooleanField( + default=False, + verbose_name=_("send as JSON"), + ) + params = models.JSONField( _("webhook parameters"), null=True, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e051e00d6..eb1eba8f1 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1876,6 +1876,7 @@ class WorkflowActionWebhookSerializer(serializers.ModelSerializer): "id", "url", "use_params", + "as_json", "params", "body", "headers", diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 1d21b962b..e60585a37 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -573,14 +573,29 @@ def run_workflows_updated(sender, document: Document, logging_group=None, **kwar max_retries=3, throws=(httpx.HTTPError,), ) -def send_webhook(url, data, headers, files): +def send_webhook( + url: str, + data: str | dict, + headers: dict, + files: dict, + *, + as_json: bool = False, +): try: - httpx.post( - url, - data=data, - files=files, - headers=headers, - ).raise_for_status() + if as_json: + httpx.post( + url, + json=data, + files=files, + headers=headers, + ).raise_for_status() + else: + httpx.post( + url, + data=data, + files=files, + headers=headers, + ).raise_for_status() logger.info( f"Webhook sent to {url}", ) @@ -1092,6 +1107,7 @@ def run_workflows( data=data, headers=headers, files=files, + as_json=action.webhook.as_json, ) logger.debug( f"Webhook to {action.webhook.url} queued", diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 9f976bbfe..cb5a132af 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -11,6 +11,7 @@ from guardian.shortcuts import assign_perm from guardian.shortcuts import get_groups_with_perms from guardian.shortcuts import get_users_with_perms from httpx import HTTPStatusError +from pytest_httpx import HTTPXMock from rest_framework.test import APITestCase from documents.signals.handlers import run_workflows @@ -2407,6 +2408,7 @@ class TestWorkflows( data=f"Test message: http://localhost:8000/documents/{doc.id}/", headers={}, files=None, + as_json=False, ) @override_settings( @@ -2468,6 +2470,7 @@ class TestWorkflows( data=f"Test message: http://localhost:8000/documents/{doc.id}/", headers={}, files={"file": ("simple.pdf", mock.ANY, "application/pdf")}, + as_json=False, ) @override_settings( @@ -2669,3 +2672,43 @@ class TestWorkflows( ) mock_post.assert_called_once() + + +class TestWebhookSend: + def test_send_webhook_data_or_json( + self, + httpx_mock: HTTPXMock, + ): + """ + GIVEN: + - Nothing + WHEN: + - send_webhook is called with data or dict + THEN: + - data is sent as form-encoded and json, respectively + """ + httpx_mock.add_response( + content=b"ok", + ) + + send_webhook( + url="http://paperless-ngx.com", + data="Test message", + headers={}, + files=None, + as_json=False, + ) + assert httpx_mock.get_request().headers.get("Content-Type") is None + httpx_mock.reset() + + httpx_mock.add_response( + json={"status": "ok"}, + ) + send_webhook( + url="http://paperless-ngx.com", + data={"message": "Test message"}, + headers={}, + files=None, + as_json=True, + ) + assert httpx_mock.get_request().headers["Content-Type"] == "application/json"