@Override
    public HttpService asService() {
        return (ctx, req) -> {
            final HttpMethod method = ctx.method();
            if (method != HttpMethod.GET && method != HttpMethod.HEAD) {
                return HttpResponse.of(HttpStatus.METHOD_NOT_ALLOWED);
            }

            return HttpResponse.of(readAttributes(ctx.blockingTaskExecutor()).thenApply(attrs -> {
                if (attrs == null) {
                    return HttpResponse.of(HttpStatus.NOT_FOUND);
                }

                // See https://datatracker.ietf.org/doc/html/rfc7232#section-6 for more information
                // about how conditional requests are handled.

                // Handle 'if-none-match' header.
                final RequestHeaders reqHeaders = req.headers();
                final String etag = generateEntityTag(attrs);
                final String ifNoneMatch = reqHeaders.get(HttpHeaderNames.IF_NONE_MATCH);
                if (etag != null && ifNoneMatch != null) {
                    if ("*".equals(ifNoneMatch) || entityTagMatches(etag, ifNoneMatch)) {
                        return newNotModified(attrs, etag);
                    }
                }

                // Handle 'if-modified-since' header, only if 'if-none-match' does not exist.
                if (ifNoneMatch == null) {
                    try {
                        final Long ifModifiedSince =
                                reqHeaders.getTimeMillis(HttpHeaderNames.IF_MODIFIED_SINCE);
                        if (ifModifiedSince != null) {
                            // HTTP-date does not have subsecond-precision; add 999ms to it.
                            final long ifModifiedSinceMillis = LongMath.saturatedAdd(ifModifiedSince, 999);
                            if (attrs.lastModifiedMillis() <= ifModifiedSinceMillis) {
                                return newNotModified(attrs, etag);
                            }
                        }
                    } catch (Exception ignore) {
                        // Malformed date.
                    }
                }

                // Precondition did not match. Handle as usual.
                switch (ctx.method()) {
                    case HEAD:
                        final ResponseHeaders resHeaders = readHeaders(attrs);
                        if (resHeaders != null) {
                            return HttpResponse.of(resHeaders);
                        }
                        break;
                    case GET:
                        final HttpResponse res = read(ctx.blockingTaskExecutor(), ctx.alloc(), attrs);
                        if (res != null) {
                            return res;
                        }
                        break;
                    default:
                        throw new Error(); // Never reaches here.
                }

                // readHeaders() or read() returned null above.
                return HttpResponse.of(HttpStatus.NOT_FOUND);
            }));
        };
    }
Returns a new {@link HttpResponse} which streams the content of the file which follows the specified
{@link ResponseHeaders}.

@param headers the {@link ResponseHeaders}
@param length the content length. The returned {@link HttpResponse} must stream only as many bytes as
              this value.
@param fileReadExecutor the {@link Executor} which should be used for performing a blocking file I/O
@param alloc the {@link ByteBufAllocator} which should be used for allocating an input buffer

@return the {@link HttpResponse}, or {@code null} if the file does not exist.
@throws IOException if failed to open the file. Note that an I/O error which occurred during content
                    streaming will be notified via the returned {@link HttpResponse}'s error
                    notification mechanism.
core/src/main/java/com/linecorp/armeria/server/file/AbstractHttpFile.java