Repository: sissbruecker/linkding

Issue:
Malformed href in Netscape HTML file results in import error
I was importing a very large export of HTML bookmarks, and it was failing with the vague UI error "An error occurred during bookmark import."

<details>
<summary>The logs contained this:</summary>

```
Jul 29 13:29:07 2025-07-29 17:29:07,941 ERROR Unexpected error during bookmark import
Jul 29 13:29:07 Traceback (most recent call last):
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 105, in _execute
Jul 29 13:29:07 return self.cursor.execute(sql, params)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 psycopg2.errors.StringDataRightTruncation: value too long for type character varying(64)
Jul 29 13:29:07 2025-07-29T17:29:07Z
Jul 29 13:29:07 2025-07-29T17:29:07Z
Jul 29 13:29:07 The above exception was the direct cause of the following exception:
Jul 29 13:29:07 2025-07-29T17:29:07Z
Jul 29 13:29:07 Traceback (most recent call last):
Jul 29 13:29:07 File "/app/code/bookmarks/views/settings.py", line 213, in bookmark_import
Jul 29 13:29:07 result = importer.import_netscape_html(content, request.user, import_options)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/bookmarks/services/importer.py", line 74, in import_netscape_html
Jul 29 13:29:07 _create_missing_tags(netscape_bookmarks, user)
Jul 29 13:29:07 File "/app/code/bookmarks/services/importer.py", line 106, in _create_missing_tags
Jul 29 13:29:07 Tag.objects.bulk_create(tags_to_create)
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/models/manager.py", line 87, in manager_method
Jul 29 13:29:07 return getattr(self.get_queryset(), name)(*args, **kwargs)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/models/query.py", line 835, in bulk_create
Jul 29 13:29:07 returned_columns = self._batched_insert(
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/models/query.py", line 1875, in _batched_insert
Jul 29 13:29:07 self._insert(
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/models/query.py", line 1847, in _insert
Jul 29 13:29:07 return query.get_compiler(using=using).execute_sql(returning_fields)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1836, in execute_sql
Jul 29 13:29:07 cursor.execute(sql, params)
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 79, in execute
Jul 29 13:29:07 return self._execute_with_wrappers(
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 92, in _execute_with_wrappers
Jul 29 13:29:07 return executor(sql, params, many, context)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 100, in _execute
Jul 29 13:29:07 with self.db.wrap_database_errors:
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/utils.py", line 91, in __exit__
Jul 29 13:29:07 raise dj_exc_value.with_traceback(traceback) from exc_value
Jul 29 13:29:07 File "/app/code/venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 105, in _execute
Jul 29 13:29:07 return self.cursor.execute(sql, params)
Jul 29 13:29:07 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Jul 29 13:29:07 django.db.utils.DataError: value too long for type character varying(64)
Jul 29 13:29:07 2025-07-29T17:29:07Z
Jul 29 13:29:07 [pid: 18|app: 0|req: 294/570] 2601:189:8101:29b0:893e:c101:1365:8d09 () {70 vars in 1417 bytes} [Tue Jul 29 17:29:07 2025] POST /settings/import => generated 0 bytes in 60 msecs (HTTP/1.1 302) 10 headers in 516 bytes (1 switches on core 0)
```
</details>

The culprit was this malformed URL in an href attribute:

```html
<DT><A HREF="http://:C" ADD_DATE="1394214280" PRIVATE="1" TOREAD="" TAGS="http://www.npr.org/blogs/health/2014/03/06/286786987/for-some-people-music-truly-doesnt-make-them-happy">[3 min read] Some People Don't Feel Anything (When They Listen To Music)</A>
```

Not sure how that got into my bookmarks, but it would be nice if Linkding handled this failure better.


Discussion:
If I'm reading the logs correctly the issue is actually the the tag name that is too long for the database column.
Oh thanks! Sorry for the erroneous report.



Patch:
diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py
index 41fda084..50acaebb 100644
--- a/bookmarks/services/importer.py
+++ b/bookmarks/services/importer.py
@@ -96,6 +96,13 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User)
 
     for netscape_bookmark in netscape_bookmarks:
         for tag_name in netscape_bookmark.tag_names:
+            # Skip tag names that exceed the maximum allowed length
+            if len(tag_name) > 64:
+                logger.warning(
+                    f"Ignoring tag '{tag_name}' (length {len(tag_name)}) as it exceeds maximum length of 64 characters"
+                )
+                continue
+
             tag = tag_cache.get(tag_name)
             if not tag:
                 tag = Tag(name=tag_name, owner=user)


Test patch:
diff --git a/bookmarks/tests/test_importer.py b/bookmarks/tests/test_importer.py
index 996e9b9c..49949b33 100644
--- a/bookmarks/tests/test_importer.py
+++ b/bookmarks/tests/test_importer.py
@@ -366,6 +366,32 @@ def test_replace_whitespace_in_tag_names(self):
 
         self.assertListEqual(tag_names, ["tag-1", "tag-2", "tag-3"])
 
+    def test_ignore_long_tag_names(self):
+        long_tag = "a" * 65
+        valid_tag = "valid-tag"
+
+        test_html = self.render_html(
+            tags_html=f"""
+            <DT><A HREF="https://example.com" TAGS="{long_tag}, {valid_tag}">Example.com</A>
+            <DD>Example.com
+        """
+        )
+        result = import_netscape_html(test_html, self.get_or_create_test_user())
+
+        # Import should succeed
+        self.assertEqual(result.success, 1)
+        self.assertEqual(result.failed, 0)
+
+        # Only the valid tag should be created
+        tags = Tag.objects.all()
+        self.assertEqual(len(tags), 1)
+        self.assertEqual(tags[0].name, valid_tag)
+
+        # Bookmark should only have the valid tag assigned
+        bookmark = Bookmark.objects.get(url="https://example.com")
+        bookmark_tag_names = [tag.name for tag in bookmark.tags.all()]
+        self.assertEqual(bookmark_tag_names, [valid_tag])
+
     @disable_logging
     def test_validate_empty_or_missing_bookmark_url(self):
         test_html = self.render_html(