Repository: mealie-recipes/mealie (python)

Issue:
[BUG] - 500 Internal Server Error when Patching Recipe Tags

### First Check

- [x] This is not a feature request.
- [x] I added a very descriptive title to this issue (title field is above this).
- [x] I used the GitHub search to find a similar issue and didn't find it.
- [x] I searched the Mealie documentation, with the integrated search.
- [x] I already read the docs and didn't find an answer.
- [x] This issue can be replicated on the demo site (https://demo.mealie.io/).

### What is the issue you are experiencing?

A 500 Internal Server Error is returned by the Mealie API when attempting to PATCH a recipe's tags using the /api/recipes/{slug} endpoint. This occurs when the payload includes new tags that do not yet exist in the database.

It appears related to the RecipeTag schema validation missing the group_id field during the update process, causing a TypeError in the backend when it attempts to initialize the tag object.

### Steps to Reproduce

1. Have a running instance of Mealie.
2. Identify an existing recipe slug.
3. Send a PATCH request to /api/recipes/{slug} with a payload that includes a new tag name that does not exist in the database.

```

{
  "tags": [
    {
      "name": "New Tag",
      "slug": "new-tag"
    }
  ]
}
```

4. Observe the 500 Internal Server Error response.

### Please provide relevant logs

`TypeError: init() missing 1 required positional argument: 'group_id'
`
(Note: This is the expected traceback based on similar reports; please confirm with your actual server logs if available).

### Mealie Version

Build ID: 4fbace36-29e6-4cd6-94f8-f7445fd74855

### Deployment

Other (please specify below)

### Additional Deployment Details

Proxmox LXC Container

Interacting with Mealie via Python script using urllib. The issue is reproducible consistently via API.


Patch:
diff --git a/mealie/db/models/recipe/api_extras.py b/mealie/db/models/recipe/api_extras.py
index 659ea062e7a..eed2c28055f 100644
--- a/mealie/db/models/recipe/api_extras.py
+++ b/mealie/db/models/recipe/api_extras.py
@@ -9,7 +9,7 @@ def api_extras(func):
     """Decorator function to unpack the extras into a dict; requires an "extras" column"""
 
     def wrapper(*args, **kwargs):
-        extras = kwargs.pop("extras")
+        extras = kwargs.pop("extras", None)
 
         if extras is None:
             extras = []
diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py
index 23972d0246e..aa7512fd4d0 100644
--- a/mealie/repos/repository_recipes.py
+++ b/mealie/repos/repository_recipes.py
@@ -201,6 +201,16 @@ def _uuids_for_items(self, items: list[UUID | str] | None, model: type[SqlAlchem
         additional_ids = self.session.execute(sa.select(model.id).filter(model.slug.in_(slugs))).scalars().all()
         return ids + additional_ids
 
+    def update(self, match_value: str | int | UUID4, new_data: dict | Recipe) -> Recipe:
+        new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
+
+        # Handle explicit group_id injection for related items that require it
+        for organizer_field in ["tags", "recipe_category", "tools"]:
+            for organizer in new_data.get(organizer_field, []):
+                organizer["group_id"] = self.group_id
+
+        return super().update(match_value, new_data)
+
     def page_all(  # type: ignore
         self,
         pagination: PaginationQuery,


Test patch:
diff --git a/tests/unit_tests/repository_tests/test_recipe_repository.py b/tests/unit_tests/repository_tests/test_recipe_repository.py
index a7cc2891053..5502b05f707 100644
--- a/tests/unit_tests/repository_tests/test_recipe_repository.py
+++ b/tests/unit_tests/repository_tests/test_recipe_repository.py
@@ -1,5 +1,5 @@
 from datetime import UTC, datetime, timedelta
-from uuid import UUID
+from uuid import UUID, uuid4
 
 import pytest
 from sqlalchemy.orm import Session
@@ -810,3 +810,48 @@ def test_order_by_rating(user_tuple: tuple[TestUser, TestUser]):
     assert data[0].slug == recipe_2.slug  # global rating == 2.5 (avg of 4 and 1)
     assert data[1].slug == recipe_3.slug  # global rating == 3
     assert data[2].slug == recipe_1.slug  # global rating == 4.25 (avg of 5 and 3.5)
+
+
+@pytest.mark.parametrize("route", ["patch", "update"])
+def test_recipe_inject_organizer_group_id(unique_user: TestUser, route: str):
+    # Regression test for #6802 - Ensure explicit group_id injection works for new organizers
+    database = unique_user.repos
+    recipe = database.recipes.create(
+        Recipe(
+            user_id=unique_user.user_id,
+            group_id=unique_user.group_id,
+            name=random_string(),
+        )
+    )
+
+    new_tag_name = random_string()
+    new_cat_name = random_string()
+    new_tool_name = random_string()
+
+    patch_data = Recipe(
+        name=recipe.name,
+        tags=[new_tag_name],
+        recipe_category=[new_cat_name],
+        tools=[{"id": uuid4(), "name": new_tool_name, "slug": new_tool_name}],
+    )
+
+    # This should not raise IntegrityError or TypeError
+    update_func = database.recipes.patch if route == "patch" else database.recipes.update
+    if route == "patch":
+        updated_recipe = update_func(recipe.slug, patch_data.model_dump(exclude_unset=True))
+    elif route == "update":
+        updated_recipe = update_func(recipe.slug, patch_data.model_dump(exclude_unset=True))
+
+    assert updated_recipe
+
+    assert updated_recipe.tags
+    assert len(updated_recipe.tags) == 1
+    assert updated_recipe.tags[0].name == new_tag_name
+
+    assert updated_recipe.recipe_category
+    assert len(updated_recipe.recipe_category) == 1
+    assert updated_recipe.recipe_category[0].name == new_cat_name
+
+    assert updated_recipe.tools
+    assert len(updated_recipe.tools) == 1
+    assert updated_recipe.tools[0].name == new_tool_name