Repository: netty/netty
Issue: Considering supporting http2 chunked input

In Http1, we can use a `ChunkedWriteHandler`  to write a `HttpChunkedInput`,  but we can only use the `Http2DataWriter#writeData(xx)` to write a `ByteBuf` instance in Http2.  

Considering supporting `writeChunked(xx)` or a unity class (probably implemetated by `Http2RemoteFlowController#isWritable()` and `Http2RemoteFlowController$Listener#writabilityChanged(stream)`) for writing a large data stream asynchronously which would cause OOM?

Issue comments:
I will see this for sure.
> I will see this for sure.

sorry, is there any conclusion?
HTTP/2 has chunked data frames already. Do you want some sort of automatic chunking of a big `ByteBuf` or `RandomFileAccess`?
Yes, just something like the `ChunkedInput` for writing a large file.
@Ech0Fan It's on my TO-DO list. But super busy these days so I'll tackle this a little later.
Hi @normanmaurer 
As this ticket is still open, do you confirm there's currently no way to use `ChunkedInput` with HTTP/2 in Netty?
Cheers
lol, I seriously forgot this. I will fire a POC PR by this weekend.
Actually, I suspect my issue is that I have a `ChunkedInput<ByteBuf>` but it might work if I have a `ChunkedInput<HttpContent>`.
Confirmed. `ChunkedInput<HttpContent>` works nicely with `HttpToHttp2ConnectionHandler`. My issue was that `HttpToHttp2ConnectionHandler` lets anything else than `HttpMessage` and `HttpContent` pass through instead of generating DataFrames. I just had to change my `ChunkedInput<ByteBuf>` and decorate my chunks with `DefaultHttpContent`.
There we go, there are three HTTP/2 implementation (if I'm correct). Should I focus on `Http2FrameCodec`?

@normanmaurer Let me know your opinion. 
@slandelle this should be fixed now... 
Thanks to @hyperxpro 

Pull request review comments:
```suggestion
public final  class Http2DataChunkedInput implements ChunkedInput<Http2DataFrame> {
```
```suggestion
        return false;
```
We can optimize this a bit imho.

```suggestion
    public Http2DataFrame readChunk(ByteBufAllocator allocator) throws Exception {
        if (endStreamSent) {
            return null;
        }
        if (input.isEndOfInput()) {
            endStreamSent = true;
            return new DefaultHttp2DataFrame(true).stream(stream);
        }
        ByteBuf buf = input.readChunk(allocator);
        if (buf == null) {
            return null;
        }
        final Http2DataFrame dataFrame = new DefaultHttp2DataFrame(buf, input.isEndOfInput()).stream(stream);
        if (dataFrame.isEndStream()) {
            endStreamSent = true;
        }
        return dataFrame;
    }
```
don't use wildcards 
```suggestion
    @Override
    public Http2DataFrame readChunk(ByteBufAllocator allocator) throws Exception {
```
Honestly I think this is not something we need to assert.`
```suggestion
```

Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2DataChunkedInput.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2DataChunkedInput.java
new file mode 100644
index 00000000000..ac382ffde63
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2DataChunkedInput.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ * https://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 io.netty.handler.codec.http2;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.stream.ChunkedInput;
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * A {@link ChunkedInput} that fetches data chunk by chunk for use with HTTP/2 Data Frames.
+ * <p>
+ * Each chunk from the input data will be wrapped within a {@link Http2DataFrame}. At the end of the input data,
+ * {@link Http2DataFrame#isEndStream()} will be set to true and will be written.
+ * <p>
+ * <p>
+ * <pre>
+ *
+ *     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ *         if (msg instanceof Http2HeadersFrame) {
+ *             Http2HeadersFrame http2HeadersFrame = (Http2HeadersFrame) msg;
+ *
+ *             Http2HeadersFrame response = new DefaultHttp2HeadersFrame(new DefaultHttp2Headers().status("200"));
+ *             response.stream(http2HeadersFrame.stream());
+ *             ctx.write(response);
+ *
+ *             ChannelFuture sendFileFuture = ctx.writeAndFlush(new Http2DataChunkedInput(
+ *                     new ChunkedFile(new File(("/home/meow/cats.mp4"))), http2HeadersFrame.stream()));
+ *         }
+ *     }
+ * </pre>
+ */
+public final class Http2DataChunkedInput implements ChunkedInput<Http2DataFrame> {
+
+    private final ChunkedInput<ByteBuf> input;
+    private final Http2FrameStream stream;
+    private boolean endStreamSent;
+
+    /**
+     * Creates a new instance using the specified input.
+     *
+     * @param input  {@link ChunkedInput} containing data to write
+     * @param stream {@link Http2FrameStream} holding stream info
+     */
+    public Http2DataChunkedInput(ChunkedInput<ByteBuf> input, Http2FrameStream stream) {
+        this.input = ObjectUtil.checkNotNull(input, "input");
+        this.stream = ObjectUtil.checkNotNull(stream, "stream");
+    }
+
+    @Override
+    public boolean isEndOfInput() throws Exception {
+        if (input.isEndOfInput()) {
+            // Only end of input after last HTTP chunk has been sent
+            return endStreamSent;
+        }
+        return false;
+    }
+
+    @Override
+    public void close() throws Exception {
+        input.close();
+    }
+
+    @Deprecated
+    @Override
+    public Http2DataFrame readChunk(ChannelHandlerContext ctx) throws Exception {
+        return readChunk(ctx.alloc());
+    }
+
+    @Override
+    public Http2DataFrame readChunk(ByteBufAllocator allocator) throws Exception {
+        if (endStreamSent) {
+            return null;
+        }
+
+        if (input.isEndOfInput()) {
+            endStreamSent = true;
+            return new DefaultHttp2DataFrame(true).stream(stream);
+        }
+
+        ByteBuf buf = input.readChunk(allocator);
+        if (buf == null) {
+            return null;
+        }
+
+        final Http2DataFrame dataFrame = new DefaultHttp2DataFrame(buf, input.isEndOfInput()).stream(stream);
+        if (dataFrame.isEndStream()) {
+            endStreamSent = true;
+        }
+
+        return dataFrame;
+    }
+
+    @Override
+    public long length() {
+        return input.length();
+    }
+
+    @Override
+    public long progress() {
+        return input.progress();
+    }
+}
diff --git a/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServer.java b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServer.java
new file mode 100644
index 00000000000..03a04b60a6a
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServer.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   https://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 io.netty.example.http2.file;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.SupportedCipherSuiteFilter;
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+
+public final class Http2StaticFileServer {
+
+    private static final int PORT = Integer.parseInt(System.getProperty("port", "8443"));
+
+    public static void main(String[] args) throws Exception {
+        SelfSignedCertificate ssc = new SelfSignedCertificate();
+        SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+                .sslProvider(SslProvider.JDK)
+                .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
+                .applicationProtocolConfig(new ApplicationProtocolConfig(
+                        ApplicationProtocolConfig.Protocol.ALPN,
+                        // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
+                        ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+                        // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
+                        ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+                        ApplicationProtocolNames.HTTP_2,
+                        ApplicationProtocolNames.HTTP_1_1))
+                .build();
+
+        EventLoopGroup bossGroup = new NioEventLoopGroup(2);
+        EventLoopGroup workerGroup = new NioEventLoopGroup(4);
+        try {
+            ServerBootstrap b = new ServerBootstrap();
+            b.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .handler(new LoggingHandler(LogLevel.INFO))
+                    .childHandler(new Http2StaticFileServerInitializer(sslCtx));
+
+            Channel ch = b.bind(PORT).sync().channel();
+
+            System.out.println("Open your web browser and navigate to https://127.0.0.1:" + PORT + '/');
+
+            ch.closeFuture().sync();
+        } finally {
+            bossGroup.shutdownGracefully();
+            workerGroup.shutdownGracefully();
+        }
+    }
+}
diff --git a/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerHandler.java b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerHandler.java
new file mode 100644
index 00000000000..2f4ca8ae587
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerHandler.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2022 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   https://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 io.netty.example.http2.file;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelProgressiveFuture;
+import io.netty.channel.ChannelProgressiveFutureListener;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
+import io.netty.handler.codec.http2.Http2DataChunkedInput;
+import io.netty.handler.codec.http2.Http2DataFrame;
+import io.netty.handler.codec.http2.Http2FrameStream;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2HeadersFrame;
+import io.netty.handler.stream.ChunkedFile;
+import io.netty.util.CharsetUtil;
+import io.netty.util.internal.SystemPropertyUtil;
+
+import javax.activation.MimetypesFileTypeMap;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+import static io.netty.handler.codec.http.HttpMethod.GET;
+import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
+import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
+import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
+import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+
+/**
+ * A simple handler that serves incoming HTTP requests to send their respective
+ * HTTP responses.  It also implements {@code 'If-Modified-Since'} header to
+ * take advantage of browser cache, as described in
+ * <a href="https://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>.
+ *
+ * <h3>How Browser Caching Works</h3>
+ * <p>
+ * Web browser caching works with HTTP headers as illustrated by the following
+ * sample:
+ * <ol>
+ * <li>Request #1 returns the content of {@code /file1.txt}.</li>
+ * <li>Contents of {@code /file1.txt} is cached by the browser.</li>
+ * <li>Request #2 for {@code /file1.txt} does not return the contents of the
+ *     file again. Rather, a 304 Not Modified is returned. This tells the
+ *     browser to use the contents stored in its cache.</li>
+ * <li>The server knows the file has not been modified because the
+ *     {@code If-Modified-Since} date is the same as the file's last
+ *     modified date.</li>
+ * </ol>
+ *
+ * <pre>
+ * Request #1 Headers
+ * ===================
+ * GET /file1.txt HTTP/1.1
+ *
+ * Response #1 Headers
+ * ===================
+ * HTTP/1.1 200 OK
+ * Date:               Tue, 01 Mar 2011 22:44:26 GMT
+ * Last-Modified:      Wed, 30 Jun 2010 21:36:48 GMT
+ * Expires:            Tue, 01 Mar 2012 22:44:26 GMT
+ * Cache-Control:      private, max-age=31536000
+ *
+ * Request #2 Headers
+ * ===================
+ * GET /file1.txt HTTP/1.1
+ * If-Modified-Since:  Wed, 30 Jun 2010 21:36:48 GMT
+ *
+ * Response #2 Headers
+ * ===================
+ * HTTP/1.1 304 Not Modified
+ * Date:               Tue, 01 Mar 2011 22:44:28 GMT
+ *
+ * </pre>
+ */
+public class Http2StaticFileServerHandler extends ChannelDuplexHandler {
+
+    public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+    public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
+    public static final int HTTP_CACHE_SECONDS = 60;
+
+    private Http2FrameStream stream;
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (msg instanceof Http2HeadersFrame) {
+            Http2HeadersFrame headersFrame = (Http2HeadersFrame) msg;
+            this.stream = headersFrame.stream();
+
+            if (!GET.toString().equals(headersFrame.headers().method().toString())) {
+                sendError(ctx, METHOD_NOT_ALLOWED);
+                return;
+            }
+
+            final String uri = headersFrame.headers().path().toString();
+            final String path = sanitizeUri(uri);
+            if (path == null) {
+                sendError(ctx, FORBIDDEN);
+                return;
+            }
+
+            File file = new File(path);
+            if (file.isHidden() || !file.exists()) {
+                sendError(ctx, NOT_FOUND);
+                return;
+            }
+
+            if (file.isDirectory()) {
+                if (uri.endsWith("/")) {
+                    sendListing(ctx, file, uri);
+                } else {
+                    sendRedirect(ctx, uri + '/');
+                }
+                return;
+            }
+
+            if (!file.isFile()) {
+                sendError(ctx, FORBIDDEN);
+                return;
+            }
+
+            // Cache Validation
+            CharSequence ifModifiedSince = headersFrame.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
+            if (ifModifiedSince != null && !ifModifiedSince.toString().isEmpty()) {
+                SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
+                Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince.toString());
+
+                // 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) {
+                    sendNotModified(ctx);
+                    return;
+                }
+            }
+
+            RandomAccessFile raf;
+            try {
+                raf = new RandomAccessFile(file, "r");
+            } catch (FileNotFoundException ignore) {
+                sendError(ctx, NOT_FOUND);
+                return;
+            }
+            long fileLength = raf.length();
+
+            Http2Headers headers = new DefaultHttp2Headers();
+            headers.status("200");
+            headers.setLong(HttpHeaderNames.CONTENT_LENGTH, fileLength);
+
+            setContentTypeHeader(headers, file);
+            setDateAndCacheHeaders(headers, file);
+
+            // Write the initial line and the header.
+            ctx.writeAndFlush(new DefaultHttp2HeadersFrame(headers).stream(stream));
+
+            // Write the content.
+            ChannelFuture sendFileFuture;
+            sendFileFuture = ctx.writeAndFlush(new Http2DataChunkedInput(
+                    new ChunkedFile(raf, 0, fileLength, 8192), stream), ctx.newProgressivePromise());
+
+            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
+                @Override
+                public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
+                    if (total < 0) { // total unknown
+                        System.err.println(future.channel() + " Transfer progress: " + progress);
+                    } else {
+                        System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
+                    }
+                }
+
+                @Override
+                public void operationComplete(ChannelProgressiveFuture future) {
+                    System.err.println(future.channel() + " Transfer complete.");
+                }
+            });
+        } else {
+            // Unsupported message type
+            System.out.println("Unsupported message type: " + msg);
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        cause.printStackTrace();
+        if (ctx.channel().isActive()) {
+            sendError(ctx, INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
+
+    private static String sanitizeUri(String uri) throws UnsupportedEncodingException {
+        // Decode the path.
+        uri = URLDecoder.decode(uri, "UTF-8");
+
+        if (uri.isEmpty() || uri.charAt(0) != '/') {
+            return null;
+        }
+
+        // Convert file separators.
+        uri = uri.replace('/', File.separatorChar);
+
+        // Simplistic dumb security check.
+        // You will have to do something serious in the production environment.
+        if (uri.contains(File.separator + '.') ||
+                uri.contains('.' + File.separator) ||
+                uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' ||
+                INSECURE_URI.matcher(uri).matches()) {
+            return null;
+        }
+
+        // Convert to absolute path.
+        return SystemPropertyUtil.get("user.dir") + File.separator + uri;
+    }
+
+    private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");
+
+    private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
+        StringBuilder buf = new StringBuilder()
+                .append("<!DOCTYPE html>\r\n")
+                .append("<html><head><meta charset='utf-8' /><title>")
+                .append("Listing of: ")
+                .append(dirPath)
+                .append("</title></head><body>\r\n")
+
+                .append("<h3>Listing of: ")
+                .append(dirPath)
+                .append("</h3>\r\n")
+
+                .append("<ul>")
+                .append("<li><a href=\"../\">..</a></li>\r\n");
+
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for (File f : files) {
+                if (f.isHidden() || !f.canRead()) {
+                    continue;
+                }
+
+                String name = f.getName();
+                if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
+                    continue;
+                }
+
+                buf.append("<li><a href=\"")
+                        .append(name)
+                        .append("\">")
+                        .append(name)
+                        .append("</a></li>\r\n");
+            }
+        }
+
+        buf.append("</ul></body></html>\r\n");
+
+        ByteBuf buffer = ctx.alloc().buffer(buf.length());
+        buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8);
+
+        Http2Headers headers = new DefaultHttp2Headers();
+        headers.status(OK.toString());
+        headers.add(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
+
+        ctx.write(new DefaultHttp2HeadersFrame(headers).stream(stream));
+        ctx.writeAndFlush(new DefaultHttp2DataFrame(buffer, true).stream(stream));
+    }
+
+    private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
+        Http2Headers headers = new DefaultHttp2Headers();
+        headers.status(FOUND.toString());
+        headers.add(HttpHeaderNames.LOCATION, newUri);
+
+        ctx.writeAndFlush(new DefaultHttp2HeadersFrame(headers, true).stream(stream));
+    }
+
+    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
+        Http2Headers headers = new DefaultHttp2Headers();
+        headers.status(status.toString());
+        headers.add(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
+
+        Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers);
+        headersFrame.stream(stream);
+
+        Http2DataFrame dataFrame = new DefaultHttp2DataFrame(
+                Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8), true);
+        dataFrame.stream(stream);
+
+        ctx.write(headersFrame);
+        ctx.writeAndFlush(dataFrame);
+    }
+
+    /**
+     * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
+     *
+     * @param ctx Context
+     */
+    private void sendNotModified(ChannelHandlerContext ctx) {
+        Http2Headers headers = new DefaultHttp2Headers();
+        headers.status(NOT_MODIFIED.toString());
+        setDateHeader(headers);
+
+        ctx.writeAndFlush(new DefaultHttp2HeadersFrame(headers, true).stream(stream));
+    }
+
+    /**
+     * Sets the Date header for the HTTP response
+     *
+     * @param headers Http2 Headers
+     */
+    private static void setDateHeader(Http2Headers headers) {
+        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
+        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
+
+        Calendar time = new GregorianCalendar();
+        headers.set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
+    }
+
+    /**
+     * Sets the Date and Cache headers for the HTTP Response
+     *
+     * @param headers     Http2 Headers
+     * @param fileToCache file to extract content type
+     */
+    private static void setDateAndCacheHeaders(Http2Headers headers, File fileToCache) {
+        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
+        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
+
+        // Date header
+        Calendar time = new GregorianCalendar();
+        headers.set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
+
+        // Add cache headers
+        time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
+        headers.set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
+        headers.set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
+        headers.set(HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
+    }
+
+    /**
+     * Sets the content type header for the HTTP Response
+     *
+     * @param headers Http2 Headers
+     * @param file    file to extract content type
+     */
+    private static void setContentTypeHeader(Http2Headers headers, File file) {
+        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
+        headers.set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
+    }
+}
diff --git a/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerInitializer.java b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerInitializer.java
new file mode 100644
index 00000000000..c52215eda4d
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/file/Http2StaticFileServerInitializer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   https://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 io.netty.example.http2.file;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.stream.ChunkedWriteHandler;
+
+public class Http2StaticFileServerInitializer extends ChannelInitializer<SocketChannel> {
+
+    private final SslContext sslCtx;
+
+    public Http2StaticFileServerInitializer(SslContext sslCtx) {
+        this.sslCtx = sslCtx;
+    }
+
+    @Override
+    public void initChannel(SocketChannel ch) {
+        ChannelPipeline pipeline = ch.pipeline();
+        pipeline.addLast(sslCtx.newHandler(ch.alloc()));
+        pipeline.addLast(Http2FrameCodecBuilder.forServer().build());
+        pipeline.addLast(new ChunkedWriteHandler());
+        pipeline.addLast(new Http2StaticFileServerHandler());
+    }
+}


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DataChunkedInputTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DataChunkedInputTest.java
new file mode 100644
index 00000000000..26ea3fd05d9
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DataChunkedInputTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ * https://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 io.netty.handler.codec.http2;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.stream.ChunkedFile;
+import io.netty.handler.stream.ChunkedInput;
+import io.netty.handler.stream.ChunkedNioFile;
+import io.netty.handler.stream.ChunkedNioStream;
+import io.netty.handler.stream.ChunkedStream;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.internal.PlatformDependent;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.Channels;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Http2DataChunkedInputTest {
+    private static final byte[] BYTES = new byte[1024 * 64];
+    private static final File TMP;
+
+    // Just a dummy interface implementation of stream
+    private static final Http2FrameStream STREAM = new Http2FrameStream() {
+        @Override
+        public int id() {
+            return 1;
+        }
+
+        @Override
+        public Http2Stream.State state() {
+            return Http2Stream.State.OPEN;
+        }
+    };
+
+    static {
+        for (int i = 0; i < BYTES.length; i++) {
+            BYTES[i] = (byte) i;
+        }
+
+        FileOutputStream out = null;
+        try {
+            TMP = PlatformDependent.createTempFile("netty-chunk-", ".tmp", null);
+            TMP.deleteOnExit();
+            out = new FileOutputStream(TMP);
+            out.write(BYTES);
+            out.flush();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testChunkedStream() {
+        check(new Http2DataChunkedInput(new ChunkedStream(new ByteArrayInputStream(BYTES)), STREAM));
+    }
+
+    @Test
+    public void testChunkedNioStream() {
+        check(new Http2DataChunkedInput(new ChunkedNioStream(Channels.newChannel(new ByteArrayInputStream(BYTES))),
+                STREAM));
+    }
+
+    @Test
+    public void testChunkedFile() throws IOException {
+        check(new Http2DataChunkedInput(new ChunkedFile(TMP), STREAM));
+    }
+
+    @Test
+    public void testChunkedNioFile() throws IOException {
+        check(new Http2DataChunkedInput(new ChunkedNioFile(TMP), STREAM));
+    }
+
+    @Test
+    public void testWrappedReturnNull() throws Exception {
+        Http2DataChunkedInput input = new Http2DataChunkedInput(new ChunkedInput<ByteBuf>() {
+
+            @Override
+            public boolean isEndOfInput() throws Exception {
+                return false;
+            }
+
+            @Override
+            public void close() throws Exception {
+                // NOOP
+            }
+
+            @Override
+            public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
+                return null;
+            }
+
+            @Override
+            public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
+                return null;
+            }
+
+            @Override
+            public long length() {
+                return 0;
+            }
+
+            @Override
+            public long progress() {
+                return 0;
+            }
+        }, STREAM);
+        assertNull(input.readChunk(ByteBufAllocator.DEFAULT));
+    }
+
+    private static void check(ChunkedInput<?>... inputs) {
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        for (ChunkedInput<?> input : inputs) {
+            ch.writeOutbound(input);
+        }
+
+        assertTrue(ch.finish());
+
+        int i = 0;
+        int read = 0;
+        Http2DataFrame http2DataFrame = null;
+        for (;;) {
+            Http2DataFrame dataFrame = ch.readOutbound();
+            if (dataFrame == null) {
+                break;
+            }
+
+            ByteBuf buffer = dataFrame.content();
+            while (buffer.isReadable()) {
+                assertEquals(BYTES[i++], buffer.readByte());
+                read++;
+                if (i == BYTES.length) {
+                    i = 0;
+                }
+            }
+            buffer.release();
+
+            // Save last chunk
+            http2DataFrame = dataFrame;
+        }
+
+        assertEquals(BYTES.length * inputs.length, read);
+        assertNotNull(http2DataFrame);
+        assertTrue(http2DataFrame.isEndStream(), "Last chunk must be Http2DataFrame#isEndStream() set to true");
+    }
+}