This is an automated email from the ASF dual-hosted git repository.

reswqa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git

commit c1f4c5ea64c1862c9c915f5d071e1a455da5a306
Author: Zihao Chen <[email protected]>
AuthorDate: Thu Jun 11 23:42:56 2026 +0800

    [FLINK-39911][historyserver] Extract AbstractHistoryServerHandler from 
HistoryServerStaticFileServerHandler
---
 ...dler.java => AbstractHistoryServerHandler.java} | 227 +++++++++++---------
 .../runtime/webmonitor/history/HistoryServer.java  |   5 +-
 .../HistoryServerStaticFileServerHandler.java      | 228 +--------------------
 .../history/AbstractHistoryServerHandlerTest.java  | 179 ++++++++++++++++
 .../HistoryServerStaticFileServerHandlerTest.java  |  95 ---------
 .../web-dashboard/src/app/app.interceptor.ts       |   2 +-
 6 files changed, 328 insertions(+), 408 deletions(-)

diff --git 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java
similarity index 62%
copy from 
flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
copy to 
flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java
index 40e5cb379d8..8b93ef01349 100644
--- 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
+++ 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandler.java
@@ -9,23 +9,16 @@
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
  */
 
 package org.apache.flink.runtime.webmonitor.history;
 
-/**
- * *************************************************************************** 
This code is based on
- * the "HttpStaticFileServerHandler" from the Netty project's HTTP server 
example.
- *
- * <p>See http://netty.io and
- * 
https://github.com/netty/netty/blob/4.0/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
- * ***************************************************************************
- */
 import org.apache.flink.runtime.rest.NotFoundException;
 import org.apache.flink.runtime.rest.handler.RestHandlerException;
 import 
org.apache.flink.runtime.rest.handler.legacy.files.StaticFileServerHandler;
@@ -60,7 +53,6 @@ import java.io.RandomAccessFile;
 import java.net.URI;
 import java.net.URL;
 import java.nio.file.Files;
-import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.Date;
@@ -74,46 +66,33 @@ import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpRes
 import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
 import static org.apache.flink.util.Preconditions.checkNotNull;
 
-/**
- * Simple file server handler used by the {@link HistoryServer} that serves 
requests to web
- * frontend's static files, such as HTML, CSS, JS or JSON files.
- *
- * <p>This code is based on the "HttpStaticFileServerHandler" from the Netty 
project's HTTP server
- * example.
- *
- * <p>This class is a copy of the {@link StaticFileServerHandler}. The 
differences are that the
- * request path is modified to end on ".json" if it does not have a filename 
extension; when
- * "index.html" is requested we load "index_hs.html" instead to inject the 
modified HistoryServer
- * WebInterface and that the caching of the "/joboverview" page is prevented.
- */
+/** Abstract base class for HistoryServer handlers. */
 @ChannelHandler.Sharable
-public class HistoryServerStaticFileServerHandler
+public abstract class AbstractHistoryServerHandler<Entry>
         extends SimpleChannelInboundHandler<RoutedRequest> {
 
-    /** Default logger, if none is specified. */
-    private static final Logger LOG =
-            
LoggerFactory.getLogger(HistoryServerStaticFileServerHandler.class);
-
-    // ------------------------------------------------------------------------
+    private static final Logger LOG = 
LoggerFactory.getLogger(AbstractHistoryServerHandler.class);
 
     /** The path in which the static documents are. */
-    private final File rootPath;
+    protected final File rootPath;
+
+    protected final ArchiveStorage<Entry> archiveStorage;
 
-    public HistoryServerStaticFileServerHandler(File rootPath) throws 
IOException {
+    protected AbstractHistoryServerHandler(ArchiveStorage<Entry> 
archiveStorage, File rootPath)
+            throws IOException {
+        this.archiveStorage = archiveStorage;
         this.rootPath = checkNotNull(rootPath).getCanonicalFile();
     }
 
-    // ------------------------------------------------------------------------
-    //  Responses to requests
-    // ------------------------------------------------------------------------
+    // 
-------------------------------------------------------------------------
+    //  Netty lifecycle
+    // 
-------------------------------------------------------------------------
 
     @Override
-    public void channelRead0(ChannelHandlerContext ctx, RoutedRequest 
routedRequest)
+    public final void channelRead0(ChannelHandlerContext ctx, RoutedRequest 
routedRequest)
             throws Exception {
-        String requestPath = routedRequest.getPath();
-
         try {
-            respondWithFile(ctx, routedRequest.getRequest(), requestPath);
+            respondToRequest(ctx, routedRequest);
         } catch (RestHandlerException rhe) {
             HandlerUtils.sendErrorResponse(
                     ctx,
@@ -124,9 +103,33 @@ public class HistoryServerStaticFileServerHandler
         }
     }
 
-    /** Response when running with leading JobManager. */
-    private void respondWithFile(ChannelHandlerContext ctx, HttpRequest 
request, String requestPath)
-            throws IOException, ParseException, RestHandlerException {
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        if (ctx.channel().isActive()) {
+            LOG.error("Caught exception", cause);
+            HandlerUtils.sendErrorResponse(
+                    ctx,
+                    false,
+                    new ErrorResponseBody("Internal server error."),
+                    INTERNAL_SERVER_ERROR,
+                    Collections.emptyMap());
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    //  Request handling
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Handles HTTP requests by routing to JSON resource or static file 
response.
+     *
+     * @param ctx Netty Channel context
+     * @param routedRequest The routed HTTP request
+     * @throws Exception Any exception that occurs during processing
+     */
+    protected void respondToRequest(ChannelHandlerContext ctx, RoutedRequest 
routedRequest)
+            throws Exception {
+        String requestPath = routedRequest.getPath();
 
         // make sure we request the "index.html" in case there is a directory 
request
         if (requestPath.endsWith("/")) {
@@ -135,49 +138,38 @@ public class HistoryServerStaticFileServerHandler
 
         if (!requestPath.contains(".")) { // we assume that the path ends in 
either .html or .js
             requestPath = requestPath + ".json";
-        }
 
-        // convert to absolute path
-        final File file = new File(rootPath, requestPath);
-
-        if (!file.exists()) {
-            // file does not exist. Try to load it with the classloader
-            ClassLoader cl = 
HistoryServerStaticFileServerHandler.class.getClassLoader();
-
-            try (InputStream resourceStream = cl.getResourceAsStream("web" + 
requestPath)) {
-                boolean success = false;
-                try {
-                    if (resourceStream != null) {
-                        URL root = cl.getResource("web");
-                        URL requested = cl.getResource("web" + requestPath);
-
-                        if (root != null && requested != null) {
-                            URI rootURI = new URI(root.getPath()).normalize();
-                            URI requestedURI = new 
URI(requested.getPath()).normalize();
-
-                            // Check that we don't load anything from outside 
of the
-                            // expected scope.
-                            if 
(!rootURI.relativize(requestedURI).equals(requestedURI)) {
-                                LOG.debug("Loading missing file from 
classloader: {}", requestPath);
-                                // ensure that directory to file exists.
-                                file.getParentFile().mkdirs();
-                                Files.copy(resourceStream, file.toPath());
-
-                                success = true;
-                            }
-                        }
-                    }
-                } catch (Throwable t) {
-                    LOG.error("error while responding", t);
-                } finally {
-                    if (!success) {
-                        LOG.debug("Unable to load requested file {} from 
classloader", requestPath);
-                        throw new NotFoundException("File not found.");
-                    }
-                }
+            LOG.debug("Responding to request for path {}", requestPath);
+            Entry resource = loadResource(requestPath);
+
+            if (resource == null) {
+                LOG.debug("Unable to load requested resource {}", requestPath);
+                throw new NotFoundException("Resource not found.");
+            }
+
+            respondWithResource(ctx, routedRequest.getRequest(), requestPath, 
resource);
+        } else {
+            File destFile = new File(rootPath, requestPath);
+            if (!destFile.exists()) {
+                tryLoadFromClassloader(destFile, requestPath);
             }
+            responseWithFile(ctx, routedRequest.getRequest(), requestPath, 
destFile);
         }
+    }
+
+    // 
-------------------------------------------------------------------------
+    //  Response helpers
+    // 
-------------------------------------------------------------------------
+
+    /** Respond to the request with the resource. */
+    protected abstract void respondWithResource(
+            ChannelHandlerContext ctx, HttpRequest request, String 
requestPath, Entry resource)
+            throws Exception;
 
+    /** Respond to the request with the file. */
+    protected void responseWithFile(
+            ChannelHandlerContext ctx, HttpRequest request, String 
requestPath, File file)
+            throws Exception {
         StaticFileServerHandler.checkFileValidity(file, rootPath, LOG);
 
         // cache validation
@@ -267,16 +259,65 @@ public class HistoryServerStaticFileServerHandler
         }
     }
 
-    @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
-        if (ctx.channel().isActive()) {
-            LOG.error("Caught exception", cause);
-            HandlerUtils.sendErrorResponse(
-                    ctx,
-                    false,
-                    new ErrorResponseBody("Internal server error."),
-                    INTERNAL_SERVER_ERROR,
-                    Collections.emptyMap());
+    // 
-------------------------------------------------------------------------
+    //  Resource loading
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Loads the resource for the given request path from the archive storage.
+     *
+     * @param requestPath The request path
+     * @return The resource for the given request path, or null if not found
+     */
+    private Entry loadResource(String requestPath) throws Exception {
+        String requestKey = requestPath.startsWith("/") ? 
requestPath.substring(1) : requestPath;
+        if (archiveStorage.exists(requestKey)) {
+            return archiveStorage.getEntry(requestKey);
+        }
+        return null;
+    }
+
+    /**
+     * Attempts to load a missing resource from the classloader's {@code web/} 
resource directory
+     * and copies it to {@code destFile} on disk.
+     *
+     * @param destFile destination file
+     * @param requestPath relative resource path (e.g. {@code /index.html})
+     */
+    protected void tryLoadFromClassloader(File destFile, String requestPath) 
throws Exception {
+        ClassLoader cl = AbstractHistoryServerHandler.class.getClassLoader();
+
+        try (InputStream resourceStream = cl.getResourceAsStream("web" + 
requestPath)) {
+            boolean success = false;
+            try {
+                if (resourceStream != null) {
+                    URL root = cl.getResource("web");
+                    URL requested = cl.getResource("web" + requestPath);
+
+                    if (root != null && requested != null) {
+                        URI rootURI = new URI(root.getPath()).normalize();
+                        URI requestedURI = new 
URI(requested.getPath()).normalize();
+
+                        // Check that we don't load anything from outside of 
the
+                        // expected scope.
+                        if 
(!rootURI.relativize(requestedURI).equals(requestedURI)) {
+                            LOG.debug("Loading missing resource from 
classloader: {}", requestPath);
+                            // ensure that directory to file exists.
+                            destFile.getParentFile().mkdirs();
+                            Files.copy(resourceStream, destFile.toPath());
+
+                            success = true;
+                        }
+                    }
+                }
+            } catch (Throwable t) {
+                LOG.error("error while responding", t);
+            }
+
+            if (!success) {
+                LOG.debug("Unable to load requested resource {} from 
classloader", requestPath);
+                throw new NotFoundException("Resource not found.");
+            }
         }
     }
 }
diff --git 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java
 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java
index 8ddcd57a402..ba836be15ef 100644
--- 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java
+++ 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServer.java
@@ -363,7 +363,10 @@ public class HistoryServer {
                                             new GeneratedLogUrlHandler(
                                                     
CompletableFuture.completedFuture(pattern))));
 
-            router.addGet("/:*", new 
HistoryServerStaticFileServerHandler(webDir));
+            router.addGet(
+                    "/:*",
+                    new HistoryServerStaticFileServerHandler(
+                            (FileArchiveStorage) archiveStorage, webDir));
 
             createDashboardConfigFile();
 
diff --git 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
index 40e5cb379d8..695f8edfffe 100644
--- 
a/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
+++ 
b/flink-runtime-web/src/main/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandler.java
@@ -26,53 +26,14 @@ package org.apache.flink.runtime.webmonitor.history;
  * 
https://github.com/netty/netty/blob/4.0/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
  * ***************************************************************************
  */
-import org.apache.flink.runtime.rest.NotFoundException;
-import org.apache.flink.runtime.rest.handler.RestHandlerException;
 import 
org.apache.flink.runtime.rest.handler.legacy.files.StaticFileServerHandler;
-import org.apache.flink.runtime.rest.handler.router.RoutedRequest;
-import org.apache.flink.runtime.rest.handler.util.HandlerUtils;
-import org.apache.flink.runtime.rest.messages.ErrorResponseBody;
 
-import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFuture;
-import org.apache.flink.shaded.netty4.io.netty.channel.ChannelFutureListener;
 import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandler;
 import org.apache.flink.shaded.netty4.io.netty.channel.ChannelHandlerContext;
-import org.apache.flink.shaded.netty4.io.netty.channel.DefaultFileRegion;
-import 
org.apache.flink.shaded.netty4.io.netty.channel.SimpleChannelInboundHandler;
-import 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.DefaultHttpResponse;
-import 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpChunkedInput;
-import 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaderValues;
 import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpRequest;
-import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponse;
-import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpUtil;
-import 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.LastHttpContent;
-import org.apache.flink.shaded.netty4.io.netty.handler.ssl.SslHandler;
-import org.apache.flink.shaded.netty4.io.netty.handler.stream.ChunkedFile;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.RandomAccessFile;
-import java.net.URI;
-import java.net.URL;
-import java.nio.file.Files;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.Date;
-import java.util.Locale;
-
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpHeaderNames.IF_MODIFIED_SINCE;
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus.OK;
-import static 
org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
-import static org.apache.flink.util.Preconditions.checkNotNull;
 
 /**
  * Simple file server handler used by the {@link HistoryServer} that serves 
requests to web
@@ -87,20 +48,17 @@ import static 
org.apache.flink.util.Preconditions.checkNotNull;
  * WebInterface and that the caching of the "/joboverview" page is prevented.
  */
 @ChannelHandler.Sharable
-public class HistoryServerStaticFileServerHandler
-        extends SimpleChannelInboundHandler<RoutedRequest> {
-
-    /** Default logger, if none is specified. */
-    private static final Logger LOG =
-            
LoggerFactory.getLogger(HistoryServerStaticFileServerHandler.class);
+public class HistoryServerStaticFileServerHandler extends 
AbstractHistoryServerHandler<File> {
 
     // ------------------------------------------------------------------------
 
-    /** The path in which the static documents are. */
-    private final File rootPath;
-
     public HistoryServerStaticFileServerHandler(File rootPath) throws 
IOException {
-        this.rootPath = checkNotNull(rootPath).getCanonicalFile();
+        this(new FileArchiveStorage(rootPath), rootPath);
+    }
+
+    public HistoryServerStaticFileServerHandler(
+            FileArchiveStorage fileArchiveStorage, File rootPath) throws 
IOException {
+        super(fileArchiveStorage, rootPath);
     }
 
     // ------------------------------------------------------------------------
@@ -108,175 +66,9 @@ public class HistoryServerStaticFileServerHandler
     // ------------------------------------------------------------------------
 
     @Override
-    public void channelRead0(ChannelHandlerContext ctx, RoutedRequest 
routedRequest)
+    protected void respondWithResource(
+            ChannelHandlerContext ctx, HttpRequest request, String 
requestPath, File file)
             throws Exception {
-        String requestPath = routedRequest.getPath();
-
-        try {
-            respondWithFile(ctx, routedRequest.getRequest(), requestPath);
-        } catch (RestHandlerException rhe) {
-            HandlerUtils.sendErrorResponse(
-                    ctx,
-                    routedRequest.getRequest(),
-                    new ErrorResponseBody(rhe.getMessage()),
-                    rhe.getHttpResponseStatus(),
-                    Collections.emptyMap());
-        }
-    }
-
-    /** Response when running with leading JobManager. */
-    private void respondWithFile(ChannelHandlerContext ctx, HttpRequest 
request, String requestPath)
-            throws IOException, ParseException, RestHandlerException {
-
-        // make sure we request the "index.html" in case there is a directory 
request
-        if (requestPath.endsWith("/")) {
-            requestPath = requestPath + "index.html";
-        }
-
-        if (!requestPath.contains(".")) { // we assume that the path ends in 
either .html or .js
-            requestPath = requestPath + ".json";
-        }
-
-        // convert to absolute path
-        final File file = new File(rootPath, requestPath);
-
-        if (!file.exists()) {
-            // file does not exist. Try to load it with the classloader
-            ClassLoader cl = 
HistoryServerStaticFileServerHandler.class.getClassLoader();
-
-            try (InputStream resourceStream = cl.getResourceAsStream("web" + 
requestPath)) {
-                boolean success = false;
-                try {
-                    if (resourceStream != null) {
-                        URL root = cl.getResource("web");
-                        URL requested = cl.getResource("web" + requestPath);
-
-                        if (root != null && requested != null) {
-                            URI rootURI = new URI(root.getPath()).normalize();
-                            URI requestedURI = new 
URI(requested.getPath()).normalize();
-
-                            // Check that we don't load anything from outside 
of the
-                            // expected scope.
-                            if 
(!rootURI.relativize(requestedURI).equals(requestedURI)) {
-                                LOG.debug("Loading missing file from 
classloader: {}", requestPath);
-                                // ensure that directory to file exists.
-                                file.getParentFile().mkdirs();
-                                Files.copy(resourceStream, file.toPath());
-
-                                success = true;
-                            }
-                        }
-                    }
-                } catch (Throwable t) {
-                    LOG.error("error while responding", t);
-                } finally {
-                    if (!success) {
-                        LOG.debug("Unable to load requested file {} from 
classloader", requestPath);
-                        throw new NotFoundException("File not found.");
-                    }
-                }
-            }
-        }
-
-        StaticFileServerHandler.checkFileValidity(file, rootPath, LOG);
-
-        // cache validation
-        final String ifModifiedSince = 
request.headers().get(IF_MODIFIED_SINCE);
-        if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
-            SimpleDateFormat dateFormatter =
-                    new 
SimpleDateFormat(StaticFileServerHandler.HTTP_DATE_FORMAT, Locale.US);
-            Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);
-
-            // Only compare up to the second because the datetime format we 
send to the client
-            // does not have milliseconds
-            long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 
1000;
-            long fileLastModifiedSeconds = file.lastModified() / 1000;
-            if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug(
-                            "Responding 'NOT MODIFIED' for file '" + 
file.getAbsolutePath() + '\'');
-                }
-
-                StaticFileServerHandler.sendNotModified(ctx);
-                return;
-            }
-        }
-
-        if (LOG.isDebugEnabled()) {
-            LOG.debug("Responding with file '" + file.getAbsolutePath() + 
'\'');
-        }
-
-        // Don't need to close this manually. Netty's DefaultFileRegion will 
take care of it.
-        final RandomAccessFile raf;
-        try {
-            raf = new RandomAccessFile(file, "r");
-        } catch (FileNotFoundException e) {
-            if (LOG.isDebugEnabled()) {
-                LOG.debug("Could not find file {}.", file.getAbsolutePath());
-            }
-            HandlerUtils.sendErrorResponse(
-                    ctx,
-                    request,
-                    new ErrorResponseBody("File not found."),
-                    NOT_FOUND,
-                    Collections.emptyMap());
-            return;
-        }
-
-        try {
-            long fileLength = raf.length();
-
-            HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
-            StaticFileServerHandler.setContentTypeHeader(response, file);
-
-            // the job overview should be updated as soon as possible
-            if (!requestPath.equals("/joboverview.json")) {
-                StaticFileServerHandler.setDateAndCacheHeaders(response, file);
-            }
-            if (HttpUtil.isKeepAlive(request)) {
-                response.headers().set(CONNECTION, 
HttpHeaderValues.KEEP_ALIVE);
-            }
-            HttpUtil.setContentLength(response, fileLength);
-
-            // write the initial line and the header.
-            ctx.write(response);
-
-            // write the content.
-            ChannelFuture lastContentFuture;
-            if (ctx.pipeline().get(SslHandler.class) == null) {
-                ctx.write(
-                        new DefaultFileRegion(raf.getChannel(), 0, fileLength),
-                        ctx.newProgressivePromise());
-                lastContentFuture = 
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
-            } else {
-                lastContentFuture =
-                        ctx.writeAndFlush(
-                                new HttpChunkedInput(new ChunkedFile(raf, 0, 
fileLength, 8192)),
-                                ctx.newProgressivePromise());
-                // HttpChunkedInput will write the end marker 
(LastHttpContent) for us.
-            }
-
-            // close the connection, if no keep-alive is needed
-            if (!HttpUtil.isKeepAlive(request)) {
-                lastContentFuture.addListener(ChannelFutureListener.CLOSE);
-            }
-        } catch (Exception e) {
-            raf.close();
-            LOG.error("Failed to serve file.", e);
-            throw new RestHandlerException("Internal server error.", 
INTERNAL_SERVER_ERROR);
-        }
-    }
-
-    @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
-        if (ctx.channel().isActive()) {
-            LOG.error("Caught exception", cause);
-            HandlerUtils.sendErrorResponse(
-                    ctx,
-                    false,
-                    new ErrorResponseBody("Internal server error."),
-                    INTERNAL_SERVER_ERROR,
-                    Collections.emptyMap());
-        }
+        responseWithFile(ctx, request, requestPath, file);
     }
 }
diff --git 
a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java
 
b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java
new file mode 100644
index 00000000000..7a7107da5d4
--- /dev/null
+++ 
b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/AbstractHistoryServerHandlerTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.runtime.webmonitor.history;
+
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.rest.handler.router.Router;
+import org.apache.flink.runtime.webmonitor.testutils.HttpUtils;
+import org.apache.flink.runtime.webmonitor.utils.WebFrontendBootstrap;
+import org.apache.flink.testutils.junit.extensions.parameterized.Parameter;
+import 
org.apache.flink.testutils.junit.extensions.parameterized.ParameterizedTestExtension;
+import org.apache.flink.testutils.junit.extensions.parameterized.Parameters;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Common HTTP-level tests for {@link AbstractHistoryServerHandler} 
subclasses. New subclasses can
+ * be exercised by adding an entry to {@link #handlerFactories()}.
+ */
+@ExtendWith(ParameterizedTestExtension.class)
+public class AbstractHistoryServerHandlerTest {
+
+    /** Factory that creates a concrete handler bound to the given web 
directory. */
+    @FunctionalInterface
+    public interface HandlerFactory {
+        AbstractHistoryServerHandler<?> create(File webDir) throws Exception;
+    }
+
+    /**
+     * Provides the concrete {@link AbstractHistoryServerHandler} 
implementations under test. To
+     * cover an additional handler simply add another factory here.
+     */
+    @Parameters(name = "handlerFactory={0}")
+    private static Collection<HandlerFactory> handlerFactories() {
+        HandlerFactory staticFileServerHandlerFactory = 
HistoryServerStaticFileServerHandler::new;
+        return Collections.singletonList(staticFileServerHandlerFactory);
+    }
+
+    @Parameter public HandlerFactory handlerFactory;
+
+    @TempDir private Path tmpDir;
+
+    private Path webDir;
+    private AbstractHistoryServerHandler<?> handler;
+    private WebFrontendBootstrap webUI;
+    private String baseUrl;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        webDir = Files.createDirectory(tmpDir.resolve("webDir"));
+        final Path uploadDir = 
Files.createDirectory(tmpDir.resolve("uploadDir"));
+
+        handler = handlerFactory.create(webDir.toFile());
+        Router<?> router = new Router().addGet("/:*", handler);
+        webUI =
+                new WebFrontendBootstrap(
+                        router,
+                        LoggerFactory.getLogger(getClass()),
+                        uploadDir.toFile(),
+                        null,
+                        "localhost",
+                        0,
+                        new Configuration());
+        baseUrl = "http://localhost:"; + webUI.getServerPort();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (webUI != null) {
+            webUI.shutdown();
+        }
+    }
+
+    /**
+     * Tests requests against static files served from the {@code web/} 
resources packaged with the
+     * handler module:
+     *
+     * <ul>
+     *   <li>a request for {@code /} is rewritten to {@code /index.html};
+     *   <li>{@code /index.html} is loaded via the classloader fallback when 
not present on disk;
+     *   <li>a missing static file (e.g. {@code /hello.html}) results in 404.
+     * </ul>
+     */
+    @TestTemplate
+    void testRespondWithStaticFile() throws Exception {
+        // /index.html is loaded from the classloader (web/index.html) and 
served
+        Tuple2<Integer, String> index = HttpUtils.getFromHTTP(baseUrl + 
"/index.html");
+        assertThat(index.f0).isEqualTo(200);
+        assertThat(index.f1).contains("Apache Flink Web Dashboard");
+
+        // a trailing slash is rewritten to "/index.html"
+        Tuple2<Integer, String> index2 = HttpUtils.getFromHTTP(baseUrl + "/");
+        assertThat(index2).isEqualTo(index);
+
+        // a static file that is neither on disk nor in the classloader yields 
404
+        Tuple2<Integer, String> missing = HttpUtils.getFromHTTP(baseUrl + 
"/hello.html");
+        assertThat(missing.f0).isEqualTo(404);
+        assertThat(missing.f1).contains("not found");
+    }
+
+    /**
+     * Tests file-system safety checks performed by {@code responseWithFile}:
+     *
+     * <ul>
+     *   <li>a directory request returns 405;
+     *   <li>a path that escapes the {@code webDir} returns 403.
+     * </ul>
+     */
+    @TestTemplate
+    void testRespondWithFile() throws Exception {
+        // requesting a directory rather than a file yields 405
+        Files.createDirectory(webDir.resolve("dir.json"));
+        Tuple2<Integer, String> dirNotFound = HttpUtils.getFromHTTP(baseUrl + 
"/dir.json");
+        assertThat(dirNotFound.f0).isEqualTo(405);
+        assertThat(dirNotFound.f1).contains("not found");
+
+        // requesting a file outside of the webDir is rejected with 403
+        Files.createFile(tmpDir.resolve("secret"));
+        Tuple2<Integer, String> outsideWebDir = HttpUtils.getFromHTTP(baseUrl 
+ "/../secret");
+        assertThat(outsideWebDir.f0).isEqualTo(403);
+        assertThat(outsideWebDir.f1).contains("Forbidden");
+    }
+
+    /**
+     * Tests requests served via the {@link ArchiveStorage} resource.
+     *
+     * <ul>
+     *   <li>an existing entry is returned with its content;
+     *   <li>a missing entry results in 404.
+     * </ul>
+     */
+    @TestTemplate
+    void testRespondWithResource() throws Exception {
+        String resourcePath = "overviews/job1.json";
+        String resourceContent = "{\"job\":\"job1\"}";
+        handler.archiveStorage.putArchiveContent(resourcePath, 
resourceContent);
+
+        // request without an extension: handler will append ".json" and serve 
the
+        // entry from the ArchiveStorage via respondWithResource
+        Tuple2<Integer, String> resource = HttpUtils.getFromHTTP(baseUrl + 
"/overviews/job1");
+        assertThat(resource.f0).isEqualTo(200);
+        assertThat(resource.f1).isEqualTo(resourceContent);
+
+        // request a missing resource: archiveStorage returns null and the 
handler
+        // responds 404
+        Tuple2<Integer, String> missing = HttpUtils.getFromHTTP(baseUrl + 
"/hello");
+        assertThat(missing.f0).isEqualTo(404);
+        assertThat(missing.f1).contains("not found");
+    }
+}
diff --git 
a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandlerTest.java
 
b/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandlerTest.java
deleted file mode 100644
index d908cf633b8..00000000000
--- 
a/flink-runtime-web/src/test/java/org/apache/flink/runtime/webmonitor/history/HistoryServerStaticFileServerHandlerTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.flink.runtime.webmonitor.history;
-
-import org.apache.flink.api.java.tuple.Tuple2;
-import org.apache.flink.configuration.Configuration;
-import org.apache.flink.runtime.rest.handler.router.Router;
-import org.apache.flink.runtime.webmonitor.testutils.HttpUtils;
-import org.apache.flink.runtime.webmonitor.utils.WebFrontendBootstrap;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.slf4j.LoggerFactory;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/** Tests for the HistoryServerStaticFileServerHandler. */
-class HistoryServerStaticFileServerHandlerTest {
-
-    @Test
-    void testRespondWithFile(@TempDir Path tmpDir) throws Exception {
-        final Path webDir = Files.createDirectory(tmpDir.resolve("webDir"));
-        final Path uploadDir = 
Files.createDirectory(tmpDir.resolve("uploadDir"));
-
-        Router router =
-                new Router()
-                        .addGet("/:*", new 
HistoryServerStaticFileServerHandler(webDir.toFile()));
-        WebFrontendBootstrap webUI =
-                new WebFrontendBootstrap(
-                        router,
-                        
LoggerFactory.getLogger(HistoryServerStaticFileServerHandlerTest.class),
-                        uploadDir.toFile(),
-                        null,
-                        "localhost",
-                        0,
-                        new Configuration());
-
-        int port = webUI.getServerPort();
-        try {
-            // verify that 404 message is returned when requesting a 
non-existent file
-            Tuple2<Integer, String> notFound404 =
-                    HttpUtils.getFromHTTP("http://localhost:"; + port + 
"/hello");
-            assertThat(notFound404.f0).isEqualTo(404);
-            assertThat(notFound404.f1).contains("not found");
-
-            // verify that a) a file can be loaded using the ClassLoader and 
b) that the
-            // HistoryServer
-            // index_hs.html is injected
-            Tuple2<Integer, String> index =
-                    HttpUtils.getFromHTTP("http://localhost:"; + port + 
"/index.html");
-            assertThat(index.f0).isEqualTo(200);
-            assertThat(index.f1).contains("Apache Flink Web Dashboard");
-
-            // verify that index.html is appended if the request path ends on 
'/'
-            Tuple2<Integer, String> index2 =
-                    HttpUtils.getFromHTTP("http://localhost:"; + port + "/");
-            assertThat(index2).isEqualTo(index);
-
-            // verify that a 405 message is returned when requesting a 
directory
-            Files.createDirectory(webDir.resolve("dir.json"));
-            Tuple2<Integer, String> dirNotFound =
-                    HttpUtils.getFromHTTP("http://localhost:"; + port + "/dir");
-            assertThat(dirNotFound.f0).isEqualTo(405);
-            assertThat(dirNotFound.f1).contains("not found");
-
-            // verify that a 403 message is returned when requesting a file 
outside the webDir
-            Files.createFile(tmpDir.resolve("secret"));
-            Tuple2<Integer, String> dirOutsideDirectory =
-                    HttpUtils.getFromHTTP("http://localhost:"; + port + 
"/../secret");
-            assertThat(dirOutsideDirectory.f0).isEqualTo(403);
-            assertThat(dirOutsideDirectory.f1).contains("Forbidden");
-        } finally {
-            webUI.shutdown();
-        }
-    }
-}
diff --git a/flink-runtime-web/web-dashboard/src/app/app.interceptor.ts 
b/flink-runtime-web/web-dashboard/src/app/app.interceptor.ts
index f7bbc12f9dd..cfc85d20091 100644
--- a/flink-runtime-web/web-dashboard/src/app/app.interceptor.ts
+++ b/flink-runtime-web/web-dashboard/src/app/app.interceptor.ts
@@ -41,7 +41,7 @@ export class AppInterceptor implements HttpInterceptor {
   intercept(req: HttpRequest<unknown>, next: HttpHandler): 
Observable<HttpEvent<unknown>> {
     // Error response from below url should be ignored
     const ignoreErrorUrlEndsList = ['checkpoints/config', 'checkpoints'];
-    const ignoreErrorMessage = ['File not found.'];
+    const ignoreErrorMessage = ['File not found.', 'Resource not found.'];
     const option: NzNotificationDataOptions = {
       nzDuration: 0,
       nzStyle: { width: 'auto', 'white-space': 'pre-wrap' }


Reply via email to