Skip to content

Introduce DirectoryBrowserSupportFilter extension point - #27216

Open
bphinz wants to merge 1 commit into
jenkinsci:masterfrom
bphinz:directory-browser-support-filter
Open

Introduce DirectoryBrowserSupportFilter extension point#27216
bphinz wants to merge 1 commit into
jenkinsci:masterfrom
bphinz:directory-browser-support-filter

Conversation

@bphinz

@bphinz bphinz commented Aug 9, 2026

Copy link
Copy Markdown

Allows plugins to filter or transform file input streams and HTTP response metadata served by DirectoryBrowserSupport (such as build artifacts and workspace files).

This extension point enables plugins to:

  • Perform on-the-fly stream decompression (e.g., viewing GZIP artifacts).
  • Perform artifact decryption.
  • Transform file formats for browser viewing (e.g., rendering Markdown to HTML).

Implement a plugin extension point suitable for provided the desired feature of PR #7288

Testing done

  • Added automated unit tests in DirectoryBrowserSupportTest.java:
    • directoryBrowserSupportFilterTest: Verifies stream interception and modification on served files.
    • directoryBrowserSupportMarkdownRenderingTest: Verifies converting Markdown content to HTML and setting Content-Type: text/html;charset=UTF-8 dynamically.
    • Verified using an external consumer plugin (gzip-artifact-view-plugin):
    • GzipDirectoryBrowserSupportFilterTest verified transparent on-the-fly decompression of .txt.gz build artifacts when viewed over HTTP.
    • All core unit tests and plugin integration tests passed (BUILD SUCCESS).

Screenshots (UI changes only)

Before

After

Proposed changelog entries

  • Add DirectoryBrowserSupportFilter extension point to allow plugins to filter or transform file content served by
    DirectoryBrowserSupport.

Proposed changelog category

/label developer

Proposed upgrade guidelines

N/A

Submitter checklist

  • The issue, if it exists, is well-described.
  • The changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developers, depending on the change) and are in the imperative mood (see examples). Fill in the Proposed upgrade guidelines section only if there are breaking changes or changes that may require extra steps from users during upgrade.
  • There is automated testing or an explanation as to why this change has no tests.
  • New public classes, fields, and methods are annotated with @Restricted or have @since TODO Javadocs, as appropriate.
  • New deprecations are annotated with @Deprecated(since = "TODO") or @Deprecated(forRemoval = true, since = "TODO"), if applicable.
  • UI changes do not introduce regressions when enforcing the current default rules of Content Security Policy Plugin. In particular, new or substantially changed JavaScript is not defined inline and does not call eval to ease future introduction of Content Security Policy (CSP) directives (see documentation).
  • For dependency updates, there are links to external changelogs and, if possible, full differentials.
  • For new APIs and extension points, there is a link to at least one consumer.

Desired reviewers

@jenkinsci/core-pr-reviewers
@timja
@dwnusbaum
@NotMyFault

Before the changes are marked as ready-for-merge:

Maintainer checklist

  • There are at least two (2) approvals for the pull request and no outstanding requests for change.
  • Conversations in the pull request are over, or it is explicit that a reviewer is not blocking the change.
  • Changelog entries in the pull request title and/or Proposed changelog entries are accurate, human-readable, and in the imperative mood.
  • Proper changelog labels are set so that the changelog can be generated automatically.
  • If the change needs additional upgrade steps from users, the upgrade-guide-needed label is set and there is a Proposed upgrade guidelines section in the pull request title (see example).
  • If it would make sense to backport the change to LTS, be a Bug or Improvement, and either the issue or pull request must be labeled as lts-candidate to be considered.

@comment-ops-bot comment-ops-bot Bot added the developer Changes which impact plugin developers label Aug 9, 2026
@timja
timja requested a balanced review from Copilot August 10, 2026 08:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an extension point for plugins to transform files served by DirectoryBrowserSupport, supporting use cases such as PR #7288’s GZIP artifact viewing.

Changes:

  • Adds a mutable filtering context for streams and response metadata.
  • Applies registered filters to view and normal file responses.
  • Adds stream and Markdown transformation tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
DirectoryBrowserSupportFilter.java Defines the extension API and filtering context.
DirectoryBrowserSupport.java Integrates filters into file-serving paths.
DirectoryBrowserSupportTest.java Tests content and metadata transformations.
Suppressed comments (1)

core/src/main/java/hudson/model/DirectoryBrowserSupport.java:453

  • Do not continue serving after a filter failure. A filter may already have consumed the current stream or partially mutated its metadata before throwing, so catching the exception can return empty/corrupt content; after a successful decrypting filter followed by a failing sanitizer, it can also serve an unsafe intermediate representation. Fail the request, or restore a pristine stream and context before continuing.
            } catch (Exception e) {
                LOGGER.log(Level.WARNING, "Failed to filter stream for " + baseFile + " using " + filter, e);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +388 to +390
if (contentType != null) {
rsp.setContentType(contentType);
}
Comment on lines +108 to +110
* Sets a new {@link InputStream} for the file content.
*/
public void setInputStream(@NonNull InputStream inputStream) {
}
}

@TestExtension("directoryBrowserSupportFilterTest")
* @return the transformed context (or the same context), or {@code null} if no changes are made
* @throws IOException if an I/O error occurs during filtering
*/
public abstract Context filter(@NonNull Context context) throws IOException;
Comment on lines +422 to +426
DirectoryBrowserSupportFilter.Context context = applyFilters(req, baseFile, in, length, false);
in = context.getInputStream();
length = context.getLength();
String fileName = context.getFileName();
String contentType = context.getContentType();
Allows plugins to filter or transform file input streams and HTTP response metadata served by DirectoryBrowserSupport (such as build artifacts and workspace files).

Potential use cases for plugins include, for example:
- Performing on-the-fly stream decompression (e.g., viewing GZIP artifacts).
- Performing artifact decryption.

Security & Resource Management:
- DirectoryBrowserSupportFilter.Context implements AutoCloseable to track and close superseded streams, preventing resource leaks.
- Fail-fast exception handling aborts failed filter executions cleanly.
@bphinz
bphinz force-pushed the directory-browser-support-filter branch from 4218aee to 3712c1f Compare August 11, 2026 23:58
@bphinz

bphinz commented Aug 12, 2026

Copy link
Copy Markdown
Author

@timja can you re-trigger the copilot review please?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (6)

core/src/main/java/hudson/model/DirectoryBrowserSupportFilter.java:159

  • close() leaves the active inputStream open. This leaks the stream from VirtualFile.open() when a filter throws before serveFile is called (the error path at applyFilters calls this method, but the current stream is not in supersededStreams). Close the current stream first, then the superseded streams so wrapper dependencies are released in the right order.
        @Override
        public void close() {
            for (InputStream s : supersededStreams) {
                IOUtils.closeQuietly(s);
            }

core/src/main/java/hudson/model/DirectoryBrowserSupport.java:434

  • This catch also handles failures from rsp.serveFile; if streaming has already committed the response, the subsequent sendError is invalid and may replace the useful client-disconnect exception with an illegal response-state failure. Limit this handling to applyFilters, or rethrow committed-response failures.
                } catch (IOException ioe) {
                    LOGGER.log(Level.WARNING, "Failed to serve file for " + baseFile, ioe);
                    rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

core/src/main/java/hudson/model/DirectoryBrowserSupport.java:446

  • Replacing the context drops ownership of the previous context and all streams it tracks. Since the API explicitly permits returning a transformed Context, a filter that constructs a new context leaves the prior file stream open and can leak a descriptor on every request. Make the pipeline mutate one context (for example, make filter return void/disallow a different instance), or define and implement an ownership transfer that safely handles wrapper streams.
                DirectoryBrowserSupportFilter.Context next = filter.filter(context);
                if (next != null) {
                    context = next;

core/src/main/java/hudson/model/DirectoryBrowserSupportFilter.java:124

  • Replacing the stream retains the old content length, even though transformations such as the advertised decompression normally change it. If a plugin does not also call setLength, serveFile receives a stale length and may emit an incorrect Content-Length, truncating the response or causing clients to wait for bytes that never arrive. Invalidate the length on replacement; filters that know the new size can set it afterward.
        public void setInputStream(@NonNull InputStream inputStream) {
            if (this.inputStream != inputStream) {
                this.supersededStreams.add(this.inputStream);
                this.inputStream = inputStream;
            }

core/src/main/java/hudson/model/DirectoryBrowserSupport.java:395

  • This catch also covers rsp.serveFile, which can throw after response headers/body have been committed (for example, on a client disconnect). Calling sendError on an already committed response is invalid and can mask the original I/O failure. Scope the catch to filter setup, or rethrow when rsp.isCommitted() is true.

This issue also appears on line 432 of the same file.

            } catch (IOException ioe) {
                LOGGER.log(Level.WARNING, "Failed to serve file for " + baseFile, ioe);
                rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java:1569

  • The added tests only exercise stream and length replacement; none covers the new response-metadata path (setFileName driving MIME type/headers), although the PR description claims a Markdown-to-HTML content-type test. Add a test filter that changes the filename to an HTML extension and assert the resulting Content-Type and disposition behavior.
                context.setInputStream(new java.io.ByteArrayInputStream(bytes));
                context.setLength(bytes.length);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

developer Changes which impact plugin developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants