mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 15:48:03 +00:00
fix: Handle unknown item types gracefully in DeserializeBaseItem
When querying items with recursive=true, items with types from removed plugins would cause a 500 error. Now these items are skipped with a warning log instead of throwing an exception. Fixes #15945
This commit is contained in:
@@ -277,7 +277,7 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||||
|
|
||||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
result.StartIndex = filter.StartIndex ?? 0;
|
result.StartIndex = filter.StartIndex ?? 0;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||||
|
|
||||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -341,7 +341,7 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
mainquery = ApplyNavigations(mainquery, filter);
|
mainquery = ApplyNavigations(mainquery, filter);
|
||||||
|
|
||||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1159,7 +1159,7 @@ public sealed class BaseItemRepository
|
|||||||
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
||||||
if (_serverConfigurationManager?.Configuration is null)
|
if (_serverConfigurationManager?.Configuration is null)
|
||||||
@@ -1182,11 +1182,19 @@ public sealed class BaseItemRepository
|
|||||||
/// <param name="logger">Logger.</param>
|
/// <param name="logger">Logger.</param>
|
||||||
/// <param name="appHost">The application server Host.</param>
|
/// <param name="appHost">The application server Host.</param>
|
||||||
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
||||||
/// <returns>A mapped BaseItem.</returns>
|
/// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
|
||||||
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
|
public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||||
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
|
||||||
{
|
{
|
||||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
var type = GetType(baseItemEntity.Type);
|
||||||
|
if (type is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.",
|
||||||
|
baseItemEntity.Id,
|
||||||
|
baseItemEntity.Type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
BaseItemDto? dto = null;
|
BaseItemDto? dto = null;
|
||||||
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
||||||
{
|
{
|
||||||
@@ -1353,10 +1361,9 @@ public sealed class BaseItemRepository
|
|||||||
.. resultQuery
|
.. resultQuery
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(e => e is not null)
|
.Where(e => e is not null)
|
||||||
.Select(e =>
|
.Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
|
||||||
{
|
.Where(e => e.Item is not null)
|
||||||
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
.Select(e => (e.Item!, e.itemCount))
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1367,10 +1374,9 @@ public sealed class BaseItemRepository
|
|||||||
.. query
|
.. query
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(e => e is not null)
|
.Where(e => e is not null)
|
||||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
.Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)null))
|
||||||
{
|
.Where(e => e.Item is not null)
|
||||||
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
|
.Select(e => (e.Item!, e.ItemCounts))
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2671,6 +2677,6 @@ public sealed class BaseItemRepository
|
|||||||
.Where(e => artistNames.Contains(e.Name))
|
.Where(e => artistNames.Contains(e.Name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
|
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1247,8 +1247,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
|||||||
}
|
}
|
||||||
|
|
||||||
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
|
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
|
||||||
|
if (baseItem is not null)
|
||||||
|
{
|
||||||
var dataKeys = baseItem.GetUserDataKeys();
|
var dataKeys = baseItem.GetUserDataKeys();
|
||||||
userDataKeys.AddRange(dataKeys);
|
userDataKeys.AddRange(dataKeys);
|
||||||
|
}
|
||||||
|
|
||||||
return (entity, userDataKeys.ToArray());
|
return (entity, userDataKeys.ToArray());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Server.Implementations.Item;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Tests.Item;
|
||||||
|
|
||||||
|
public class BaseItemRepositoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeBaseItem_WithUnknownType_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entity = new BaseItemEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = "NonExistent.Plugin.CustomItemType"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeBaseItem_WithUnknownType_LogsWarning()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entity = new BaseItemEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = "NonExistent.Plugin.CustomItemType"
|
||||||
|
};
|
||||||
|
var loggerMock = new Mock<ILogger>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
loggerMock.Verify(
|
||||||
|
x => x.Log(
|
||||||
|
LogLevel.Warning,
|
||||||
|
It.IsAny<EventId>(),
|
||||||
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
It.IsAny<Exception?>(),
|
||||||
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeBaseItem_WithKnownType_ReturnsItem()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entity = new BaseItemEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = "MediaBrowser.Controller.Entities.Movies.Movie"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user