Repository: wagtail/wagtail

Issue:
Document tags longer than 100 characters raise a 500 error
### Issue Summary

When you upload a document, you can add tags.
If you type a very long tag name (I have weird clients), then the tag name will never be saved (endless spinner) and the user is not informed of it. The AJAX request supposed to save the tags returns a 500 error (the exact error depends on the database backend) and the frontend does not properly handle this case.

### Steps to Reproduce

1. Go to `/admin/documents/multiple/add/` on any Wagtail project with documents enabled
2. Upload any document
3. Fill the tags field with at least one tag longer than 100 characters
4. Try to save the changes, you should see a 500 error in the Network tab of dev tools and an endless spinner on the save button

Note that if you do not fill the tags when uploading the document but go back to it later to add tags, then you will see the 500 error directly, as the tags are saved using a full document POST, not an AJAX POST.

The error depends on the database backend. With psycopg:

```python
Traceback (most recent call last):
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/urls/__init__.py", line 180, in wrapper
    return view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/auth.py", line 165, in decorated_view
    response = view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/auth.py", line 58, in wrapped_view_func
    return view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/documents/views/documents.py", line 186, in edit
    doc = form.save()
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/documents/forms.py", line 53, in save
    super().save(commit=commit)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/forms/collections.py", line 163, in save
    return super().save(commit=commit)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/forms/models.py", line 543, in save
    self._save_m2m()
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/forms/models.py", line 524, in _save_m2m
    f.save_form_data(self.instance, cleaned_data[f.name])
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 556, in save_form_data
    getattr(instance, self.name).set(value)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/utils.py", line 124, in inner
    return func(self, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 282, in set
    objs = self._to_tag_model_instances(tags, tag_kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 248, in _to_tag_model_instances
    tag, create = manager.get_or_create(**lookup, defaults={"name": new_tag})
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 923, in get_or_create
    return self.create(**params), True
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 658, in create
    obj.save(force_insert=True, using=self.db)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/models.py", line 55, in save
    res = super().save(*args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 814, in save
    self.save_base(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 877, in save_base
    updated = self._save_table(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 1020, in _save_table
    results = self._do_insert(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 1061, in _do_insert
    return manager._insert(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 1805, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1822, in execute_sql
    cursor.execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 102, in execute
    return super().execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/psycopg/cursor.py", line 737, in execute
    raise ex.with_traceback(None)
django.db.utils.DataError: value too long for type character varying(100)
```

### Technical details

-   Python version: Run 3.11.5
-   Django version: 4.2.5
-   Wagtail version: 5.1.1

The easiest way to fix this issue would be to add a proper backend validation of the field. Both the full document POST and the AJAX POST handle well validation errors, so that would be enough. As a bonus, it could be great to make the input contain a `maxlength="100"` to avoid the frustration of having to rewrite the tag **after** filling it.

Document tags longer than 100 characters raise a 500 error
### Issue Summary

When you upload a document, you can add tags.
If you type a very long tag name (I have weird clients), then the tag name will never be saved (endless spinner) and the user is not informed of it. The AJAX request supposed to save the tags returns a 500 error (the exact error depends on the database backend) and the frontend does not properly handle this case.

### Steps to Reproduce

1. Go to `/admin/documents/multiple/add/` on any Wagtail project with documents enabled
2. Upload any document
3. Fill the tags field with at least one tag longer than 100 characters
4. Try to save the changes, you should see a 500 error in the Network tab of dev tools and an endless spinner on the save button

Note that if you do not fill the tags when uploading the document but go back to it later to add tags, then you will see the 500 error directly, as the tags are saved using a full document POST, not an AJAX POST.

The error depends on the database backend. With psycopg:

```python
Traceback (most recent call last):
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/urls/__init__.py", line 180, in wrapper
    return view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/auth.py", line 165, in decorated_view
    response = view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/auth.py", line 58, in wrapped_view_func
    return view_func(request, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/documents/views/documents.py", line 186, in edit
    doc = form.save()
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/documents/forms.py", line 53, in save
    super().save(commit=commit)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/wagtail/admin/forms/collections.py", line 163, in save
    return super().save(commit=commit)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/forms/models.py", line 543, in save
    self._save_m2m()
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/forms/models.py", line 524, in _save_m2m
    f.save_form_data(self.instance, cleaned_data[f.name])
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 556, in save_form_data
    getattr(instance, self.name).set(value)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/utils.py", line 124, in inner
    return func(self, *args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 282, in set
    objs = self._to_tag_model_instances(tags, tag_kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/managers.py", line 248, in _to_tag_model_instances
    tag, create = manager.get_or_create(**lookup, defaults={"name": new_tag})
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 923, in get_or_create
    return self.create(**params), True
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 658, in create
    obj.save(force_insert=True, using=self.db)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/taggit/models.py", line 55, in save
    res = super().save(*args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 814, in save
    self.save_base(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 877, in save_base
    updated = self._save_table(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 1020, in _save_table
    results = self._do_insert(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/base.py", line 1061, in _do_insert
    return manager._insert(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/query.py", line 1805, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1822, in execute_sql
    cursor.execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 102, in execute
    return super().execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
  File "/home/bertrand/.virtualenvs/flaubert/lib/python3.10/site-packages/psycopg/cursor.py", line 737, in execute
    raise ex.with_traceback(None)
django.db.utils.DataError: value too long for type character varying(100)
```

### Technical details

-   Python version: Run 3.11.5
-   Django version: 4.2.5
-   Wagtail version: 5.1.1

The easiest way to fix this issue would be to add a proper backend validation of the field. Both the full document POST and the AJAX POST handle well validation errors, so that would be enough. As a bonus, it could be great to make the input contain a `maxlength="100"` to avoid the frustration of having to rewrite the tag **after** filling it.



Discussion:
Hi @BertrandBordage I would need more information:
The error you stated above, are you getting it off a development server( I mean served using your localhost)?

If yes, then I have tried reproducing the error. I uploaded a document attaching a tag of 215 characters long and it uploaded alright. It is using a SQLlite backend.

Perhaps, you  need to provide more information to help in reproducing the error. Thanks

![msedge_N1SASONNef](https://github.com/wagtail/wagtail/assets/87539866/3b0e8287-232d-4f9f-9349-bdc7fd1aaa87)


@Temidayo32 thanks for the reply! It was on production, but on the dev server you should see the error as well but... Not on SQLite. SQLite does not enforce database types nor constraints, even though it accepts the definition of types for compatibility with SQL.

Yes, you can actually store unlimited strings in a VARCHAR(10) with SQLite 🙃 
You can even store text in integer columns etc.

This error occurs "only" on real SQL databases like PostgreSQL, MySQL, Oracle etc.
(oops, closed by misclick)
@BertrandBordage Oh I see. Thanks for the response.
I just confirmed the error. 

![msedge_0ukC8scJkd](https://github.com/wagtail/wagtail/assets/87539866/69ddab4d-b2e1-4eea-a5af-2460099a385f)

@BertrandBordage perhaps, you might want to take a look at this quick fix in the meantime 
https://github.com/jazzband/django-taggit/issues/872
@Temidayo32 Oh, nice! Yes, maybe I should have created the issue in django-taggit instead 🤔 At least this Wagtail issue will allow us to track when a release to Wagtail could be considered.

The quickfix should work but… It’s a bit ugly to set up. We have to use monkey patching for that, something like (untested):
```python
from functools import wraps

class FixedTagField(TagField):
    def clean(self, value):
        value = super().clean(value)
        try:
            tags = parse_tags(value)
        except ValueError:
            raise forms.ValidationError(
                _("Please provide a comma-separated list of tags.")
            )
        max_length = TagBase._meta.get_field("name").max_length
        if any(len(tag) > max_length for tag in tags):
            raise forms.ValidationError(
                _("Tags cannot be longer than %s characters long.") % max_length,
            )
        return tags

TagField.clean = wraps(TagField.clean)(FixedTagField.clean)
```

But actually, the tagging field and widget that Wagtail uses is very custom, so some of the fix must be on the Wagtail part.
The fix for the length of the frontend input especially has to be on the Wagtail side. I don’t know much about this `w-tag` widget that we are using, but to me we should add an option here: https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/admin/widgets/tags.py#L52-L58
Something like `"tagMaxLength": Tag._meta.get_field("name").max_length,` and make the JS side use it.

Strangely, we already have our custom `clean` method that’s supposed to prevent this issue on the backend side, but it doesn’t seem to work:
https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/admin/forms/tags.py#L39-L53

I guess it’s because the `Document` forms are focused on using `AdminTagWidget` instead of using Wagtail’s `TagField` (which would also remove the need to specify `AdminTagWidget`)?
https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/documents/forms.py#L62

In any case, this issue is likely not limited to the `Document` model and Wagtail should not have the backend fix for the `max_length` validation, only the frontend fix.
@BertrandBordage Hmmm.. this is interesting. So you think the code fix for the tag length that is already in Wagtail is unnecessary, and only a frontend validation should be done? And that the backend fix, if any, should be done in Django-taggit instead. Is that what you are saying?

@Temidayo32 Exactly!
@BertrandBordage Can you raise an issue instead for a frontend validation?

As regards the backend fix with Django-taggit, this is the issue tracker for it: https://github.com/jazzband/django-taggit/issues/872. We can get that fixed over there.

But another option is if we can use the `TagField` instead for the Document model to fix the backend problem here in Wagtail
@BertrandBordage I would be linking a pull request for this issue soon! It incorporates your suggestions for frontend validation as well as a server side validation. 
Hi @BertrandBordage I would need more information:
The error you stated above, are you getting it off a development server( I mean served using your localhost)?

If yes, then I have tried reproducing the error. I uploaded a document attaching a tag of 215 characters long and it uploaded alright. It is using a SQLlite backend.

Perhaps, you  need to provide more information to help in reproducing the error. Thanks

![msedge_N1SASONNef](https://github.com/wagtail/wagtail/assets/87539866/3b0e8287-232d-4f9f-9349-bdc7fd1aaa87)


@Temidayo32 thanks for the reply! It was on production, but on the dev server you should see the error as well but... Not on SQLite. SQLite does not enforce database types nor constraints, even though it accepts the definition of types for compatibility with SQL.

Yes, you can actually store unlimited strings in a VARCHAR(10) with SQLite 🙃 
You can even store text in integer columns etc.

This error occurs "only" on real SQL databases like PostgreSQL, MySQL, Oracle etc.
(oops, closed by misclick)
@BertrandBordage Oh I see. Thanks for the response.
I just confirmed the error. 

![msedge_0ukC8scJkd](https://github.com/wagtail/wagtail/assets/87539866/69ddab4d-b2e1-4eea-a5af-2460099a385f)

@BertrandBordage perhaps, you might want to take a look at this quick fix in the meantime 
https://github.com/jazzband/django-taggit/issues/872
@Temidayo32 Oh, nice! Yes, maybe I should have created the issue in django-taggit instead 🤔 At least this Wagtail issue will allow us to track when a release to Wagtail could be considered.

The quickfix should work but… It’s a bit ugly to set up. We have to use monkey patching for that, something like (untested):
```python
from functools import wraps

class FixedTagField(TagField):
    def clean(self, value):
        value = super().clean(value)
        try:
            tags = parse_tags(value)
        except ValueError:
            raise forms.ValidationError(
                _("Please provide a comma-separated list of tags.")
            )
        max_length = TagBase._meta.get_field("name").max_length
        if any(len(tag) > max_length for tag in tags):
            raise forms.ValidationError(
                _("Tags cannot be longer than %s characters long.") % max_length,
            )
        return tags

TagField.clean = wraps(TagField.clean)(FixedTagField.clean)
```

But actually, the tagging field and widget that Wagtail uses is very custom, so some of the fix must be on the Wagtail part.
The fix for the length of the frontend input especially has to be on the Wagtail side. I don’t know much about this `w-tag` widget that we are using, but to me we should add an option here: https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/admin/widgets/tags.py#L52-L58
Something like `"tagMaxLength": Tag._meta.get_field("name").max_length,` and make the JS side use it.

Strangely, we already have our custom `clean` method that’s supposed to prevent this issue on the backend side, but it doesn’t seem to work:
https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/admin/forms/tags.py#L39-L53

I guess it’s because the `Document` forms are focused on using `AdminTagWidget` instead of using Wagtail’s `TagField` (which would also remove the need to specify `AdminTagWidget`)?
https://github.com/wagtail/wagtail/blob/a2b0d04563ce9f4f5c1ca1916a64c724aab0adb4/wagtail/documents/forms.py#L62

In any case, this issue is likely not limited to the `Document` model and Wagtail should not have the backend fix for the `max_length` validation, only the frontend fix.
@BertrandBordage Hmmm.. this is interesting. So you think the code fix for the tag length that is already in Wagtail is unnecessary, and only a frontend validation should be done? And that the backend fix, if any, should be done in Django-taggit instead. Is that what you are saying?

@Temidayo32 Exactly!
@BertrandBordage Can you raise an issue instead for a frontend validation?

As regards the backend fix with Django-taggit, this is the issue tracker for it: https://github.com/jazzband/django-taggit/issues/872. We can get that fixed over there.

But another option is if we can use the `TagField` instead for the Document model to fix the backend problem here in Wagtail
@BertrandBordage I would be linking a pull request for this issue soon! It incorporates your suggestions for frontend validation as well as a server side validation. 

Resolving patch:
diff --git a/wagtail/admin/forms/tags.py b/wagtail/admin/forms/tags.py
--- a/wagtail/admin/forms/tags.py
+++ b/wagtail/admin/forms/tags.py
@@ -1,11 +1,32 @@
 from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
 from taggit.forms import TagField as TaggitTagField
-from taggit.models import Tag
+from taggit.models import Tag, TagBase
 
 from wagtail.admin.widgets import AdminTagWidget
 
 
+def validate_tag_length(
+    value, max_tag_length=TagBase._meta.get_field("name").max_length
+):
+    if not value:
+        return
+    value_too_long = ""
+    for val in value:
+        if len(val) > max_tag_length:
+            if value_too_long:
+                value_too_long += ", "
+            value_too_long += val
+    if value_too_long:
+        raise ValidationError(
+            _("Tag(s) %(value_too_long)s are over %(max_tag_length)d characters")
+            % {
+                "value_too_long": value_too_long,
+                "max_tag_length": max_tag_length,
+            }
+        )
+
+
 class TagField(TaggitTagField):
     """
     Extends taggit's TagField with the option to prevent creating tags that do not already exist
@@ -34,22 +55,7 @@ def __init__(self, *args, **kwargs):
 
     def clean(self, value):
         value = super().clean(value)
-
-        max_tag_length = self.tag_model.name.field.max_length
-        value_too_long = ""
-        for val in value:
-            if len(val) > max_tag_length:
-                if value_too_long:
-                    value_too_long += ", "
-                value_too_long += val
-        if value_too_long:
-            raise ValidationError(
-                _("Tag(s) %(value_too_long)s are over %(max_tag_length)d characters")
-                % {
-                    "value_too_long": value_too_long,
-                    "max_tag_length": max_tag_length,
-                }
-            )
+        validate_tag_length(value, self.tag_model.name.field.max_length)
 
         if not self.free_tagging:
             # filter value to just the tags that already exist in tag_model
diff --git a/wagtail/documents/forms.py b/wagtail/documents/forms.py
--- a/wagtail/documents/forms.py
+++ b/wagtail/documents/forms.py
@@ -8,6 +8,7 @@
     CollectionChoiceField,
     collection_member_permission_formset_factory,
 )
+from wagtail.admin.forms.tags import validate_tag_length
 from wagtail.admin.widgets import AdminTagWidget
 from wagtail.documents.models import Document
 from wagtail.documents.permissions import (
@@ -61,6 +62,11 @@ def save(self, commit=True):
     class Meta:
         widgets = {"tags": AdminTagWidget, "file": forms.FileInput()}
 
+    def clean_tags(self):
+        tags = self.cleaned_data["tags"]
+        validate_tag_length(tags)
+        return tags
+
 
 def get_document_base_form():
     base_form_override = getattr(settings, "WAGTAILDOCS_DOCUMENT_FORM_BASE", "")
diff --git a/wagtail/images/forms.py b/wagtail/images/forms.py
--- a/wagtail/images/forms.py
+++ b/wagtail/images/forms.py
@@ -9,6 +9,7 @@
     CollectionChoiceField,
     collection_member_permission_formset_factory,
 )
+from wagtail.admin.forms.tags import validate_tag_length
 from wagtail.admin.widgets import AdminTagWidget
 from wagtail.images.fields import WagtailImageField
 from wagtail.images.formats import get_image_formats
@@ -78,6 +79,11 @@ class Meta:
             ),
         }
 
+    def clean_tags(self):
+        tags = self.cleaned_data["tags"]
+        validate_tag_length(tags)
+        return tags
+
 
 def get_image_base_form():
     base_form_override = getattr(settings, "WAGTAILIMAGES_IMAGE_FORM_BASE", "")


Test patch:
diff --git a/wagtail/documents/tests/test_form_overrides.py b/wagtail/documents/tests/test_form_overrides.py
--- a/wagtail/documents/tests/test_form_overrides.py
+++ b/wagtail/documents/tests/test_form_overrides.py
@@ -37,6 +37,28 @@ def test_tags_widget_with_custom_tag_model(self):
         self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
         self.assertEqual(form.fields["tags"].widget.tag_model, RestaurantTag)
 
+    def test_tags_longer_than_max_characters(self):
+        long_value = "longtag" * 20
+
+        form_data = {
+            "title": "Test Document",
+            "file": OverriddenWidget,
+            "tags": [long_value],
+        }
+
+        form_cls = get_document_form(models.Document)
+        form = form_cls(form_data)
+
+        self.assertFalse(form.is_valid())
+        self.assertIn("tags", form.errors)
+        self.assertEqual(
+            form.errors["tags"][0],
+            "Tag(s) ['{val}'] are over {max_tag_length} characters".format(
+                val=long_value,
+                max_tag_length=taggit_models.TagBase._meta.get_field("name").max_length,
+            ),
+        )
+
     @override_settings(
         WAGTAILDOCS_DOCUMENT_FORM_BASE="wagtail.test.testapp.media_forms.AlternateDocumentForm"
     )
diff --git a/wagtail/images/tests/test_form_overrides.py b/wagtail/images/tests/test_form_overrides.py
--- a/wagtail/images/tests/test_form_overrides.py
+++ b/wagtail/images/tests/test_form_overrides.py
@@ -33,6 +33,28 @@ def test_tags_widget_with_custom_tag_model(self):
         self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
         self.assertEqual(form.fields["tags"].widget.tag_model, RestaurantTag)
 
+    def test_tags_longer_than_max_characters(self):
+        long_value = "longtag" * 20
+
+        form_data = {
+            "title": "Image",
+            "file": OverriddenWidget,
+            "tags": [long_value],
+        }
+
+        form_cls = get_image_form(models.Image)
+        form = form_cls(form_data)
+
+        self.assertFalse(form.is_valid())
+        self.assertIn("tags", form.errors)
+        self.assertEqual(
+            form.errors["tags"][0],
+            "Tag(s) ['{val}'] are over {max_tag_length} characters".format(
+                val=long_value,
+                max_tag_length=taggit_models.TagBase._meta.get_field("name").max_length,
+            ),
+        )
+
     @override_settings(
         WAGTAILIMAGES_IMAGE_FORM_BASE="wagtail.test.testapp.media_forms.AlternateImageForm"
     )