An ASP.NET Core library for injecting customizable transforms into the HTTP response pipeline. This library enables dynamic modification of HTTP responses, with specialized support for HTML document manipulation and content injection.
This library was built with Jellyfin plugins in mind, since the plugin API doesn't expose any UI capabilities. However, it is a general-purpose tool for use in any ASP.NET Core application and has no Jellyfin dependencies.
Note: Modifying the HTML files directly in the filesystem is not recommended, especially if being deployed via docker.
dotnet add package HttpResponseTransformerAdd the response transformer to the IServiceCollection, and configure any transforms.
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseTransformer(builder => builder
.TransformDocument(config => config
.InjectScript(script => script
.FromUrl("https://cdn.example.com/script.js")
.At("//head")
.AsAsync())
.InjectStyleSheet(css => css
.FromUrl("https://cdn.example.com/styles.css")
.At("//head"))));
}To use HttpResponseTransformer in a Jellyfin plugin, implement IPluginServiceRegistrator and add response transformer to the service collection.
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddResponseTransformer(config => config
.TransformDocument(injectPage => injectPage
.When(ctx => ctx.Request.Path == "/web")
.InjectScript(script => script
.FromEmbeddedResource($"{GetType().Namespace}.scripts.my-custom-script.js", GetType().Assembly)
.AsDeferred())));
}
}HttpResponseTransformer integrates into the ASP.NET Core's middleware pipeline to intercept and transform HTTP responses. Filters determine whether transforms should run (IResponseTransformer.ShouldTransform, or builder.When() for injection transforms). When a transform executes, the response stream is buffered in memory and transformed by IResponseTransformer.ExecuteTransform() before being returned to the client.
Important: Transforms buffer the entire response stream in memory. Care should be taken when working with large documents or streaming content.
ShouldTransform()orbuilder.When()should be used to target specific requests and avoid unnecessary buffering.
By default, HttpResponseTransformer bypasses response compression when executing a transform by temporarily clearing the Accept-Encoding HTTP header.
This behavior can be disabled by calling AllowResponseCompression().
services.AddResponseTransformer(builder => builder
.AllowResponseCompression()
.TransformDocument(config => config
...
));Warning: When response compression is enabled, transforms should be prepared to handle compressed content themselves. Built-in transforms will fail if they receive compressed data.
The library provides four types of transforms, each building on the previous level:
InjectContentResponseTransform- Pre-built transform for common content injection scenariosDocumentResponseTransform- Specialized for HTML documents, provides parsed DOM access via HtmlAgilityPackTextResponseTransform- Base class for text-based responses, handles encoding/decoding automaticallyIResponseTransform- Base interface for all transforms, works with raw byte arrays
The library includes a built-in embedded resource serving system that works alongside the transform pipeline:
- Resource Registration: Embedded resources are automatically registered when using
FromEmbeddedResource() - Automatic Serving: The library serves embedded resources at generated URLs (e.g.,
/_/{namespace-hash}/{resource-hash}) - Content Types: Proper content-type headers are set based on file extensions
This allows for bundling CSS, JavaScript, images, and other assets directly in an assembly and having them automatically served by the application.
Transforms are executed in the order they are registered. Each transform receives the output of the previous transform, allowing multiple transformations to be chained together.
Use the built-in content injection system to add scripts, stylesheets, HTML content, and images to HTML documents using XPath targeting. This is the most common use case and requires no custom code.
services.AddResponseTransformer(builder => builder
.TransformDocument(config => config
.InjectScript(script => script.FromEmbeddedResource("MyApp.Scripts.analytics.js", typeof(Program).Assembly).At("//body"))
.InjectStyleSheet(css => css.FromUrl("/css/styles.css").At("//head"))
.InjectImage(img => img.FromUrl("/images/logo.png").At("//div[@id='header']"))
.InjectHtml(html => html.WithContent("<div>Welcome!</div>").At("//body"))));Implement DocumentResponseTransform to create custom HTML document manipulations with full access to the parsed HTML DOM via HtmlAgilityPack.
public class MetaTagTransform : DocumentResponseTransform
{
public override bool ShouldTransform(HttpContext context) =>
context.Request.Path.StartsWithSegments("/static/html");
public override void ExecuteTransform(HttpContext context, ref HtmlDocument document)
{
var head = document.DocumentNode.SelectSingleNode("//head");
var metaTag = document.CreateElement("meta");
metaTag.SetAttributeValue("name", "generated-at");
metaTag.SetAttributeValue("content", DateTime.UtcNow.ToString());
head?.AppendChild(metaTag);
}
}
...
// Register the transform
services.AddTransient<IResponseTransform, MetaTagTransform>();Implement TextResponseTransform to perform string-based transformations on any text-based HTTP responses (HTML, JSON, XML, etc.).
public class TokenReplacementTransform : TextResponseTransform
{
public override bool ShouldTransform(HttpContext context) =>
context.Response.ContentType?.Contains("text/html") == true;
public override void ExecuteTransform(HttpContext context, ref string content)
{
content = content.Replace("{{USER_NAME}}", context.User.Identity?.Name ?? "Guest");
}
}
...
// Register the transform
services.AddTransient<IResponseTransform, TokenReplacementTransform>();Implement IResponseTransform directly to work with raw byte arrays for complete control over any response type.
public class ResizeImageTransform : IResponseTransform
{
public bool ShouldTransform(HttpContext context) =>
context.Response.ContentType?.StartsWith("image/") == true;
public void ExecuteTransform(HttpContext context, ref byte[] content)
{
// Resize image logic here
content = ResizeImage(content, maxWidth: 800, maxHeight: 600);
}
}
...
// Register the transform
services.AddTransient<IResponseTransform, ResizeImageTransform>();