mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-27 06:52:53 +00:00
Fix malformed query string in StreamInfo.ToUrl() causing 500 error via proxies
StreamInfo.ToUrl() generated URLs like `/master.m3u8?&DeviceId=...` (note `?&`)
because `?` was appended to the path and all parameters started with `&`. When
the first optional parameter (DeviceProfileId) was null, the result was a
malformed query string.
This is harmless when clients hit Jellyfin directly (ASP.NET Core tolerates `?&`),
but when accessed through a reverse proxy that parses and re-serializes the URL
(e.g. Home Assistant ingress via aiohttp/yarl), `?&` becomes `?=&` — introducing
an empty-key query parameter. ParseStreamOptions then crashes on `param.Key[0]`
with IndexOutOfRangeException.
Changes:
- StreamInfo.ToUrl(): Track query start position and replace the first `&` with
`?` after all parameters are appended, producing valid query strings
- ParseStreamOptions: Guard against empty query parameter keys
- Tests: Remove .Replace("?&", "?") workaround that masked the bug
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -268,7 +268,7 @@ public static class StreamingHelpers
|
||||
Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
|
||||
foreach (var param in queryString)
|
||||
{
|
||||
if (char.IsLower(param.Key[0]))
|
||||
if (param.Key.Length > 0 && char.IsLower(param.Key[0]))
|
||||
{
|
||||
// This was probably not parsed initially and should be a StreamOptions
|
||||
// or the generated URL should correctly serialize it
|
||||
|
||||
@@ -895,7 +895,7 @@ public class StreamInfo
|
||||
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
sb.Append("/master.m3u8?");
|
||||
sb.Append("/master.m3u8");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -906,10 +906,10 @@ public class StreamInfo
|
||||
sb.Append('.');
|
||||
sb.Append(Container);
|
||||
}
|
||||
|
||||
sb.Append('?');
|
||||
}
|
||||
|
||||
var queryStart = sb.Length;
|
||||
|
||||
if (!string.IsNullOrEmpty(DeviceProfileId))
|
||||
{
|
||||
sb.Append("&DeviceProfileId=");
|
||||
@@ -1133,6 +1133,12 @@ public class StreamInfo
|
||||
sb.Append(query);
|
||||
}
|
||||
|
||||
// Replace the first '&' with '?' to form a valid query string.
|
||||
if (sb.Length > queryStart)
|
||||
{
|
||||
sb[queryStart] = '?';
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -216,8 +216,7 @@ public class StreamInfoTests
|
||||
|
||||
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
|
||||
|
||||
// New version will return and & after the ? due to optional parameters.
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
|
||||
|
||||
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
|
||||
}
|
||||
@@ -234,8 +233,7 @@ public class StreamInfoTests
|
||||
FillAllProperties(streamInfo);
|
||||
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
|
||||
|
||||
// New version will return and & after the ? due to optional parameters.
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
|
||||
|
||||
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user