[Issue]: UPnP requests fail if the meta data contains control characters #4667

Closed
opened 2025-12-22 00:27:35 +01:00 by backuprepo · 8 comments
Owner

Originally created by @iamsrp on GitHub (Feb 24, 2023).

Please describe your bug

Retrieving data via UPnP will fail with an exception in the server and a 400 error in the client if an MP3 has control characters in its meta-data. The exception is something like:

[2023-02-19 20:23:36.477 -08:00] [ERR] Error processing request. URL "POST" "/dlna/02e0c80b-de70-4b3e-bdb7-156b908bb314/contentdirectory/control".
System.ArgumentException: '^E', hexadecimal value 0x05, is an invalid character.
   at System.Xml.XmlEncodedRawTextWriter.WriteElementTextBlock(Char* pSrc, Char* pSrcEnd)
   at System.Xml.XmlEncodedRawTextWriter.WriteString(String text)
   at System.Xml.XmlWellFormedWriter.WriteString(String text)
   at System.Xml.XmlWriter.WriteElementString(String localName, String value)
   at Emby.Dlna.Service.ControlErrorHandler.GetResponse(Exception ex)
   at Emby.Dlna.Service.BaseControlHandler.ProcessControlRequestAsync(ControlRequest request)
   at Jellyfin.Api.Controllers.DlnaServerController.ProcessContentDirectoryControlRequest(String serverId)
   at lambda_method322(Closure , Object )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Jellyfin.Server.Middleware.ServerStartupMessageMiddleware.Invoke(HttpContext httpContext, IServerApplicationHost serverApplicationHost, ILocalizationManager localizationManager)
   at Jellyfin.Server.Middleware.WebSocketHandlerMiddleware.Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
   at Jellyfin.Server.Middleware.IpBasedAccessValidationMiddleware.Invoke(HttpContext httpContext, INetworkManager networkManager)
   at Jellyfin.Server.Middleware.LanFilteringMiddleware.Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
   at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Jellyfin.Server.Middleware.QueryStringDecodingMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.ReDoc.ReDocMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Jellyfin.Server.Middleware.RobotsRedirectionMiddleware.Invoke(HttpContext httpContext)
   at Jellyfin.Server.Middleware.LegacyEmbyRouteRewriteMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
   at Jellyfin.Server.Middleware.ResponseTimeMiddleware.Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
   at Jellyfin.Server.Middleware.ExceptionMiddleware.Invoke(HttpContext context)

My reproducer script is:

#!/usr/bin/env python

import upnpy
from didl_lite import didl_lite
from fnmatch   import fnmatch

def get_songs(device, glob='Pop/Songs/*', dirname='', object_id='0'):
    print(f"Getting dirname='{dirname}' id={object_id} from '{device.friendly_name}'")
    try:
        data = device.ContentDirectory.Browse(
                   Filter        ='*',
                   ObjectID      =object_id,
                   BrowseFlag    ='BrowseDirectChildren',
                   StartingIndex ='0',
                   RequestedCount='-1',
                   SortCriteria  =''
               )
        entries = didl_lite.from_xml_string(data['Result'],
                                            strict=False)
    except Exception as e:
        print(f"Failed to browse {dirname}: {e}")
        return []

    # Okay, let's parse them
    result = []
    for entry in entries:
        # Create the path
        basename = entry.title.replace("/", "|")
        if dirname:
            path = f'{dirname}{basename}'
        else: 
            path = f'{basename}'
        # Different actions depending on what we have
        if isinstance(entry, didl_lite.StorageFolder):
            # Recurse into folder?
            path += '/'
            if (len(glob.split('/')) > len(path.split('/')) or
                fnmatch(path, glob)):
                result.extend(get_songs(device,
                                        glob     =glob,
                                        dirname  =path,
                                        object_id=entry.id))
        elif isinstance(entry, didl_lite.MusicTrack):
            # Parse song info
            try:
                result.append({
                    'name'   :  entry.title      or None,
                    'album'  :  entry.album      or None,
                    'artist' :  entry.artist     or None,
                    'url'    : (entry.res[0].uri or None),
                    'entry'  :  entry,
                    'path'   : path,
                })
            except Exception:
                # Ignore wonky ones
                pass

    # And give them all back
    print("Got %d songs" % len(result))
    return result

# Find all the devices. Some might show up multiple times so we use their name
# to make them unique.
upnp = upnpy.UPnP()
devices = {str(d) : d for d in upnp.discover()}

# Store in a dict by "path", which should be unique even if we discover a song
# by multiple routes
songs = dict()
for device in devices.values():
    print("Parsing from %s" % device)
    songs.update({song['path'] : song
                  for song in get_songs(device)})
print("Done")
print("Got %d songs" % len(songs))

However, the current upnpy seems to be buggy and doesn't like Jellyfin for some reason, so you might need to use my tweaked version: https://github.com/iamsrp/upnpy

Thanks,

srp

Jellyfin Version

Other

if other:

10.8.4

Environment

- OS: Ubuntu 22.04.1 LTS
- Virtualization: Inside VirtualBox
- Clients: Cheesy script, above
- Reverse Proxy: No
- Networking: Host Bridge
- Storage: NFS

Jellyfin logs

See error description.

FFmpeg logs

N/A

Please attach any browser or client logs here

N/A

Please attach any screenshots here

N/A

Code of Conduct

  • I agree to follow this project's Code of Conduct
Originally created by @iamsrp on GitHub (Feb 24, 2023). ### Please describe your bug Retrieving data via UPnP will fail with an exception in the server and a 400 error in the client if an MP3 has control characters in its meta-data. The exception is something like: ``` [2023-02-19 20:23:36.477 -08:00] [ERR] Error processing request. URL "POST" "/dlna/02e0c80b-de70-4b3e-bdb7-156b908bb314/contentdirectory/control". System.ArgumentException: '^E', hexadecimal value 0x05, is an invalid character. at System.Xml.XmlEncodedRawTextWriter.WriteElementTextBlock(Char* pSrc, Char* pSrcEnd) at System.Xml.XmlEncodedRawTextWriter.WriteString(String text) at System.Xml.XmlWellFormedWriter.WriteString(String text) at System.Xml.XmlWriter.WriteElementString(String localName, String value) at Emby.Dlna.Service.ControlErrorHandler.GetResponse(Exception ex) at Emby.Dlna.Service.BaseControlHandler.ProcessControlRequestAsync(ControlRequest request) at Jellyfin.Api.Controllers.DlnaServerController.ProcessContentDirectoryControlRequest(String serverId) at lambda_method322(Closure , Object ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Jellyfin.Server.Middleware.ServerStartupMessageMiddleware.Invoke(HttpContext httpContext, IServerApplicationHost serverApplicationHost, ILocalizationManager localizationManager) at Jellyfin.Server.Middleware.WebSocketHandlerMiddleware.Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) at Jellyfin.Server.Middleware.IpBasedAccessValidationMiddleware.Invoke(HttpContext httpContext, INetworkManager networkManager) at Jellyfin.Server.Middleware.LanFilteringMiddleware.Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Jellyfin.Server.Middleware.QueryStringDecodingMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.ReDoc.ReDocMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Jellyfin.Server.Middleware.RobotsRedirectionMiddleware.Invoke(HttpContext httpContext) at Jellyfin.Server.Middleware.LegacyEmbyRouteRewriteMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context) at Jellyfin.Server.Middleware.ResponseTimeMiddleware.Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager) at Jellyfin.Server.Middleware.ExceptionMiddleware.Invoke(HttpContext context) ``` My reproducer script is: ``` #!/usr/bin/env python import upnpy from didl_lite import didl_lite from fnmatch import fnmatch def get_songs(device, glob='Pop/Songs/*', dirname='', object_id='0'): print(f"Getting dirname='{dirname}' id={object_id} from '{device.friendly_name}'") try: data = device.ContentDirectory.Browse( Filter ='*', ObjectID =object_id, BrowseFlag ='BrowseDirectChildren', StartingIndex ='0', RequestedCount='-1', SortCriteria ='' ) entries = didl_lite.from_xml_string(data['Result'], strict=False) except Exception as e: print(f"Failed to browse {dirname}: {e}") return [] # Okay, let's parse them result = [] for entry in entries: # Create the path basename = entry.title.replace("/", "|") if dirname: path = f'{dirname}{basename}' else: path = f'{basename}' # Different actions depending on what we have if isinstance(entry, didl_lite.StorageFolder): # Recurse into folder? path += '/' if (len(glob.split('/')) > len(path.split('/')) or fnmatch(path, glob)): result.extend(get_songs(device, glob =glob, dirname =path, object_id=entry.id)) elif isinstance(entry, didl_lite.MusicTrack): # Parse song info try: result.append({ 'name' : entry.title or None, 'album' : entry.album or None, 'artist' : entry.artist or None, 'url' : (entry.res[0].uri or None), 'entry' : entry, 'path' : path, }) except Exception: # Ignore wonky ones pass # And give them all back print("Got %d songs" % len(result)) return result # Find all the devices. Some might show up multiple times so we use their name # to make them unique. upnp = upnpy.UPnP() devices = {str(d) : d for d in upnp.discover()} # Store in a dict by "path", which should be unique even if we discover a song # by multiple routes songs = dict() for device in devices.values(): print("Parsing from %s" % device) songs.update({song['path'] : song for song in get_songs(device)}) print("Done") print("Got %d songs" % len(songs)) ``` However, the current `upnpy` seems to be buggy and doesn't like Jellyfin for some reason, so you might need to use my tweaked version: https://github.com/iamsrp/upnpy Thanks, srp ### Jellyfin Version Other ### if other: 10.8.4 ### Environment ```markdown - OS: Ubuntu 22.04.1 LTS - Virtualization: Inside VirtualBox - Clients: Cheesy script, above - Reverse Proxy: No - Networking: Host Bridge - Storage: NFS ``` ### Jellyfin logs ```shell See error description. ``` ### FFmpeg logs ```shell N/A ``` ### Please attach any browser or client logs here N/A ### Please attach any screenshots here N/A ### Code of Conduct - [X] I agree to follow this project's Code of Conduct
backuprepo 2025-12-22 00:27:35 +01:00
  • closed this issue
  • added the
    bug
    stale
    labels
Author
Owner

@jellyfin-bot commented on GitHub (Jun 25, 2023):

This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.

If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.

This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on Matrix or Social Media.

@jellyfin-bot commented on GitHub (Jun 25, 2023): This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
Author
Owner

@iamsrp commented on GitHub (Jun 29, 2023):

[Bump]

@iamsrp commented on GitHub (Jun 29, 2023): [Bump]
Author
Owner

@jellyfin-bot commented on GitHub (Oct 29, 2023):

This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.

If you have any questions you can use one of several ways to contact us.

@jellyfin-bot commented on GitHub (Oct 29, 2023): This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
Author
Owner

@iamsrp commented on GitHub (Oct 29, 2023):

[keepalive]

@iamsrp commented on GitHub (Oct 29, 2023): [keepalive]
Author
Owner

@cvium commented on GitHub (Oct 29, 2023):

Is there any way you can test this on unstable (backup your db etc.)?

@cvium commented on GitHub (Oct 29, 2023): Is there any way you can test this on unstable (backup your db etc.)?
Author
Owner

@iamsrp commented on GitHub (Oct 29, 2023):

Hmm, thanks for the nudge here. I just discovered that, since I logged this, Ubuntu commented out my jellyfin source when it upgraded to jammy. I'm upgrading now and will see if that fixes it.

If it doesn't, work then do I just replace main with unstable in this?

me@box:/etc/apt/sources.list.d$ cat jellyfin.list
deb [arch=amd64] https://repo.jellyfin.org/ubuntu jammy main

And upgrade? I'm assuming that backing up the DB just means a tarball of /var/lib/jellyfin, correct?

@iamsrp commented on GitHub (Oct 29, 2023): Hmm, thanks for the nudge here. I just discovered that, since I logged this, Ubuntu commented out my jellyfin source when it upgraded to `jammy`. I'm upgrading now and will see if that fixes it. If it doesn't, work then do I just replace `main` with unstable in this? ``` me@box:/etc/apt/sources.list.d$ cat jellyfin.list deb [arch=amd64] https://repo.jellyfin.org/ubuntu jammy main ``` And upgrade? I'm assuming that backing up the DB just means a tarball of `/var/lib/jellyfin`, correct?
Author
Owner

@cvium commented on GitHub (Oct 29, 2023):

I use two separate installs, so I don't know for certain. If that folder contains your library.db and jellyfin.db files, then that sounds about right.

@cvium commented on GitHub (Oct 29, 2023): I use two separate installs, so I don't know for certain. If that folder contains your library.db and jellyfin.db files, then that sounds about right.
Author
Owner

@iamsrp commented on GitHub (Oct 29, 2023):

Well, hurrah for procrastination. I upgraded to the latest version and my reproducer will no longer reproduce. That either means that the bad metadata has vanished since logging this (unlikely) or that the issue has been since fixed.

Either way, since I have no reproducer at this point there's nothing actionable here, so I'll close this issue.

Thanks for the help!

srp

@iamsrp commented on GitHub (Oct 29, 2023): Well, hurrah for procrastination. I upgraded to the latest version and my reproducer will no longer reproduce. That either means that the bad metadata has vanished since logging this (unlikely) or that the issue has been since fixed. Either way, since I have no reproducer at this point there's nothing actionable here, so I'll close this issue. Thanks for the help! srp
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: starred/jellyfin#4667
No description provided.